From b280441b7c82fede3cf4a160ece95da418afb591 Mon Sep 17 00:00:00 2001 From: rkodev <43806892+rkodev@users.noreply.github.com> Date: Wed, 18 May 2022 21:16:39 +0300 Subject: [PATCH 01/14] Refactor TS code snippet to interfaces --- .../ModelGraph/ModelGraphBuilderTests.cs | 12 + .../CodeSnippetsReflection.OpenAPI.csproj | 1 + .../LanguageGenerators/TypeScriptGenerator.cs | 373 ++++++------------ .../ModelGraph/CodeProperty.cs | 10 + .../ModelGraph/ModelGraphBuilder.cs | 249 ++++++++++++ .../ModelGraph/PropertyType.cs | 25 ++ .../ModelGraph/SnippetCodeGraph.cs | 67 ++++ .../OpenApiSchemaExtensions.cs | 3 +- 8 files changed, 497 insertions(+), 243 deletions(-) create mode 100644 CodeSnippetsReflection.OpenAPI.Test/ModelGraph/ModelGraphBuilderTests.cs create mode 100644 CodeSnippetsReflection.OpenAPI/ModelGraph/CodeProperty.cs create mode 100644 CodeSnippetsReflection.OpenAPI/ModelGraph/ModelGraphBuilder.cs create mode 100644 CodeSnippetsReflection.OpenAPI/ModelGraph/PropertyType.cs create mode 100644 CodeSnippetsReflection.OpenAPI/ModelGraph/SnippetCodeGraph.cs diff --git a/CodeSnippetsReflection.OpenAPI.Test/ModelGraph/ModelGraphBuilderTests.cs b/CodeSnippetsReflection.OpenAPI.Test/ModelGraph/ModelGraphBuilderTests.cs new file mode 100644 index 000000000..213bd51ae --- /dev/null +++ b/CodeSnippetsReflection.OpenAPI.Test/ModelGraph/ModelGraphBuilderTests.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CodeSnippetsReflection.OpenAPI.Test.ModelGraph +{ + public class ModelGraphBuilderTests + { + } +} diff --git a/CodeSnippetsReflection.OpenAPI/CodeSnippetsReflection.OpenAPI.csproj b/CodeSnippetsReflection.OpenAPI/CodeSnippetsReflection.OpenAPI.csproj index d3b8310d4..3fa375f73 100644 --- a/CodeSnippetsReflection.OpenAPI/CodeSnippetsReflection.OpenAPI.csproj +++ b/CodeSnippetsReflection.OpenAPI/CodeSnippetsReflection.OpenAPI.csproj @@ -8,6 +8,7 @@ + diff --git a/CodeSnippetsReflection.OpenAPI/LanguageGenerators/TypeScriptGenerator.cs b/CodeSnippetsReflection.OpenAPI/LanguageGenerators/TypeScriptGenerator.cs index 347accb98..e1e89a35b 100644 --- a/CodeSnippetsReflection.OpenAPI/LanguageGenerators/TypeScriptGenerator.cs +++ b/CodeSnippetsReflection.OpenAPI/LanguageGenerators/TypeScriptGenerator.cs @@ -5,6 +5,7 @@ using System.Text; using System.Text.Json; using System.Text.RegularExpressions; +using CodeSnippetsReflection.OpenAPI.ModelGraph; using CodeSnippetsReflection.StringExtensions; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Services; @@ -18,304 +19,192 @@ public class TypeScriptGenerator : ILanguageGenerator /// Model of the Snippets info /// String of the snippet in Javascript code - /// + /// - private const string clientVarName = "graphServiceClient"; - private const string clientVarType = "GraphServiceClient"; + private const string ClientVarName = "graphServiceClient"; + private const string ClientVarType = "GraphServiceClient"; + private const string RequestHeadersVarName = "headers"; + private const string RequestOptionsVarName = "options"; + private const string RequestConfigurationVarName = "configuration"; + private const string RequestParametersVarName = "requestParameters"; + private const string RequestBodyVarName = "requestBody"; public string GenerateCodeSnippet(SnippetModel snippetModel) { if (snippetModel == null) throw new ArgumentNullException("Argument snippetModel cannot be null"); - var indentManager = new IndentManager(); + + var codeGraph = ModelGraphBuilder.BuildCodeGraph(snippetModel); var snippetBuilder = new StringBuilder( "//THIS SNIPPET IS A PREVIEW FOR THE KIOTA BASED SDK. NON-PRODUCTION USE ONLY" + Environment.NewLine + - $"const {clientVarName} = {clientVarType}.init({{authProvider}});{Environment.NewLine}{Environment.NewLine}"); - var (requestPayload, payloadVarName) = GetRequestPayloadAndVariableName(snippetModel, indentManager); - snippetBuilder.Append(requestPayload); - var responseAssignment = snippetModel.ResponseSchema == null ? string.Empty : "const result = "; - - // add headers - var (requestHeadersPayload, requestHeadersVarName) = GetRequestHeaders(snippetModel, indentManager); - if (!string.IsNullOrEmpty(requestHeadersPayload)) snippetBuilder.Append(requestHeadersPayload); + $"const {ClientVarName} = {ClientVarType}.init({{authProvider}});{Environment.NewLine}{Environment.NewLine}"); - // add query parameters - var (queryParamsPayload, queryParamsVarName) = GetRequestQueryParameters(snippetModel, indentManager); - if (!string.IsNullOrEmpty(queryParamsPayload)) snippetBuilder.Append(queryParamsPayload); - - // add parameters - var parametersList = GetActionParametersList(payloadVarName, queryParamsVarName, requestHeadersVarName); - var methodName = snippetModel.Method.ToString().ToLower(); - snippetBuilder.AppendLine($"{responseAssignment}async () => {{"); - indentManager.Indent(); - snippetBuilder.AppendLine($"{indentManager.GetIndent()}await {clientVarName}.{GetFluentApiPath(snippetModel.PathNodes)}.{methodName}({parametersList});"); - indentManager.Unindent(); - snippetBuilder.AppendLine($"}}"); + writeSnippet(codeGraph, snippetBuilder); return snippetBuilder.ToString(); } - private const string RequestHeadersVarName = "headers"; - private static (string, string) GetRequestHeaders(SnippetModel snippetModel, IndentManager indentManager) + + private static void writeSnippet(SnippetCodeGraph codeGraph, StringBuilder builder) { - var payloadSB = new StringBuilder(); - var filteredHeaders = snippetModel.RequestHeaders.Where(h => !h.Key.Equals("Host", StringComparison.OrdinalIgnoreCase)) - .ToList(); - if (filteredHeaders.Any()) - { - payloadSB.AppendLine($"{indentManager.GetIndent()}const {RequestHeadersVarName} = {{"); - indentManager.Indent(); - filteredHeaders.ForEach(h => - payloadSB.AppendLine($"{indentManager.GetIndent()}\"{h.Key}\": \"{h.Value.FirstOrDefault().Replace("\"", "\\\"")}\",") - ); - indentManager.Unindent(); - payloadSB.AppendLine($"{indentManager.GetIndent()}}};"); - return (payloadSB.ToString(), RequestHeadersVarName); - } - return (default, default); + writeHeadersAndOptions(codeGraph, builder); + WriteParameters(codeGraph, builder); + WriteBody(codeGraph, builder); + builder.AppendLine(""); + + WriteExecutionStatement( + codeGraph, + builder, + codeGraph.HasBody() ? RequestBodyVarName : default, + codeGraph.HasParameters() ? RequestParametersVarName : default, + codeGraph.HasHeaders() || codeGraph.HasOptions() ? RequestConfigurationVarName : default + ); } - private static string GetActionParametersList(params string[] parameters) + + private static void writeHeadersAndOptions(SnippetCodeGraph codeGraph, StringBuilder builder) { - var nonEmptyParameters = parameters.Where(p => !string.IsNullOrEmpty(p)); - if (nonEmptyParameters.Any()) - return string.Join(", ", nonEmptyParameters.Aggregate((a, b) => $"{a}, {b}")); - else return string.Empty; + if (!codeGraph.HasHeaders() && !codeGraph.HasOptions()) return; + + var indentManager = new IndentManager(); + builder.AppendLine($"const {RequestConfigurationVarName} = {{"); + indentManager.Indent(); + WriteHeader(codeGraph, builder, indentManager); + WriteOptions(codeGraph, builder, indentManager); + indentManager.Unindent(); + builder.AppendLine($"{indentManager.GetIndent()}}};"); } - private const string RequestParametersVarName = "requestParameters"; - private static (string, string) GetRequestQueryParameters(SnippetModel model, IndentManager indentManager) + + private static void WriteHeader(SnippetCodeGraph codeGraph, StringBuilder builder, IndentManager indentManager) { - var payloadSB = new StringBuilder(); - if (!string.IsNullOrEmpty(model.QueryString)) + if (codeGraph.HasHeaders()) { - payloadSB.AppendLine($"{indentManager.GetIndent()}let {RequestParametersVarName} = {{"); + builder.AppendLine($"{indentManager.GetIndent()}{RequestHeadersVarName} : {{"); indentManager.Indent(); - var (queryString, replacements) = ReplaceNestedOdataQueryParameters(model.QueryString); - foreach (var queryParam in queryString.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries)) - { - if (queryParam.Contains("=")) - { - var kvPair = queryParam.Split('=', StringSplitOptions.RemoveEmptyEntries); - payloadSB.AppendLine($"{indentManager.GetIndent()}{NormalizeQueryParameterName(kvPair[0])} : {GetQueryParameterValue(kvPair[1], replacements)},"); - } - else - payloadSB.AppendLine($"q.{indentManager.GetIndent()}{NormalizeQueryParameterName(queryParam)} = undefined;"); - } + foreach (var param in codeGraph.Headers) + builder.AppendLine($"{indentManager.GetIndent()}\"{param.Name}\": \"{param.Value.Replace("\"", "\\\"")}\","); indentManager.Unindent(); - payloadSB.AppendLine($"{indentManager.GetIndent()}}};"); - return (payloadSB.ToString(), RequestParametersVarName); + builder.AppendLine($"{indentManager.GetIndent()}}}"); } - return (default, default); - } - private static Regex nestedStatementRegex = new(@"(\w+)(\([^)]+\))", RegexOptions.IgnoreCase|RegexOptions.Compiled); - private static (string, Dictionary) ReplaceNestedOdataQueryParameters(string queryParams) - { - var replacements = new Dictionary(); - var matches = nestedStatementRegex.Matches(queryParams); - if (matches.Any()) - foreach (Match match in matches) - { - var key = match.Groups[1].Value; - var value = match.Groups[2].Value; - replacements.Add(key, value); - queryParams = queryParams.Replace(value, string.Empty); - } - return (queryParams, replacements); } - private static string GetQueryParameterValue(string originalValue, Dictionary replacements) + private static void WriteOptions(SnippetCodeGraph codeGraph, StringBuilder builder, IndentManager indentManager) { - if (originalValue.Equals("true", StringComparison.OrdinalIgnoreCase) || originalValue.Equals("false", StringComparison.OrdinalIgnoreCase)) - return originalValue.ToLowerInvariant(); - else if (int.TryParse(originalValue, out var intValue)) - return intValue.ToString(); - else + if (codeGraph.HasOptions()) { - var valueWithNested = originalValue.Split(',') - .Select(v => replacements.ContainsKey(v) ? v + replacements[v] : v) - .Aggregate((a, b) => $"{a},{b}"); - return $"\"{valueWithNested}\""; + if (codeGraph.HasHeaders()) + builder.Append(","); + + builder.AppendLine($"{indentManager.GetIndent()}{RequestOptionsVarName} : {{"); + indentManager.Indent(); + foreach (var param in codeGraph.Options) + builder.AppendLine($"{indentManager.GetIndent()}\"{param.Name}\": \"{param.Value.Replace("\"", "\\\"")}\","); + indentManager.Unindent(); + builder.AppendLine($"{indentManager.GetIndent()}}}"); } } - private static string NormalizeQueryParameterName(string queryParam) => queryParam.TrimStart('$').ToFirstCharacterLowerCase(); - private const string RequestBodyVarName = "requestBody"; - private static (string, string) GetRequestPayloadAndVariableName(SnippetModel snippetModel, IndentManager indentManager) + + private static void WriteParameters(SnippetCodeGraph codeGraph, StringBuilder builder) { - if (string.IsNullOrWhiteSpace(snippetModel?.RequestBody)) - return (default, default); - if (indentManager == null) throw new ArgumentNullException(nameof(indentManager)); + if (!codeGraph.HasParameters()) return; - var payloadSB = new StringBuilder(); - switch (snippetModel.ContentType?.Split(';').First().ToLowerInvariant()) - { - case "application/json": - TryParseBody(snippetModel, payloadSB, indentManager); - break; - case "application/octet-stream": - payloadSB.AppendLine($"using var {RequestBodyVarName} = new WebStream();"); - break; - default: - if(TryParseBody(snippetModel, payloadSB, indentManager)) //in case the content type header is missing but we still have a json payload - break; - else - throw new InvalidOperationException($"Unsupported content type: {snippetModel.ContentType}"); - } - var result = payloadSB.ToString(); - return (result, string.IsNullOrEmpty(result) ? string.Empty : RequestBodyVarName); - } - private static bool TryParseBody(SnippetModel snippetModel, StringBuilder payloadSB, IndentManager indentManager) { - if(snippetModel.IsRequestBodyValid) - try { - using var parsedBody = JsonDocument.Parse(snippetModel.RequestBody, new JsonDocumentOptions { AllowTrailingCommas = true }); - var schema = snippetModel.RequestSchema; - var className = schema.GetSchemaTitle().ToFirstCharacterUpperCase() ?? $"{snippetModel.Path.Split("/").Last().ToFirstCharacterUpperCase()}RequestBody"; - payloadSB.AppendLine($"const {RequestBodyVarName} = new {className}();"); - WriteJsonObjectValue(RequestBodyVarName, payloadSB, parsedBody.RootElement, schema, indentManager); - return true; - } catch (Exception ex) when (ex is JsonException || ex is ArgumentException) { - // the payload wasn't json or poorly formatted - } - return false; + var indentManager = new IndentManager(); + builder.AppendLine($"const {RequestParametersVarName} = {{"); + indentManager.Indent(); + foreach (var param in codeGraph.Parameters) + builder.AppendLine($"{indentManager.GetIndent()}{param.Name} : {param.Value},"); + indentManager.Unindent(); + builder.AppendLine($"{indentManager.GetIndent()}}};"); } - private static void WriteAnonymousObjectValues(StringBuilder payloadSB, JsonElement value, OpenApiSchema schema, IndentManager indentManager, bool includePropertyAssignment = true) + + private static void WriteExecutionStatement(SnippetCodeGraph codeGraph, StringBuilder builder, params string[] parameters) { - if (value.ValueKind != JsonValueKind.Object) throw new InvalidOperationException($"Expected JSON object and got {value.ValueKind}"); - indentManager.Indent(); + var methodName = codeGraph.HttpMethod.ToString().ToLower(); + var responseAssignment = codeGraph.ResponseSchema == null ? string.Empty : "const result = "; - var propertiesAndSchema = value.EnumerateObject() - .Select(x => new Tuple(x, schema.GetPropertySchema(x.Name))); - foreach (var propertyAndSchema in propertiesAndSchema) - { - var propertyName = propertyAndSchema.Item1.Name.ToFirstCharacterLowerCase(); - var propertyAssignment = includePropertyAssignment ? $"{indentManager.GetIndent()} [\"{propertyName}\" , " : string.Empty; - WriteProperty(string.Empty, payloadSB, propertyAndSchema.Item1.Value, propertyAndSchema.Item2, indentManager, propertyAssignment, "]", ","); - } + var parametersList = GetActionParametersList(parameters); + var indentManager = new IndentManager(); + builder.AppendLine($"{responseAssignment}async () => {{"); + indentManager.Indent(); + builder.AppendLine($"{indentManager.GetIndent()}await {ClientVarName}.{GetFluentApiPath(codeGraph.Nodes)}.{methodName}({parametersList});"); indentManager.Unindent(); + builder.AppendLine($"}}"); } - private static void WriteJsonObjectValue(String objectName, StringBuilder payloadSB, JsonElement value, OpenApiSchema schema, IndentManager indentManager, bool includePropertyAssignment = true) + + private static void WriteBody(SnippetCodeGraph codeGraph, StringBuilder builder) { - if (value.ValueKind != JsonValueKind.Object) throw new InvalidOperationException($"Expected JSON object and got {value.ValueKind}"); - indentManager.Indent(); - var propertiesAndSchema = value.EnumerateObject() - .Select(x => new Tuple(x, schema.GetPropertySchema(x.Name))); - foreach (var propertyAndSchema in propertiesAndSchema.Where(x => x.Item2 != null)) + if (codeGraph.Body.PropertyType == PropertyType.Default) return; + + var indentManager = new IndentManager(); + + if (codeGraph.Body.PropertyType == PropertyType.Binary) { - var propertyName = propertyAndSchema.Item1.Name.ToFirstCharacterLowerCase(); - var propertyAssignment = includePropertyAssignment ? $"{objectName}.{propertyName} = " : string.Empty; - WriteProperty($"{objectName}.{propertyName}", payloadSB, propertyAndSchema.Item1.Value, propertyAndSchema.Item2, indentManager, propertyAssignment); + builder.AppendLine($"{indentManager.GetIndent()}const {RequestBodyVarName} = new WebStream();"); } - var propertiesWithoutSchema = propertiesAndSchema.Where(x => x.Item2 == null).Select(x => x.Item1); - if (propertiesWithoutSchema.Any()) + else { - payloadSB.AppendLine($"{objectName}.additionalData = {{"); + builder.AppendLine($"{indentManager.GetIndent()}const {RequestBodyVarName} : {codeGraph.Body.Name} = {{"); indentManager.Indent(); - - int elementIndex = 0; - var lastIndex = propertiesWithoutSchema.Count() - 1; - foreach (var property in propertiesWithoutSchema) - { - var propertyAssignment = $"{indentManager.GetIndent()} \"{property.Name}\" : "; - WriteProperty(objectName, payloadSB, property.Value, null, indentManager, propertyAssignment, "", (lastIndex == elementIndex) ? default : ","); - elementIndex++; - } + WriteCodePropertyObject(builder, codeGraph.Body, indentManager); indentManager.Unindent(); - payloadSB.AppendLine($"{indentManager.GetIndent()} }}"); + builder.AppendLine($"}};"); } - indentManager.Unindent(); } - private static void WriteProperty(String objectName, StringBuilder payloadSB, JsonElement value, OpenApiSchema propSchema, IndentManager indentManager, string propertyAssignment, string propertySuffix = default, string terminateLine = ";", bool objectPropertyAssign = true) + + private static void WriteCodePropertyObject(StringBuilder builder, CodeProperty codeProperty, IndentManager indentManager) { - switch (value.ValueKind) + foreach (var child in codeProperty.Children) { - case JsonValueKind.String: - if (propSchema?.Format?.Equals("base64url", StringComparison.OrdinalIgnoreCase) ?? false) - payloadSB.AppendLine($"{propertyAssignment}btoa(\"{value.GetString()}\"){propertySuffix}{terminateLine}"); - else if (propSchema?.Format?.Equals("date-time", StringComparison.OrdinalIgnoreCase) ?? false) - payloadSB.AppendLine($"{propertyAssignment} new Date(\"{value.GetString()}\"){propertySuffix}{terminateLine}"); - else - { - var enumSchema = propSchema?.AnyOf.FirstOrDefault(x => x.Enum.Count > 0); - if(enumSchema == null) + switch (child.PropertyType) + { + case PropertyType.Object: + case PropertyType.Map: + if (codeProperty.PropertyType == PropertyType.Array) { - payloadSB.AppendLine($"{propertyAssignment}\"{value.GetString()}\"{propertySuffix}{terminateLine}"); + builder.AppendLine($"{indentManager.GetIndent()}{{"); } else { - payloadSB.AppendLine($"{propertyAssignment}{enumSchema.Title.ToFirstCharacterUpperCase()}.{value.GetString().ToFirstCharacterUpperCase()}{propertySuffix}{terminateLine}"); + builder.AppendLine($"{indentManager.GetIndent()}{child.Name.ToFirstCharacterLowerCase()} : {{"); } - } - break; - case JsonValueKind.Number: - payloadSB.AppendLine($"{propertyAssignment}{value}{propertySuffix}{terminateLine}"); - break; - case JsonValueKind.False: - case JsonValueKind.True: - payloadSB.AppendLine($"{propertyAssignment}{value.GetBoolean().ToString().ToLowerInvariant()}{propertySuffix}{terminateLine}"); - break; - case JsonValueKind.Null: - payloadSB.AppendLine($"{propertyAssignment}null{propertySuffix},"); - break; - case JsonValueKind.Object: - if (propSchema != null) - { - if(objectPropertyAssign) - payloadSB.AppendLine($"{propertyAssignment}new {propSchema.GetSchemaTitle().ToFirstCharacterUpperCase()}(){terminateLine}"); - - WriteJsonObjectValue(objectName, payloadSB, value, propSchema, indentManager); - } - else - { - WriteAnonymousObjectValues(payloadSB, value, propSchema, indentManager); - } - break; - case JsonValueKind.Array: - WriteJsonArrayValue(objectName, payloadSB, value, propSchema, indentManager, propertyAssignment, "],"); - break; - default: - throw new NotImplementedException($"Unsupported JsonValueKind: {value.ValueKind}"); - } - } - private static void WriteJsonArrayValue(String objectName, StringBuilder payloadSB, JsonElement value, OpenApiSchema schema, IndentManager indentManager, string propertyAssignment, string terminateLine) - { - indentManager.Indent(2); + indentManager.Indent(); + WriteCodePropertyObject(builder, child, indentManager); + indentManager.Unindent(); + builder.AppendLine($"{indentManager.GetIndent()}}},"); - // for an array of objects the properties should be written after all the objects have be initiated and not before - var itemsBuilder = new StringBuilder(); - var arrayListBuilder = new StringBuilder(); - - int elementIndex = 0; - var elements = value.EnumerateArray(); - var lastIndex = elements.Count() - 1; - foreach (var item in elements) - { - if (item.ValueKind == JsonValueKind.Object) - { - var termination = (lastIndex == elementIndex) ? default : ","; + break; + case PropertyType.Array: - var elementName = $"{schema.GetSchemaTitle().ToLowerInvariant()}{(elementIndex == 0 ? default : elementIndex.ToString())}"; - itemsBuilder.AppendLine($"const {elementName} = new {schema.GetSchemaTitle().ToFirstCharacterUpperCase()}();"); + builder.AppendLine($"{indentManager.GetIndent()}{child.Name} : ["); + indentManager.Indent(); + WriteCodePropertyObject(builder, child, indentManager); + indentManager.Unindent(); + builder.AppendLine($"{indentManager.GetIndent()}],"); - // create a new object - WriteProperty(elementName, itemsBuilder, item, schema, indentManager, default, default, termination, false); - arrayListBuilder.AppendLine($"{indentManager.GetIndent()}{elementName}{termination}"); - } - else - { - WriteProperty(objectName, arrayListBuilder, item, schema, indentManager, indentManager.GetIndent(), default, (lastIndex == elementIndex) ? default : ","); + break; + case PropertyType.String: + var propName = codeProperty.PropertyType == PropertyType.Map ? $"\"{child.Name.ToFirstCharacterLowerCase()}\"" : child.Name.ToFirstCharacterLowerCase(); + builder.AppendLine($"{indentManager.GetIndent()}{propName} : \"{child.Value}\","); + break; + case PropertyType.Enum: + builder.AppendLine($"{indentManager.GetIndent()}{child.Name.ToFirstCharacterLowerCase()} : {child.Value},"); + break; + case PropertyType.Date: + builder.AppendLine($"{indentManager.GetIndent()}{child.Name} : new Date(\"{child.Value}\"),"); + break; + default: + builder.AppendLine($"{indentManager.GetIndent()}{child.Name.ToFirstCharacterLowerCase()} : {child.Value.ToFirstCharacterLowerCase()},"); + break; } - elementIndex++; } + } - - payloadSB.Append(itemsBuilder.ToString()); - - payloadSB.AppendLine($"{propertyAssignment}["); - payloadSB.Append(arrayListBuilder.ToString()); - indentManager.Unindent(); - payloadSB.AppendLine($"{indentManager.GetIndent()}]"); - indentManager.Unindent(); - - - + private static string GetActionParametersList(params string[] parameters) + { + var nonEmptyParameters = parameters.Where(p => !string.IsNullOrEmpty(p)); + if (nonEmptyParameters.Any()) + return string.Join(", ", nonEmptyParameters.Aggregate((a, b) => $"{a}, {b}")); + else return string.Empty; } + private static string GetFluentApiPath(IEnumerable nodes) { if (!(nodes?.Any() ?? false)) return string.Empty; diff --git a/CodeSnippetsReflection.OpenAPI/ModelGraph/CodeProperty.cs b/CodeSnippetsReflection.OpenAPI/ModelGraph/CodeProperty.cs new file mode 100644 index 000000000..8d9378049 --- /dev/null +++ b/CodeSnippetsReflection.OpenAPI/ModelGraph/CodeProperty.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CodeSnippetsReflection.OpenAPI.ModelGraph +{ + public record struct CodeProperty(string Name, string Value, List Children, PropertyType PropertyType = PropertyType.String); +} diff --git a/CodeSnippetsReflection.OpenAPI/ModelGraph/ModelGraphBuilder.cs b/CodeSnippetsReflection.OpenAPI/ModelGraph/ModelGraphBuilder.cs new file mode 100644 index 000000000..cc2402286 --- /dev/null +++ b/CodeSnippetsReflection.OpenAPI/ModelGraph/ModelGraphBuilder.cs @@ -0,0 +1,249 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using CodeSnippetsReflection.StringExtensions; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; + +namespace CodeSnippetsReflection.OpenAPI.ModelGraph +{ + + public static class ModelGraphBuilder + { + + private static readonly Regex nestedStatementRegex = new(@"(\w+)(\([^)]+\))", RegexOptions.IgnoreCase | RegexOptions.Compiled); + + private static readonly CodeProperty EMPTY_PROPERTY = new() { Name = null , Value = null , Children = null, PropertyType = PropertyType.Default }; + + private static readonly CodeProperty BINARY_PROPERTY = new() { Name = null, Value = null, Children = null, PropertyType = PropertyType.Binary }; + + public static SnippetCodeGraph BuildCodeGraph(SnippetModel snippetModel) + { + return new SnippetCodeGraph { + ResponseSchema = snippetModel.ResponseSchema, + HttpMethod = snippetModel.Method, + Nodes = snippetModel.PathNodes, + Headers = parseHeaders(snippetModel), + Options = parseOptions(snippetModel), + Parameters = buildParameters(snippetModel), + Body = buildBody(snippetModel) + }; + } + + /*** + * Returns headers filtering `Host` out + */ + private static IEnumerable parseHeaders(SnippetModel snippetModel) + { + return snippetModel.RequestHeaders.Where(h => !h.Key.Equals("Host", StringComparison.OrdinalIgnoreCase)) + .Select(h => new CodeProperty { Name = h.Key , Value = h.Value.FirstOrDefault(), Children = null, PropertyType = PropertyType.String }) + .ToList(); + } + + /// TODO Add support for options + private static List parseOptions(SnippetModel snippetModel) + { + return new List(); + } + + + private static List buildParameters(SnippetModel snippetModel) + { + var parameters = new List(); + if (!string.IsNullOrEmpty(snippetModel.QueryString)) + { + var (queryString, replacements) = ReplaceNestedOdataQueryParameters(snippetModel.QueryString); + foreach (var queryParam in queryString.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries)) + { + if (queryParam.Contains("=")) + { + var kvPair = queryParam.Split('=', StringSplitOptions.RemoveEmptyEntries); + parameters.Add(new() { Name = NormalizeQueryParameterName(kvPair[0]), Value = GetQueryParameterValue(kvPair[1], replacements) }); + } + else + parameters.Add(new() { Name = NormalizeQueryParameterName(queryParam), Value = GetQueryParameterValue("undefined", replacements) }); + } + + } + return parameters; + } + + private static string NormalizeQueryParameterName(string queryParam) => queryParam.TrimStart('$').ToFirstCharacterLowerCase(); + + private static (string, Dictionary) ReplaceNestedOdataQueryParameters(string queryParams) + { + var replacements = new Dictionary(); + var matches = nestedStatementRegex.Matches(queryParams); + if (matches.Any()) + foreach (Match match in matches) + { + var key = match.Groups[1].Value; + var value = match.Groups[2].Value; + replacements.Add(key, value); + queryParams = queryParams.Replace(value, string.Empty); + } + return (queryParams, replacements); + } + + private static string GetQueryParameterValue(string originalValue, Dictionary replacements) + { + if (originalValue.Equals("true", StringComparison.OrdinalIgnoreCase) || originalValue.Equals("false", StringComparison.OrdinalIgnoreCase)) + return originalValue.ToLowerInvariant(); + else if (int.TryParse(originalValue, out var intValue)) + return intValue.ToString(); + else + { + var valueWithNested = originalValue.Split(',') + .Select(v => replacements.ContainsKey(v) ? v + replacements[v] : v) + .Aggregate((a, b) => $"{a},{b}"); + return $"\"{valueWithNested}\""; + } + } + + private static CodeProperty buildBody(SnippetModel snippetModel) + { + if (string.IsNullOrWhiteSpace(snippetModel?.RequestBody)) + return EMPTY_PROPERTY; + + switch (snippetModel.ContentType?.Split(';').First().ToLowerInvariant()) + { + case "application/json": + return TryParseBody(snippetModel); + case "application/octet-stream": + return BINARY_PROPERTY; + default: + return TryParseBody(snippetModel);//in case the content type header is missing but we still have a json payload + } + } + + private static CodeProperty TryParseBody(SnippetModel snippetModel) + { + if (!snippetModel.IsRequestBodyValid) + throw new InvalidOperationException($"Unsupported content type: {snippetModel.ContentType}"); + + using var parsedBody = JsonDocument.Parse(snippetModel.RequestBody, new JsonDocumentOptions { AllowTrailingCommas = true }); + var schema = snippetModel.RequestSchema; + var className = schema.GetSchemaTitle().ToFirstCharacterUpperCase() ?? $"{snippetModel.Path.Split("/").Last().ToFirstCharacterUpperCase()}RequestBody"; + return parseJsonObjectValue(className, parsedBody.RootElement, schema, snippetModel); + } + + private static CodeProperty parseJsonObjectValue(String rootPropertyName, JsonElement value, OpenApiSchema schema, SnippetModel snippetModel = null) + { + var children = new List(); + + if (value.ValueKind != JsonValueKind.Object) throw new InvalidOperationException($"Expected JSON object and got {value.ValueKind}"); + + var propertiesAndSchema = value.EnumerateObject() + .Select(x => new Tuple(x, schema.GetPropertySchema(x.Name))); + foreach (var propertyAndSchema in propertiesAndSchema.Where(x => x.Item2 != null)) + { + var propertyName = propertyAndSchema.Item1.Name.ToFirstCharacterLowerCase(); + children.Add(parseProperty(propertyName, propertyAndSchema.Item1.Value, propertyAndSchema.Item2)); + } + + var propertiesWithoutSchema = propertiesAndSchema.Where(x => x.Item2 == null).Select(x => x.Item1); + if (propertiesWithoutSchema.Any()) + { + + var additionalChildren = new List(); + foreach (var property in propertiesWithoutSchema) + { + var propName = property.Name; + if (string.IsNullOrEmpty(propName) || (property.Value.ValueKind != JsonValueKind.Object)) + { + additionalChildren.Add(parseProperty(propName, property.Value, null)); + } + else + { + children.Add(parseProperty(propName, property.Value, null)); + } + } + + if (additionalChildren.Any() == true) + { + var propertyName = "additionalData"; + var codeProperty = new CodeProperty { Name = propertyName, PropertyType = PropertyType.Map, Children = additionalChildren }; + children.Add(codeProperty); + } + } + + return new CodeProperty { Name = rootPropertyName, PropertyType = PropertyType.Object, Children = children }; + } + + private static CodeProperty parseProperty(string propertyName, JsonElement value, OpenApiSchema propSchema) + { + switch (value.ValueKind) + { + case JsonValueKind.String: + if (propSchema?.Format?.Equals("base64url", StringComparison.OrdinalIgnoreCase) ?? false) + { + return new CodeProperty { Name = propertyName, Value = value.GetString(), PropertyType = PropertyType.Base64Url, Children = new List() }; + } + else if (propSchema?.Format?.Equals("date-time", StringComparison.OrdinalIgnoreCase) ?? false) + { + return new CodeProperty { Name = propertyName, Value = value.GetString(), PropertyType = PropertyType.Date, Children = new List() }; + } + else + { + var enumSchema = propSchema?.AnyOf.FirstOrDefault(x => x.Enum.Count > 0); + if (enumSchema == null) + { + return new CodeProperty { Name = propertyName, Value = value.GetString(), PropertyType = PropertyType.String, Children = new List() }; + } + else + { + var propValue = $"{enumSchema.Title.ToFirstCharacterUpperCase()}.{value.GetString().ToFirstCharacterUpperCase()}"; + return new CodeProperty { Name = propertyName, Value = propValue, PropertyType = PropertyType.Enum, Children = new List() }; + } + + } + case JsonValueKind.Number: + return new CodeProperty { Name = propertyName, Value = $"{value}", PropertyType = PropertyType.Number, Children = new List() }; + case JsonValueKind.False: + case JsonValueKind.True: + return new CodeProperty { Name = propertyName, Value = value.GetBoolean().ToString(), PropertyType = PropertyType.Boolean, Children = new List() }; + case JsonValueKind.Null: + return new CodeProperty { Name = propertyName, Value = "null", PropertyType = PropertyType.Null, Children = new List() }; + case JsonValueKind.Object: + if (propSchema != null) + { + return parseJsonObjectValue(propertyName, value, propSchema) ; + } + else + { + return parseAnonymousObjectValues(propertyName, value, propSchema); + } + case JsonValueKind.Array: + return parseJsonArrayValue(propertyName, value, propSchema); + default: + throw new NotImplementedException($"Unsupported JsonValueKind: {value.ValueKind}"); + } + } + + private static CodeProperty parseJsonArrayValue(string propertyName, JsonElement value, OpenApiSchema schema) + { + var children = value.EnumerateArray().Select(item => parseProperty(schema.GetSchemaTitle().ToFirstCharacterUpperCase(), item, schema)).ToList(); + return new CodeProperty { Name = propertyName, Value = null, PropertyType = PropertyType.Array, Children = children }; + } + + private static CodeProperty parseAnonymousObjectValues(string propertyName, JsonElement value, OpenApiSchema schema) + { + if (value.ValueKind != JsonValueKind.Object) throw new InvalidOperationException($"Expected JSON object and got {value.ValueKind}"); + + var children = new List(); + var propertiesAndSchema = value.EnumerateObject() + .Select(x => new Tuple(x, schema.GetPropertySchema(x.Name))); + foreach (var propertyAndSchema in propertiesAndSchema) + { + children.Add(parseProperty(propertyAndSchema.Item1.Name.ToFirstCharacterLowerCase(), propertyAndSchema.Item1.Value, propertyAndSchema.Item2)); + } + + return new CodeProperty { Name = propertyName, Value = null, PropertyType = PropertyType.Object, Children = children }; + } + } +} diff --git a/CodeSnippetsReflection.OpenAPI/ModelGraph/PropertyType.cs b/CodeSnippetsReflection.OpenAPI/ModelGraph/PropertyType.cs new file mode 100644 index 000000000..b411c255c --- /dev/null +++ b/CodeSnippetsReflection.OpenAPI/ModelGraph/PropertyType.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CodeSnippetsReflection.OpenAPI.ModelGraph +{ + public enum PropertyType + { + // Empty object + Default, + String , + Number, + Date , + Boolean , + Null, + Enum , + Object, + Base64Url, + Binary, + Array, + Map + } +} diff --git a/CodeSnippetsReflection.OpenAPI/ModelGraph/SnippetCodeGraph.cs b/CodeSnippetsReflection.OpenAPI/ModelGraph/SnippetCodeGraph.cs new file mode 100644 index 000000000..3b1551644 --- /dev/null +++ b/CodeSnippetsReflection.OpenAPI/ModelGraph/SnippetCodeGraph.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Services; + +namespace CodeSnippetsReflection.OpenAPI.ModelGraph +{ + public class SnippetCodeGraph + { + public OpenApiSchema ResponseSchema + { + get; set; + } + public HttpMethod HttpMethod + { + get; set; + } + + public IEnumerable Headers + { + get; set; + } + public IEnumerable Options + { + get; set; + } + + public IEnumerable Parameters + { + get; set; + } + + public CodeProperty Body + { + get; set; + } + + public IEnumerable Nodes + { + get; set; + } + + + public Boolean HasHeaders() + { + return Headers.Any() == true; + } + + public Boolean HasOptions() + { + return Options.Any() == true; + } + + public Boolean HasParameters() + { + return Parameters.Any() == true; + } + + public Boolean HasBody() + { + return Body.PropertyType != PropertyType.Default; + } + + } +} diff --git a/CodeSnippetsReflection.OpenAPI/OpenApiSchemaExtensions.cs b/CodeSnippetsReflection.OpenAPI/OpenApiSchemaExtensions.cs index 81994ff1b..8f6805304 100644 --- a/CodeSnippetsReflection.OpenAPI/OpenApiSchemaExtensions.cs +++ b/CodeSnippetsReflection.OpenAPI/OpenApiSchemaExtensions.cs @@ -13,7 +13,8 @@ public static IEnumerable> GetAllProperties( return schema.Properties .Union(schema.AllOf.FlattenEmptyEntries(x => x.AllOf, 2).SelectMany(x => x.Properties)) .Union(schema.AnyOf.SelectMany(x => x.Properties)) - .Union(schema.OneOf.SelectMany(x => x.Properties)); + .Union(schema.OneOf.SelectMany(x => x.Properties)) + .Union(schema.Items != null ? schema.Items.AllOf.SelectMany(x => x.Properties) : Enumerable.Empty>()); } return schema.AllOf.Union(schema.AnyOf).Union(schema.OneOf).SelectMany(x => x.GetAllProperties()); } From f7b22fad98678c5b276f148ca8bcf8d8bc0c833d Mon Sep 17 00:00:00 2001 From: rkodev <43806892+rkodev@users.noreply.github.com> Date: Thu, 26 May 2022 11:45:17 +0300 Subject: [PATCH 02/14] Adds unit tests to ModelGraphBuilder --- .../ModelGraph/ModelGraphBuilderTests.cs | 283 ++++++++++++++++++ .../TypeScriptGeneratorTest.cs | 27 +- .../ModelGraph/CodeProperty.cs | 1 + .../ModelGraph/ModelGraphBuilder.cs | 29 +- 4 files changed, 314 insertions(+), 26 deletions(-) diff --git a/CodeSnippetsReflection.OpenAPI.Test/ModelGraph/ModelGraphBuilderTests.cs b/CodeSnippetsReflection.OpenAPI.Test/ModelGraph/ModelGraphBuilderTests.cs index 213bd51ae..75c873c34 100644 --- a/CodeSnippetsReflection.OpenAPI.Test/ModelGraph/ModelGraphBuilderTests.cs +++ b/CodeSnippetsReflection.OpenAPI.Test/ModelGraph/ModelGraphBuilderTests.cs @@ -1,12 +1,295 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Text; using System.Threading.Tasks; +using CodeSnippetsReflection.OpenAPI.ModelGraph; +using Microsoft.OpenApi.Services; +using Xunit; namespace CodeSnippetsReflection.OpenAPI.Test.ModelGraph { public class ModelGraphBuilderTests { + private const string ServiceRootUrl = "https://graph.microsoft.com/v1.0"; + private static OpenApiUrlTreeNode _v1TreeNode; + + private static string TypesSample = @" + { + ""attendees"": [ + { + ""type"": ""null"", + ""emailAddress"": { + ""name"": ""Alex Wilbur"", + ""address"": ""alexw@contoso.onmicrosoft.com"" + } + } + ], + ""locationConstraint"": { + ""isRequired"": false, + ""suggestLocation"": false, + ""locations"": [ + { + ""resolveAvailability"": false, + ""displayName"": ""Conf room Hood"" + } + ] + }, + ""timeConstraint"": { + ""activityDomain"":""work"", + ""timeSlots"": [ + { + ""start"": { + ""dateTime"": ""2019-04-16T09:00:00"", + ""timeZone"": ""Pacific Standard Time"" + }, + ""end"": { + ""dateTime"": ""2019-04-18T17:00:00"", + ""timeZone"": ""Pacific Standard Time"" + } + } + ] + }, + ""isOrganizerOptional"": ""false"", + ""meetingDuration"": ""PT1H"", + ""returnSuggestionReasons"": ""true"", + ""minimumAttendeePercentage"": 100 + } + "; + + // read the file from disk + private static async Task GetV1TreeNode() + { + if (_v1TreeNode == null) + { + _v1TreeNode = await SnippetModelTests.GetTreeNode("https://raw.githubusercontent.com/microsoftgraph/msgraph-metadata/master/openapi/v1.0/openapi.yaml"); + } + return _v1TreeNode; + } + + [Fact] + public async Task ParsesHeaders() + { + using var request = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/users"); + + request.Headers.Add("Host", "graph.microsoft.com"); + request.Headers.Add("Prefer", "outlook.timezone=\"Pacific Standard Time\""); + + var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); + var result = ModelGraphBuilder.BuildCodeGraph(snippetModel); + var header = result.Headers.First(); + + Assert.True(result.HasHeaders()); + Assert.Single(result.Headers); // host should be ignored in headers + Assert.Equal("outlook.timezone=\"Pacific Standard Time\"", header.Value); + Assert.Equal("Prefer", header.Name); + Assert.Equal(PropertyType.String, header.PropertyType); + } + + [Fact] + public async Task HasHeadeIsFalseWhenNoneIsInRequest() + { + using var request = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/users"); + request.Headers.Add("Host", "graph.microsoft.com"); + + var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); + var result = ModelGraphBuilder.BuildCodeGraph(snippetModel); + + Assert.False(result.HasHeaders()); + } + + [Fact] + public async Task ParsesParameters() + { + using var request = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/users/19:4b6bed8d24574f6a9e436813cb2617d8?$select=displayName,givenName,postalCode,identities"); + + var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); + var result = ModelGraphBuilder.BuildCodeGraph(snippetModel); + var parameter = result.Parameters.First(); + + Assert.True(result.HasParameters()); + Assert.Single(result.Parameters); + + var expectedProperty = new CodeProperty { Name = "select", Value = "displayName,givenName,postalCode,identities", PropertyType = PropertyType.String, Children = null }; + Assert.Equal(expectedProperty, parameter); + + Assert.Equal("displayName,givenName,postalCode,identities", parameter.Value); + Assert.Equal("select", parameter.Name); + Assert.Equal(PropertyType.String, parameter.PropertyType); + + Assert.True(result.HasParameters()); + } + + [Fact] + public async Task HasParametersIsFalseWhenNoParamterExists() + { + using var request = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/users/19:4b6bed8d24574f6a9e436813cb2617d8"); + + var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); + var result = ModelGraphBuilder.BuildCodeGraph(snippetModel); + + Assert.False(result.HasParameters()); + } + + [Fact] + public async Task ParsesBodyTypeBinary() + { + using var request = new HttpRequestMessage(HttpMethod.Put, $"{ServiceRootUrl}/applications/{{application-id}}/logo") + { + Content = new ByteArrayContent(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10 }) + }; + request.Content.Headers.ContentType = new("application/octet-stream"); + + var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); + var result = ModelGraphBuilder.BuildCodeGraph(snippetModel); + + + Assert.NotNull(result.Body); + Assert.Equal(PropertyType.Binary, result.Body.PropertyType); + + } + + [Fact] + public async Task ParsesBodyWithoutProperContentType() + { + + var sampleBody = @" + { + ""createdDateTime"": ""2019-02-04T19:58:15.511Z"" + } + "; + + using var request = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/teams/team-id/channels/19:4b6bed8d24574f6a9e436813cb2617d8@thread.tacv2/messages") + { + Content = new StringContent(sampleBody, Encoding.UTF8) // snippet missing content type + }; + var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); + var result = ModelGraphBuilder.BuildCodeGraph(snippetModel); + ; + + var expectedObject = new CodeProperty { Name = "MessagesRequestBody", Value = null, PropertyType = PropertyType.Object, Children = new List() }; + + Assert.Equal(expectedObject.Name, result.Body.Name); + Assert.Equal(expectedObject.Value, result.Body.Value); + Assert.Equal(expectedObject.PropertyType, result.Body.PropertyType); + } + + private CodeProperty? findProperyInSnipet(CodeProperty codeProperty, string name) + { + if (codeProperty.Name == name) return codeProperty; + + if (codeProperty.Children.Any()) + { + foreach (var param in codeProperty.Children) + { + var result = findProperyInSnipet(param, name); + if (result != null) return result; + } + } + + return null; + } + + [Fact] + public async Task ParsesBodyPropertyTypeString() + { + using var request = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/me/findMeetingTimes") + { + Content = new StringContent(TypesSample, Encoding.UTF8) + }; + var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); + var snippetCodeGraph = ModelGraphBuilder.BuildCodeGraph(snippetModel); + + // meetingDuration should be a string + var property = findProperyInSnipet(snippetCodeGraph.Body, "meetingDuration"); + + Assert.NotNull(property); + Assert.Equal(PropertyType.String, property?.PropertyType); + Assert.Equal("PT1H", property?.Value); + } + + [Fact] + public async Task ParsesBodyPropertyTypeNumber() + { + using var request = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/me/findMeetingTimes") + { + Content = new StringContent(TypesSample, Encoding.UTF8) + }; + var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); + var snippetCodeGraph = ModelGraphBuilder.BuildCodeGraph(snippetModel); + + var property = findProperyInSnipet(snippetCodeGraph.Body, "minimumAttendeePercentage"); + + Assert.NotNull(property); + Assert.Equal(PropertyType.Number, property?.PropertyType); + Assert.Equal("100" , property?.Value); + } + + [Fact] + public async Task ParsesBodyPropertyTypeBoolean() + { + using var request = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/me/findMeetingTimes") + { + Content = new StringContent(TypesSample, Encoding.UTF8) + }; + var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); + var snippetCodeGraph = ModelGraphBuilder.BuildCodeGraph(snippetModel); + + var property = findProperyInSnipet(snippetCodeGraph.Body, "suggestLocation"); + + Assert.NotNull(property); + Assert.Equal(PropertyType.Boolean, property?.PropertyType); + Assert.Equal("False", property?.Value); + } + + [Fact] + public async Task ParsesBodyPropertyTypeObject() + { + using var request = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/me/findMeetingTimes") + { + Content = new StringContent(TypesSample, Encoding.UTF8) + }; + var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); + var snippetCodeGraph = ModelGraphBuilder.BuildCodeGraph(snippetModel); + + var property = findProperyInSnipet(snippetCodeGraph.Body, "locationConstraint"); + + Assert.NotNull(property); + Assert.Equal(PropertyType.Object, property?.PropertyType); + } + + [Fact] + public async Task ParsesBodyPropertyTypeArray() + { + using var request = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/me/findMeetingTimes") + { + Content = new StringContent(TypesSample, Encoding.UTF8) + }; + var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); + var snippetCodeGraph = ModelGraphBuilder.BuildCodeGraph(snippetModel); + + var property = findProperyInSnipet(snippetCodeGraph.Body, "attendees"); + + Assert.NotNull(property); + Assert.Equal(PropertyType.Array, property?.PropertyType); + } + + [Fact] + public async Task ParsesBodyPropertyTypeMap() + { + using var request = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/me/findMeetingTimes") + { + Content = new StringContent(TypesSample, Encoding.UTF8) + }; + var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); + var snippetCodeGraph = ModelGraphBuilder.BuildCodeGraph(snippetModel); + + var property = findProperyInSnipet(snippetCodeGraph.Body, "additionalData"); + + Assert.NotNull(property); + Assert.Equal(PropertyType.Map, property?.PropertyType); + } } + } diff --git a/CodeSnippetsReflection.OpenAPI.Test/TypeScriptGeneratorTest.cs b/CodeSnippetsReflection.OpenAPI.Test/TypeScriptGeneratorTest.cs index d6694ae76..f58f4d1a1 100644 --- a/CodeSnippetsReflection.OpenAPI.Test/TypeScriptGeneratorTest.cs +++ b/CodeSnippetsReflection.OpenAPI.Test/TypeScriptGeneratorTest.cs @@ -111,22 +111,29 @@ public async Task GeneratesTheDeleteMethodCall() [Fact] public async Task WritesTheRequestPayload() { - const string userJsonObject = "{\r\n \"accountEnabled\": true,\r\n " + - "\"displayName\": \"displayName-value\",\r\n " + - "\"mailNickname\": \"mailNickname-value\",\r\n " + - "\"userPrincipalName\": \"upn-value@tenant-value.onmicrosoft.com\",\r\n " + - " \"passwordProfile\" : {\r\n \"forceChangePasswordNextSignIn\": true,\r\n \"password\": \"password-value\"\r\n }\r\n}";//nested passwordProfile Object + var sampleJson = @" + { + ""accountEnabled"": true, + ""displayName"": ""displayName-value"", + ""mailNickname"": ""mailNickname-value"", + ""userPrincipalName"": ""upn-value@tenant-value.onmicrosoft.com"", + "" passwordProfile"": { + ""forceChangePasswordNextSignIn"": true, + ""password"": ""password-value"" + } + } + "; using var requestPayload = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/users") { - Content = new StringContent(userJsonObject, Encoding.UTF8, "application/json") + Content = new StringContent(sampleJson, Encoding.UTF8, "application/json") }; var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("new User", result); - Assert.Contains("requestBody.accountEnabled = true;", result); - Assert.Contains("requestBody.passwordProfile = new PasswordProfile", result); - Assert.Contains("requestBody.displayName = \"displayName-value\"", result); + + Assert.Contains("const requestBody : User = {", result); + Assert.Contains("accountEnabled : true", result); + Assert.Contains("passwordProfile : {", result); } [Fact] public async Task WritesALongAndFindsAnAction() diff --git a/CodeSnippetsReflection.OpenAPI/ModelGraph/CodeProperty.cs b/CodeSnippetsReflection.OpenAPI/ModelGraph/CodeProperty.cs index 8d9378049..12bf9dd3f 100644 --- a/CodeSnippetsReflection.OpenAPI/ModelGraph/CodeProperty.cs +++ b/CodeSnippetsReflection.OpenAPI/ModelGraph/CodeProperty.cs @@ -7,4 +7,5 @@ namespace CodeSnippetsReflection.OpenAPI.ModelGraph { public record struct CodeProperty(string Name, string Value, List Children, PropertyType PropertyType = PropertyType.String); + } diff --git a/CodeSnippetsReflection.OpenAPI/ModelGraph/ModelGraphBuilder.cs b/CodeSnippetsReflection.OpenAPI/ModelGraph/ModelGraphBuilder.cs index cc2402286..66b179f25 100644 --- a/CodeSnippetsReflection.OpenAPI/ModelGraph/ModelGraphBuilder.cs +++ b/CodeSnippetsReflection.OpenAPI/ModelGraph/ModelGraphBuilder.cs @@ -19,8 +19,6 @@ public static class ModelGraphBuilder private static readonly Regex nestedStatementRegex = new(@"(\w+)(\([^)]+\))", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly CodeProperty EMPTY_PROPERTY = new() { Name = null , Value = null , Children = null, PropertyType = PropertyType.Default }; - - private static readonly CodeProperty BINARY_PROPERTY = new() { Name = null, Value = null, Children = null, PropertyType = PropertyType.Binary }; public static SnippetCodeGraph BuildCodeGraph(SnippetModel snippetModel) { @@ -30,14 +28,14 @@ public static SnippetCodeGraph BuildCodeGraph(SnippetModel snippetModel) Nodes = snippetModel.PathNodes, Headers = parseHeaders(snippetModel), Options = parseOptions(snippetModel), - Parameters = buildParameters(snippetModel), - Body = buildBody(snippetModel) + Parameters = parseParameters(snippetModel), + Body = parseBody(snippetModel) }; } - /*** - * Returns headers filtering `Host` out - */ + /// + /// Parses Headers Filtering Out 'Host' + /// private static IEnumerable parseHeaders(SnippetModel snippetModel) { return snippetModel.RequestHeaders.Where(h => !h.Key.Equals("Host", StringComparison.OrdinalIgnoreCase)) @@ -52,7 +50,7 @@ private static List parseOptions(SnippetModel snippetModel) } - private static List buildParameters(SnippetModel snippetModel) + private static List parseParameters(SnippetModel snippetModel) { var parameters = new List(); if (!string.IsNullOrEmpty(snippetModel.QueryString)) @@ -63,10 +61,10 @@ private static List buildParameters(SnippetModel snippetModel) if (queryParam.Contains("=")) { var kvPair = queryParam.Split('=', StringSplitOptions.RemoveEmptyEntries); - parameters.Add(new() { Name = NormalizeQueryParameterName(kvPair[0]), Value = GetQueryParameterValue(kvPair[1], replacements) }); + parameters.Add(new() { Name = NormalizeQueryParameterName(kvPair[0]), Value = GetQueryParameterValue(kvPair[1], replacements), PropertyType = PropertyType.String }); } else - parameters.Add(new() { Name = NormalizeQueryParameterName(queryParam), Value = GetQueryParameterValue("undefined", replacements) }); + parameters.Add(new() { Name = NormalizeQueryParameterName(queryParam), Value = GetQueryParameterValue("undefined", replacements), PropertyType = PropertyType.String }); } } @@ -98,14 +96,13 @@ private static string GetQueryParameterValue(string originalValue, Dictionary replacements.ContainsKey(v) ? v + replacements[v] : v) - .Aggregate((a, b) => $"{a},{b}"); - return $"\"{valueWithNested}\""; + return originalValue.Split(',') + .Select(v => replacements.ContainsKey(v) ? v + replacements[v] : v) + .Aggregate((a, b) => $"{a},{b}"); } } - private static CodeProperty buildBody(SnippetModel snippetModel) + private static CodeProperty parseBody(SnippetModel snippetModel) { if (string.IsNullOrWhiteSpace(snippetModel?.RequestBody)) return EMPTY_PROPERTY; @@ -115,7 +112,7 @@ private static CodeProperty buildBody(SnippetModel snippetModel) case "application/json": return TryParseBody(snippetModel); case "application/octet-stream": - return BINARY_PROPERTY; + return new() { Name = null, Value = null, Children = null, PropertyType = PropertyType.Binary }; default: return TryParseBody(snippetModel);//in case the content type header is missing but we still have a json payload } From f7996132b51f95384ab4cfc616532f951128ae74 Mon Sep 17 00:00:00 2001 From: rkodev <43806892+rkodev@users.noreply.github.com> Date: Thu, 26 May 2022 11:58:17 +0300 Subject: [PATCH 03/14] Move test --- .../{ModelGraph => }/ModelGraphBuilderTests.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename CodeSnippetsReflection.OpenAPI.Test/{ModelGraph => }/ModelGraphBuilderTests.cs (100%) diff --git a/CodeSnippetsReflection.OpenAPI.Test/ModelGraph/ModelGraphBuilderTests.cs b/CodeSnippetsReflection.OpenAPI.Test/ModelGraphBuilderTests.cs similarity index 100% rename from CodeSnippetsReflection.OpenAPI.Test/ModelGraph/ModelGraphBuilderTests.cs rename to CodeSnippetsReflection.OpenAPI.Test/ModelGraphBuilderTests.cs From 024472e918b0a43216cb8fc6d87e4e6c033bebd6 Mon Sep 17 00:00:00 2001 From: rkodev <43806892+rkodev@users.noreply.github.com> Date: Thu, 26 May 2022 14:33:44 +0300 Subject: [PATCH 04/14] Fix array serialization --- .../LanguageGenerators/TypeScriptGenerator.cs | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/CodeSnippetsReflection.OpenAPI/LanguageGenerators/TypeScriptGenerator.cs b/CodeSnippetsReflection.OpenAPI/LanguageGenerators/TypeScriptGenerator.cs index e1e89a35b..750a76ef7 100644 --- a/CodeSnippetsReflection.OpenAPI/LanguageGenerators/TypeScriptGenerator.cs +++ b/CodeSnippetsReflection.OpenAPI/LanguageGenerators/TypeScriptGenerator.cs @@ -147,6 +147,13 @@ private static void WriteBody(SnippetCodeGraph codeGraph, StringBuilder builder) builder.AppendLine($"}};"); } } + + private static string NormalizeJsonName(string Name) + { + if (Name.Contains(".")) return $"\"{Name}\""; + + return Name; + } private static void WriteCodePropertyObject(StringBuilder builder, CodeProperty codeProperty, IndentManager indentManager) { @@ -162,7 +169,7 @@ private static void WriteCodePropertyObject(StringBuilder builder, CodeProperty } else { - builder.AppendLine($"{indentManager.GetIndent()}{child.Name.ToFirstCharacterLowerCase()} : {{"); + builder.AppendLine($"{indentManager.GetIndent()}{NormalizeJsonName(child.Name.ToFirstCharacterLowerCase())} : {{"); } indentManager.Indent(); @@ -173,7 +180,7 @@ private static void WriteCodePropertyObject(StringBuilder builder, CodeProperty break; case PropertyType.Array: - builder.AppendLine($"{indentManager.GetIndent()}{child.Name} : ["); + builder.AppendLine($"{indentManager.GetIndent()}{NormalizeJsonName(child.Name)} : ["); indentManager.Indent(); WriteCodePropertyObject(builder, child, indentManager); indentManager.Unindent(); @@ -181,8 +188,15 @@ private static void WriteCodePropertyObject(StringBuilder builder, CodeProperty break; case PropertyType.String: - var propName = codeProperty.PropertyType == PropertyType.Map ? $"\"{child.Name.ToFirstCharacterLowerCase()}\"" : child.Name.ToFirstCharacterLowerCase(); - builder.AppendLine($"{indentManager.GetIndent()}{propName} : \"{child.Value}\","); + var propName = codeProperty.PropertyType == PropertyType.Map ? $"\"{NormalizeJsonName(child.Name.ToFirstCharacterLowerCase())}\"" : child.Name.ToFirstCharacterLowerCase(); + if (String.IsNullOrWhiteSpace(propName)) + { + builder.AppendLine($"{indentManager.GetIndent()}\"{child.Value}\","); + } + else + { + builder.AppendLine($"{indentManager.GetIndent()}{propName} : \"{child.Value}\","); + } break; case PropertyType.Enum: builder.AppendLine($"{indentManager.GetIndent()}{child.Name.ToFirstCharacterLowerCase()} : {child.Value},"); From 9cc25550c69da6a6b399bee29702bfea813b9023 Mon Sep 17 00:00:00 2001 From: rkodev <43806892+rkodev@users.noreply.github.com> Date: Mon, 30 May 2022 17:38:03 +0300 Subject: [PATCH 05/14] Fix - Get schema inforation for propeties generated under additional data --- .../LanguageGenerators/TypeScriptGenerator.cs | 89 +++++++++---------- .../ModelGraph/ModelGraphBuilder.cs | 75 +++++++++------- .../OpenApiSchemaExtensions.cs | 3 +- 3 files changed, 87 insertions(+), 80 deletions(-) diff --git a/CodeSnippetsReflection.OpenAPI/LanguageGenerators/TypeScriptGenerator.cs b/CodeSnippetsReflection.OpenAPI/LanguageGenerators/TypeScriptGenerator.cs index 750a76ef7..8cc207f02 100644 --- a/CodeSnippetsReflection.OpenAPI/LanguageGenerators/TypeScriptGenerator.cs +++ b/CodeSnippetsReflection.OpenAPI/LanguageGenerators/TypeScriptGenerator.cs @@ -26,7 +26,7 @@ public class TypeScriptGenerator : ILanguageGenerator parseHeaders(SnippetModel snippetModel) { return snippetModel.RequestHeaders.Where(h => !h.Key.Equals("Host", StringComparison.OrdinalIgnoreCase)) - .Select(h => new CodeProperty { Name = h.Key , Value = h.Value.FirstOrDefault(), Children = null, PropertyType = PropertyType.String }) + .Select(h => new CodeProperty { Name = h.Key , Value = h.Value?.FirstOrDefault(), Children = null, PropertyType = PropertyType.String }) .ToList(); } @@ -71,7 +71,7 @@ private static List parseParameters(SnippetModel snippetModel) return parameters; } - private static string NormalizeQueryParameterName(string queryParam) => queryParam.TrimStart('$').ToFirstCharacterLowerCase(); + private static string NormalizeQueryParameterName(string queryParam) => System.Web.HttpUtility.UrlDecode(queryParam.TrimStart('$').ToFirstCharacterLowerCase()); private static (string, Dictionary) ReplaceNestedOdataQueryParameters(string queryParams) { @@ -90,13 +90,14 @@ private static (string, Dictionary) ReplaceNestedOdataQueryParam private static string GetQueryParameterValue(string originalValue, Dictionary replacements) { - if (originalValue.Equals("true", StringComparison.OrdinalIgnoreCase) || originalValue.Equals("false", StringComparison.OrdinalIgnoreCase)) - return originalValue.ToLowerInvariant(); - else if (int.TryParse(originalValue, out var intValue)) + var escapedParam = System.Web.HttpUtility.UrlDecode(originalValue); + if (escapedParam.Equals("true", StringComparison.OrdinalIgnoreCase) || escapedParam.Equals("false", StringComparison.OrdinalIgnoreCase)) + return escapedParam.ToLowerInvariant(); + else if (int.TryParse(escapedParam, out var intValue)) return intValue.ToString(); else { - return originalValue.Split(',') + return escapedParam.Split(',') .Select(v => replacements.ContainsKey(v) ? v + replacements[v] : v) .Aggregate((a, b) => $"{a},{b}"); } @@ -118,6 +119,31 @@ private static CodeProperty parseBody(SnippetModel snippetModel) } } + private static string ComputeRequestBody(SnippetModel snippetModel) + { + var nodes = snippetModel.PathNodes; + if (!(nodes?.Any() ?? false)) return string.Empty; + + var nodeName = nodes.Where(x => !x.Segment.IsCollectionIndex()) + .Select(x => + { + if (x.Segment.IsFunction()) + return x.Segment.Split('.').Last(); + else + return x.Segment; + }) + .Last() + .ToFirstCharacterUpperCase(); + + var singularNodeName = nodeName[nodeName.Length - 1] == 's' ? nodeName.Substring(0, nodeName.Length - 1) : nodeName; + + if (nodes.Last()?.Segment?.IsCollectionIndex() == true) + return singularNodeName; + else + return $"{nodeName}PostRequestBody"; + + } + private static CodeProperty TryParseBody(SnippetModel snippetModel) { if (!snippetModel.IsRequestBodyValid) @@ -125,7 +151,7 @@ private static CodeProperty TryParseBody(SnippetModel snippetModel) using var parsedBody = JsonDocument.Parse(snippetModel.RequestBody, new JsonDocumentOptions { AllowTrailingCommas = true }); var schema = snippetModel.RequestSchema; - var className = schema.GetSchemaTitle().ToFirstCharacterUpperCase() ?? $"{snippetModel.Path.Split("/").Last().ToFirstCharacterUpperCase()}RequestBody"; + var className = schema.GetSchemaTitle().ToFirstCharacterUpperCase() ?? ComputeRequestBody(snippetModel); return parseJsonObjectValue(className, parsedBody.RootElement, schema, snippetModel); } @@ -149,52 +175,37 @@ private static CodeProperty parseJsonObjectValue(String rootPropertyName, JsonEl var additionalChildren = new List(); foreach (var property in propertiesWithoutSchema) - { - var propName = property.Name; - if (string.IsNullOrEmpty(propName) || (property.Value.ValueKind != JsonValueKind.Object)) - { - additionalChildren.Add(parseProperty(propName, property.Value, null)); - } - else - { - children.Add(parseProperty(propName, property.Value, null)); - } - } + additionalChildren.Add(parseProperty(property.Name, property.Value, null)); - if (additionalChildren.Any() == true) - { - var propertyName = "additionalData"; - var codeProperty = new CodeProperty { Name = propertyName, PropertyType = PropertyType.Map, Children = additionalChildren }; - children.Add(codeProperty); - } + if (additionalChildren.Any()) + children.Add(new CodeProperty { Name = "additionalData", PropertyType = PropertyType.Map, Children = additionalChildren }); } return new CodeProperty { Name = rootPropertyName, PropertyType = PropertyType.Object, Children = children }; } + private static String escapeSpecialCharacters(string value) + { + return value?.Replace("\"", "\\\"")?.Replace("\n", "\\n")?.Replace("\r", "\\r"); + } + private static CodeProperty parseProperty(string propertyName, JsonElement value, OpenApiSchema propSchema) { switch (value.ValueKind) { case JsonValueKind.String: if (propSchema?.Format?.Equals("base64url", StringComparison.OrdinalIgnoreCase) ?? false) - { return new CodeProperty { Name = propertyName, Value = value.GetString(), PropertyType = PropertyType.Base64Url, Children = new List() }; - } else if (propSchema?.Format?.Equals("date-time", StringComparison.OrdinalIgnoreCase) ?? false) - { return new CodeProperty { Name = propertyName, Value = value.GetString(), PropertyType = PropertyType.Date, Children = new List() }; - } else { var enumSchema = propSchema?.AnyOf.FirstOrDefault(x => x.Enum.Count > 0); if (enumSchema == null) - { - return new CodeProperty { Name = propertyName, Value = value.GetString(), PropertyType = PropertyType.String, Children = new List() }; - } + return new CodeProperty { Name = propertyName, Value = escapeSpecialCharacters(value.GetString()), PropertyType = PropertyType.String, Children = new List() }; else { - var propValue = $"{enumSchema.Title.ToFirstCharacterUpperCase()}.{value.GetString().ToFirstCharacterUpperCase()}"; + var propValue = String.IsNullOrWhiteSpace(value.GetString()) ? null : $"{enumSchema.Title.ToFirstCharacterUpperCase()}.{value.GetString().ToFirstCharacterUpperCase()}"; return new CodeProperty { Name = propertyName, Value = propValue, PropertyType = PropertyType.Enum, Children = new List() }; } diff --git a/CodeSnippetsReflection.OpenAPI/OpenApiSchemaExtensions.cs b/CodeSnippetsReflection.OpenAPI/OpenApiSchemaExtensions.cs index 8f6805304..21f248a84 100644 --- a/CodeSnippetsReflection.OpenAPI/OpenApiSchemaExtensions.cs +++ b/CodeSnippetsReflection.OpenAPI/OpenApiSchemaExtensions.cs @@ -14,7 +14,8 @@ public static IEnumerable> GetAllProperties( .Union(schema.AllOf.FlattenEmptyEntries(x => x.AllOf, 2).SelectMany(x => x.Properties)) .Union(schema.AnyOf.SelectMany(x => x.Properties)) .Union(schema.OneOf.SelectMany(x => x.Properties)) - .Union(schema.Items != null ? schema.Items.AllOf.SelectMany(x => x.Properties) : Enumerable.Empty>()); + .Union(schema.Items != null ? schema.Items.AllOf.SelectMany(x => x.Properties) : Enumerable.Empty>()) + .Union(schema.Items != null ? schema.Items.AnyOf.SelectMany(x => x.Properties) : Enumerable.Empty>()); } return schema.AllOf.Union(schema.AnyOf).Union(schema.OneOf).SelectMany(x => x.GetAllProperties()); } From dd64e9f05b379889f4a56d687ade864d6e3dfe72 Mon Sep 17 00:00:00 2001 From: rkodev <43806892+rkodev@users.noreply.github.com> Date: Mon, 30 May 2022 19:30:06 +0300 Subject: [PATCH 06/14] Adds TS test cases --- .../ModelGraphBuilderTests.cs | 4 +- .../TypeScriptGeneratorTest.cs | 514 +++++++++++++++--- .../LanguageGenerators/TypeScriptGenerator.cs | 2 +- .../ModelGraph/ModelGraphBuilder.cs | 61 +-- 4 files changed, 481 insertions(+), 100 deletions(-) diff --git a/CodeSnippetsReflection.OpenAPI.Test/ModelGraphBuilderTests.cs b/CodeSnippetsReflection.OpenAPI.Test/ModelGraphBuilderTests.cs index 75c873c34..60c4cdd64 100644 --- a/CodeSnippetsReflection.OpenAPI.Test/ModelGraphBuilderTests.cs +++ b/CodeSnippetsReflection.OpenAPI.Test/ModelGraphBuilderTests.cs @@ -8,7 +8,7 @@ using Microsoft.OpenApi.Services; using Xunit; -namespace CodeSnippetsReflection.OpenAPI.Test.ModelGraph +namespace CodeSnippetsReflection.OpenAPI.Test { public class ModelGraphBuilderTests { @@ -168,7 +168,7 @@ public async Task ParsesBodyWithoutProperContentType() var result = ModelGraphBuilder.BuildCodeGraph(snippetModel); ; - var expectedObject = new CodeProperty { Name = "MessagesRequestBody", Value = null, PropertyType = PropertyType.Object, Children = new List() }; + var expectedObject = new CodeProperty { Name = "MessagesPostRequestBody", Value = null, PropertyType = PropertyType.Object, Children = new List() }; Assert.Equal(expectedObject.Name, result.Body.Name); Assert.Equal(expectedObject.Value, result.Body.Value); diff --git a/CodeSnippetsReflection.OpenAPI.Test/TypeScriptGeneratorTest.cs b/CodeSnippetsReflection.OpenAPI.Test/TypeScriptGeneratorTest.cs index f58f4d1a1..9c30dba8a 100644 --- a/CodeSnippetsReflection.OpenAPI.Test/TypeScriptGeneratorTest.cs +++ b/CodeSnippetsReflection.OpenAPI.Test/TypeScriptGeneratorTest.cs @@ -34,7 +34,20 @@ public async Task GeneratesTheCorrectFluentAPIPath() using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/me/messages"); var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains(".me.messages", result); + + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + + const result = async () => { + await graphServiceClient.me.messages.get(); + } + "; + + Assert.Contains( + expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), + result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) + ); } [Fact] @@ -47,16 +60,51 @@ public async Task GeneratesClassWithDefaultBodyWhenSchemaNotPresent() }; var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("const requestBody = new BatchRecordDecisionsRequestBody()", result); + + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + const requestBody : BatchRecordDecisionsPostRequestBody = { + decision : ""Approve"", + justification : ""All principals with access need continued access to the resource (Marketing Group) as all the principals are on the marketing team"", + resourceId : ""a5c51e59-3fcd-4a37-87a1-835c0c21488a"", + }; + + async () => { + await graphServiceClient.identityGovernance.accessReviews.definitionsById(""accessReviewScheduleDefinition-id"").instancesById(""accessReviewInstance-id"").batchRecordDecisions.post(requestBody); + } + "; + + + Assert.Contains( + expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), + result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) + ); } + [Fact] public async Task GeneratesTheCorrectFluentAPIPathForIndexedCollections() { using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/me/messages/{{message-id}}"); var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains(".me.messagesById(\"message-id\")", result); + + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + + const result = async () => { + await graphServiceClient.me.messagesById(""message-id"").get(); + } + "; + + + Assert.Contains( + expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), + result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) + ); } + [Fact] public async Task GeneratesTheSnippetInitializationStatement() { @@ -65,31 +113,67 @@ public async Task GeneratesTheSnippetInitializationStatement() var result = _generator.GenerateCodeSnippet(snippetModel); Assert.Contains("const graphServiceClient = GraphServiceClient.init({authProvider});", result); } + [Fact] public async Task GeneratesTheGetMethodCall() { using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/me/messages"); var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("get", result); - Assert.Contains("await", result); + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + + const result = async () => { + await graphServiceClient.me.messages.get(); + } + "; + + + Assert.Contains( + expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), + result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) + ); } + [Fact] public async Task GeneratesThePostMethodCall() { using var requestPayload = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/me/messages"); var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("post", result); + var expected = @" + const result = async () => { + await graphServiceClient.me.messages.post(); + } + "; + + + Assert.Contains( + expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), + result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) + ); } + [Fact] public async Task GeneratesThePatchMethodCall() { using var requestPayload = new HttpRequestMessage(HttpMethod.Patch, $"{ServiceRootUrl}/me/messages/{{message-id}}"); var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("patch", result); + var expected = @" + const result = async () => { + await graphServiceClient.me.messagesById(""message-id"").patch(); + } + "; + + + Assert.Contains( + expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), + result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) + ); } + [Fact] public async Task GeneratesThePutMethodCall() { @@ -98,16 +182,29 @@ public async Task GeneratesThePutMethodCall() var result = _generator.GenerateCodeSnippet(snippetModel); Assert.Contains("put", result); } + [Fact] public async Task GeneratesTheDeleteMethodCall() { using var requestPayload = new HttpRequestMessage(HttpMethod.Delete, $"{ServiceRootUrl}/me/messages/{{message-id}}"); var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("delete", result); - Assert.DoesNotContain("let result =", result); + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + + const result = async () => { + await graphServiceClient.me.messagesById(""message-id"").delete(); + } + "; + + + Assert.Contains( + expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), + result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) + ); } - // code + [Fact] public async Task WritesTheRequestPayload() { @@ -131,10 +228,34 @@ public async Task WritesTheRequestPayload() var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("const requestBody : User = {", result); - Assert.Contains("accountEnabled : true", result); - Assert.Contains("passwordProfile : {", result); + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + const requestBody : User = { + accountEnabled : true, + displayName : ""displayName-value"", + mailNickname : ""mailNickname-value"", + userPrincipalName : ""upn-value@tenant-value.onmicrosoft.com"", + additionalData : { + passwordProfile : { + forceChangePasswordNextSignIn : true, + password : ""password-value"", + }, + }, + }; + + const result = async () => { + await graphServiceClient.users.post(requestBody); + } + "; + + + Assert.Contains( + expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), + result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) + ); } + [Fact] public async Task WritesALongAndFindsAnAction() { @@ -147,21 +268,44 @@ public async Task WritesALongAndFindsAnAction() var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); Assert.Contains("10", result); - Assert.DoesNotContain("microsoft.graph", result); + Assert.DoesNotContain("microsoft.graph1", result); } + [Fact] public async Task WritesADouble() { - const string userJsonObject = "{\r\n \"minimumAttendeePercentage\": 10\r\n\r\n}"; + var sampleJson = @" + { + ""minimumAttendeePercentage"": 10 + } + "; using var requestPayload = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/me/findMeetingTimes") { - Content = new StringContent(userJsonObject, Encoding.UTF8, "application/json") + Content = new StringContent(sampleJson, Encoding.UTF8, "application/json") }; var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("10", result); + + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + const requestBody : FindMeetingTimesPostRequestBody = { + minimumAttendeePercentage : 10, + }; + + const result = async () => { + await graphServiceClient.me.findMeetingTimes.post(requestBody); + } + "; + + + Assert.Contains( + expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), + result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) + ); } + [Fact] public async Task GeneratesABinaryPayload() { @@ -172,8 +316,23 @@ public async Task GeneratesABinaryPayload() requestPayload.Content.Headers.ContentType = new("application/octet-stream"); var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("new WebStream", result); + + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + const requestBody = new ArrayBuffer(16); + + async () => { + await graphServiceClient.applicationsById(""application-id"").logo.put(requestBody); + } + "; + + Assert.Contains( + expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), + result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) + ); } + [Fact] public async Task GeneratesABase64UrlPayload() { @@ -184,8 +343,26 @@ public async Task GeneratesABase64UrlPayload() }; var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("btoa", result); + + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + const requestBody : ChatMessageHostedContent = { + contentBytes : ""wiubviuwbegviwubiu"", + }; + + const result = async () => { + await graphServiceClient.chatsById(""chat-id"").messagesById(""chatMessage-id"").hostedContents.post(requestBody); + } + "; + + + Assert.Contains( + expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), + result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) + ); } + [Fact] public async Task GeneratesADatePayload() { @@ -196,22 +373,67 @@ public async Task GeneratesADatePayload() }; var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("new Date(", result); + + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + const requestBody : Message = { + receivedDateTime : new Date(""2021-08-30T20:00:00:00Z""), + }; + + const result = async () => { + await graphServiceClient.me.messages.post(requestBody); + } + "; + + + Assert.Contains( + expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), + result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) + ); } + [Fact] public async Task GeneratesAnArrayPayloadInAdditionalData() { - const string userJsonObject = "{\r\n \"members@odata.bind\": [\r\n \"https://graph.microsoft.com/v1.0/directoryObjects/{id}\",\r\n \"https://graph.microsoft.com/v1.0/directoryObjects/{id}\",\r\n \"https://graph.microsoft.com/v1.0/directoryObjects/{id}\"\r\n ]\r\n}"; + var samplePayload = @" + { + ""members@odata.bind"": [ + ""https://graph.microsoft.com/v1.0/directoryObjects/{id}"", + ""https://graph.microsoft.com/v1.0/directoryObjects/{id}"", + ""https://graph.microsoft.com/v1.0/directoryObjects/{id}"" + ] + } + "; + using var requestPayload = new HttpRequestMessage(HttpMethod.Patch, $"{ServiceRootUrl}/groups/{{group-id}}") { - Content = new StringContent(userJsonObject, Encoding.UTF8, "application/json") + Content = new StringContent(samplePayload, Encoding.UTF8, "application/json") }; var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("\"members@odata.bind\" : [", result); - Assert.Contains("requestBody.additionalData", result); - Assert.Contains("members", result); // property name hasn't been changed + + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + const requestBody : Group = { + additionalData : { + ""members@odata.bind"" : [ + ""https://graph.microsoft.com/v1.0/directoryObjects/{id}"", + ""https://graph.microsoft.com/v1.0/directoryObjects/{id}"", + ""https://graph.microsoft.com/v1.0/directoryObjects/{id}"", + ], + }, + }; + "; + + + Assert.Contains( + expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), + result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) + ); } + [Fact] public async Task GeneratesAnArrayOfObjectsPayloadData() { @@ -222,38 +444,121 @@ public async Task GeneratesAnArrayOfObjectsPayloadData() }; var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("const extension = new Extension();", result); // property is initialized on its own - Assert.Contains("extension.additionalData = {", result); // additional data is initialized as a Record not mpa - Assert.Contains("requestBody.extensions = [", result); // property is added to list + + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + const requestBody : Group = { + extensions : [ + { + additionalData : { + dealValue : 10000, + }, + }, + ], + additionalData : { + body : { + contentType : ""HTML"", + }, + }, + }; + + const result = async () => { + await graphServiceClient.groupsById(""group-id"").patch(requestBody); + } + "; + + Assert.Contains( + expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), + result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) + ); } + [Fact] public async Task GeneratesSelectQueryParameters() { using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/me?$select=displayName,id"); var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("displayName", result); - Assert.Contains("let requestParameters = {", result); + + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + const configuration = { + queryParameters : { + select: ""displayName,id"", + } + }; + + const result = async () => { + await graphServiceClient.me.get(configuration); + } + "; + + + Assert.Contains( + expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), + result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) + ); } + [Fact] public async Task GeneratesCountBooleanQueryParameters() { using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/users?$count=true&$select=displayName,id"); var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("displayName", result); - Assert.DoesNotContain("\"true\"", result); - Assert.Contains("true", result); + + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + const configuration = { + queryParameters : { + count: ""true"", + select: ""displayName,id"", + } + }; + + const result = async () => { + await graphServiceClient.users.get(configuration); + } + "; + + + Assert.Contains( + expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), + result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) + ); } + [Fact] public async Task GeneratesSkipQueryParameters() { using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/users?$skip=10"); var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.DoesNotContain("\"10\"", result); - Assert.Contains("10", result); + + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + const configuration = { + queryParameters : { + skip: ""10"", + } + }; + + const result = async () => { + await graphServiceClient.users.get(configuration); + } + "; + + + Assert.Contains( + expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), + result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) + ); } + [Fact] public async Task GeneratesSelectExpandQueryParameters() { @@ -264,6 +569,7 @@ public async Task GeneratesSelectExpandQueryParameters() Assert.Contains("members($select=id,displayName)", result); Assert.DoesNotContain("select :", result); } + [Fact] public async Task GeneratesRequestHeaders() { @@ -271,52 +577,138 @@ public async Task GeneratesRequestHeaders() requestPayload.Headers.Add("ConsistencyLevel", "eventual"); var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("\"ConsistencyLevel\": \"eventual\",", result); - Assert.Contains("const headers = {", result); + + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + const configuration = { + headers : { + ""ConsistencyLevel"": ""eventual"", + } + }; + + const result = async () => { + await graphServiceClient.groups.get(configuration); + } + "; + + Assert.Contains( + expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), + result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) + ); } + [Fact] public async Task GenerateAdditionalData() { - const string chatObject = "{ \"createdDateTime\":\"2019-02-04T19:58:15.511Z\", " + - "\"from\":{ \"user\":{ \"id\":\"id-value\", \"displayName\":\"Joh Doe\", " + - " \"userIdentityType\":\"aadUser\" } }, \"body\":{ \"contentType\":\"html\", " + - " \"content\":\"Hello World\" }}"; + var samplePayload = @" + { + ""createdDateTime"": ""2019-02-04T19:58:15.511Z"", + ""from"": { + ""user"": { + ""id"": ""id-value"", + ""displayName"": ""Joh Doe"", + ""userIdentityType"": ""aadUser"" + } + }, + ""body"": { + ""contentType"": ""html"", + ""content"": ""Hello World"" + } + } + "; using var requestPayload = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/teams/team-id/channels/19:4b6bed8d24574f6a9e436813cb2617d8@thread.tacv2/messages") { - Content = new StringContent(chatObject, Encoding.UTF8, "application/json") + Content = new StringContent(samplePayload, Encoding.UTF8, "application/json") }; var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("new ChatMessage", result); - Assert.Contains("requestBody.from.user.additionalData", result); + + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + const requestBody : ChatMessage = { + createdDateTime : new Date(""2019-02-04T19:58:15.511Z""), + from : { + user : { + id : ""id-value"", + displayName : ""Joh Doe"", + additionalData : { + ""userIdentityType"" : ""aadUser"", + }, + }, + }, + body : { + contentType : BodyType.Html, + content : ""Hello World"", + }, + }; + + const result = async () => { + await graphServiceClient.teamsById(""team-id"").channelsById(""channel-id"").messages.post(requestBody); + } + "; + + Assert.Contains( + expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), + result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) + ); } [Fact] public async Task GeneratesEnumsWhenVariableIsEnum() { - const string userJsonObject = @"{ - ""displayName"": ""Test create"", - ""settings"": { - ""recurrence"": { - ""pattern"": { - ""type"": ""weekly"", - ""interval"": 1 - }, - ""range"": { - ""type"": ""noEnd"", - ""startDate"": ""2020-09-08T12:02:30.667Z"" - } - } - } -}"; + const string payloadJson = @"{ + ""displayName"": ""Test create"", + ""settings"": { + ""recurrence"": { + ""pattern"": { + ""type"": ""weekly"", + ""interval"": 1 + }, + ""range"": { + ""type"": ""noEnd"", + ""startDate"": ""2020-09-08T12:02:30.667Z"" + } + } + } + }"; using var requestPayload = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/identityGovernance/accessReviews/definitions") { - Content = new StringContent(userJsonObject, Encoding.UTF8, "application/json") + Content = new StringContent(payloadJson, Encoding.UTF8, "application/json") }; var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("requestBody.settings.recurrence.pattern.type = RecurrencePatternType.Weekly", result); + + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + const requestBody : AccessReviewScheduleDefinition = { + displayName : ""Test create"", + settings : { + recurrence : { + pattern : { + type : RecurrencePatternType.Weekly, + interval : 1, + }, + range : { + type : RecurrenceRangeType.NoEnd, + startDate : ""2020-09-08T12:02:30.667Z"", + }, + }, + }, + }; + + const result = async () => { + await graphServiceClient.identityGovernance.accessReviews.definitions.post(requestBody); + } + "; + + Assert.Contains( + expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), + result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) + ); } } } diff --git a/CodeSnippetsReflection.OpenAPI/LanguageGenerators/TypeScriptGenerator.cs b/CodeSnippetsReflection.OpenAPI/LanguageGenerators/TypeScriptGenerator.cs index 8cc207f02..241a9b13b 100644 --- a/CodeSnippetsReflection.OpenAPI/LanguageGenerators/TypeScriptGenerator.cs +++ b/CodeSnippetsReflection.OpenAPI/LanguageGenerators/TypeScriptGenerator.cs @@ -135,7 +135,7 @@ private static void WriteBody(SnippetCodeGraph codeGraph, StringBuilder builder) if (codeGraph.Body.PropertyType == PropertyType.Binary) { - builder.AppendLine($"{indentManager.GetIndent()}const {RequestBodyVarName} = new WebStream();"); + builder.AppendLine($"{indentManager.GetIndent()}const {RequestBodyVarName} = new ArrayBuffer({codeGraph.Body.Value.Length});"); } else { diff --git a/CodeSnippetsReflection.OpenAPI/ModelGraph/ModelGraphBuilder.cs b/CodeSnippetsReflection.OpenAPI/ModelGraph/ModelGraphBuilder.cs index 12346ff33..8757e93bf 100644 --- a/CodeSnippetsReflection.OpenAPI/ModelGraph/ModelGraphBuilder.cs +++ b/CodeSnippetsReflection.OpenAPI/ModelGraph/ModelGraphBuilder.cs @@ -1,13 +1,9 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; -using System.Text; using System.Text.Json; using System.Text.RegularExpressions; -using System.Threading.Tasks; using CodeSnippetsReflection.StringExtensions; -using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; namespace CodeSnippetsReflection.OpenAPI.ModelGraph @@ -27,7 +23,7 @@ public static SnippetCodeGraph BuildCodeGraph(SnippetModel snippetModel) HttpMethod = snippetModel.Method, Nodes = snippetModel.PathNodes, Headers = parseHeaders(snippetModel), - Options = parseOptions(snippetModel), + Options = Enumerable.Empty(), Parameters = parseParameters(snippetModel), Body = parseBody(snippetModel) }; @@ -43,12 +39,6 @@ private static IEnumerable parseHeaders(SnippetModel snippetModel) .ToList(); } - /// TODO Add support for options - private static List parseOptions(SnippetModel snippetModel) - { - return new List(); - } - private static List parseParameters(SnippetModel snippetModel) { @@ -58,7 +48,7 @@ private static List parseParameters(SnippetModel snippetModel) var (queryString, replacements) = ReplaceNestedOdataQueryParameters(snippetModel.QueryString); foreach (var queryParam in queryString.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries)) { - if (queryParam.Contains("=")) + if (queryParam.Contains('=')) { var kvPair = queryParam.Split('=', StringSplitOptions.RemoveEmptyEntries); parameters.Add(new() { Name = NormalizeQueryParameterName(kvPair[0]), Value = GetQueryParameterValue(kvPair[1], replacements), PropertyType = PropertyType.String }); @@ -113,7 +103,7 @@ private static CodeProperty parseBody(SnippetModel snippetModel) case "application/json": return TryParseBody(snippetModel); case "application/octet-stream": - return new() { Name = null, Value = null, Children = null, PropertyType = PropertyType.Binary }; + return new() { Name = null, Value = snippetModel.RequestBody?.ToString(), Children = null, PropertyType = PropertyType.Binary }; default: return TryParseBody(snippetModel);//in case the content type header is missing but we still have a json payload } @@ -152,10 +142,10 @@ private static CodeProperty TryParseBody(SnippetModel snippetModel) using var parsedBody = JsonDocument.Parse(snippetModel.RequestBody, new JsonDocumentOptions { AllowTrailingCommas = true }); var schema = snippetModel.RequestSchema; var className = schema.GetSchemaTitle().ToFirstCharacterUpperCase() ?? ComputeRequestBody(snippetModel); - return parseJsonObjectValue(className, parsedBody.RootElement, schema, snippetModel); + return parseJsonObjectValue(className, parsedBody.RootElement, schema); } - private static CodeProperty parseJsonObjectValue(String rootPropertyName, JsonElement value, OpenApiSchema schema, SnippetModel snippetModel = null) + private static CodeProperty parseJsonObjectValue(String rootPropertyName, JsonElement value, OpenApiSchema schema) { var children = new List(); @@ -189,27 +179,30 @@ private static String escapeSpecialCharacters(string value) return value?.Replace("\"", "\\\"")?.Replace("\n", "\\n")?.Replace("\r", "\\r"); } + private static CodeProperty evaluateStringProperty(string propertyName, JsonElement value, OpenApiSchema propSchema) + { + if (propSchema?.Format?.Equals("base64url", StringComparison.OrdinalIgnoreCase) ?? false) + return new CodeProperty { Name = propertyName, Value = value.GetString(), PropertyType = PropertyType.Base64Url, Children = new List() }; + + if (propSchema?.Format?.Equals("date-time", StringComparison.OrdinalIgnoreCase) ?? false) + return new CodeProperty { Name = propertyName, Value = value.GetString(), PropertyType = PropertyType.Date, Children = new List() }; + + + var enumSchema = propSchema?.AnyOf.FirstOrDefault(x => x.Enum.Count > 0); + if (enumSchema == null) + return new CodeProperty { Name = propertyName, Value = escapeSpecialCharacters(value.GetString()), PropertyType = PropertyType.String, Children = new List() }; + + + var propValue = String.IsNullOrWhiteSpace(value.GetString()) ? null : $"{enumSchema.Title.ToFirstCharacterUpperCase()}.{value.GetString().ToFirstCharacterUpperCase()}"; + return new CodeProperty { Name = propertyName, Value = propValue, PropertyType = PropertyType.Enum, Children = new List() }; + } + private static CodeProperty parseProperty(string propertyName, JsonElement value, OpenApiSchema propSchema) { switch (value.ValueKind) { case JsonValueKind.String: - if (propSchema?.Format?.Equals("base64url", StringComparison.OrdinalIgnoreCase) ?? false) - return new CodeProperty { Name = propertyName, Value = value.GetString(), PropertyType = PropertyType.Base64Url, Children = new List() }; - else if (propSchema?.Format?.Equals("date-time", StringComparison.OrdinalIgnoreCase) ?? false) - return new CodeProperty { Name = propertyName, Value = value.GetString(), PropertyType = PropertyType.Date, Children = new List() }; - else - { - var enumSchema = propSchema?.AnyOf.FirstOrDefault(x => x.Enum.Count > 0); - if (enumSchema == null) - return new CodeProperty { Name = propertyName, Value = escapeSpecialCharacters(value.GetString()), PropertyType = PropertyType.String, Children = new List() }; - else - { - var propValue = String.IsNullOrWhiteSpace(value.GetString()) ? null : $"{enumSchema.Title.ToFirstCharacterUpperCase()}.{value.GetString().ToFirstCharacterUpperCase()}"; - return new CodeProperty { Name = propertyName, Value = propValue, PropertyType = PropertyType.Enum, Children = new List() }; - } - - } + return evaluateStringProperty(propertyName, value, propSchema); case JsonValueKind.Number: return new CodeProperty { Name = propertyName, Value = $"{value}", PropertyType = PropertyType.Number, Children = new List() }; case JsonValueKind.False: @@ -219,13 +212,9 @@ private static CodeProperty parseProperty(string propertyName, JsonElement value return new CodeProperty { Name = propertyName, Value = "null", PropertyType = PropertyType.Null, Children = new List() }; case JsonValueKind.Object: if (propSchema != null) - { - return parseJsonObjectValue(propertyName, value, propSchema) ; - } + return parseJsonObjectValue(propertyName, value, propSchema); else - { return parseAnonymousObjectValues(propertyName, value, propSchema); - } case JsonValueKind.Array: return parseJsonArrayValue(propertyName, value, propSchema); default: From 9fe6a1f7ee4d6c60b1b454c40e6182bd3c29d618 Mon Sep 17 00:00:00 2001 From: rkodev <43806892+rkodev@users.noreply.github.com> Date: Mon, 30 May 2022 19:46:20 +0300 Subject: [PATCH 07/14] Fix code smells --- .../ModelGraphBuilderTests.cs | 3 --- .../LanguageGenerators/TypeScriptGenerator.cs | 6 +++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/CodeSnippetsReflection.OpenAPI.Test/ModelGraphBuilderTests.cs b/CodeSnippetsReflection.OpenAPI.Test/ModelGraphBuilderTests.cs index 60c4cdd64..f84d69661 100644 --- a/CodeSnippetsReflection.OpenAPI.Test/ModelGraphBuilderTests.cs +++ b/CodeSnippetsReflection.OpenAPI.Test/ModelGraphBuilderTests.cs @@ -144,10 +144,7 @@ public async Task ParsesBodyTypeBinary() var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); var result = ModelGraphBuilder.BuildCodeGraph(snippetModel); - - Assert.NotNull(result.Body); Assert.Equal(PropertyType.Binary, result.Body.PropertyType); - } [Fact] diff --git a/CodeSnippetsReflection.OpenAPI/LanguageGenerators/TypeScriptGenerator.cs b/CodeSnippetsReflection.OpenAPI/LanguageGenerators/TypeScriptGenerator.cs index 241a9b13b..a773c8fc4 100644 --- a/CodeSnippetsReflection.OpenAPI/LanguageGenerators/TypeScriptGenerator.cs +++ b/CodeSnippetsReflection.OpenAPI/LanguageGenerators/TypeScriptGenerator.cs @@ -87,7 +87,7 @@ private static void WriteOptions(SnippetCodeGraph codeGraph, StringBuilder build if (!codeGraph.HasOptions()) return; if (codeGraph.HasHeaders()) - builder.Append(","); + builder.Append(','); builder.AppendLine($"{indentManager.GetIndent()}{RequestOptionsVarName} : {{"); indentManager.Indent(); @@ -102,7 +102,7 @@ private static void WriteParameters(SnippetCodeGraph codeGraph, StringBuilder bu if (!codeGraph.HasParameters()) return; if (codeGraph.HasHeaders() || codeGraph.HasOptions()) - builder.Append(","); + builder.Append(','); builder.AppendLine($"{indentManager.GetIndent()}{RequestParametersVarName} : {{"); indentManager.Indent(); @@ -149,7 +149,7 @@ private static void WriteBody(SnippetCodeGraph codeGraph, StringBuilder builder) private static string NormalizeJsonName(string Name) { - return (!String.IsNullOrWhiteSpace(Name) && Name.Substring(1) != "\"") && (Name.Contains(".") || Name.Contains("-")) ? $"\"{Name}\"" : Name; + return (!String.IsNullOrWhiteSpace(Name) && Name.Substring(1) != "\"") && (Name.Contains('.') || Name.Contains('-')) ? $"\"{Name}\"" : Name; } private static void WriteCodePropertyObject(StringBuilder builder, CodeProperty codeProperty, IndentManager indentManager) From 2d7835fd6285f9ad8478ba98c1df2453dd3480a0 Mon Sep 17 00:00:00 2001 From: rkodev <43806892+rkodev@users.noreply.github.com> Date: Tue, 31 May 2022 11:39:46 +0300 Subject: [PATCH 08/14] Refactor SnippetCodeGraph to constructor initialization --- .../AssertUtil.cs | 20 ++ ...ilderTests.cs => SnippetCodeGraphTests.cs} | 47 +++- .../SnippetModelTests.cs | 2 +- .../TypeScriptGeneratorTest.cs | 106 ++------ .../CodeSnippetsReflection.OpenAPI.csproj | 1 - .../LanguageGenerators/TypeScriptGenerator.cs | 8 +- .../ModelGraph/ModelGraphBuilder.cs | 246 ------------------ .../ModelGraph/SnippetCodeGraph.cs | 240 ++++++++++++++++- .../StringExtensions/StringExtensions.cs | 5 + 9 files changed, 320 insertions(+), 355 deletions(-) create mode 100644 CodeSnippetsReflection.OpenAPI.Test/AssertUtil.cs rename CodeSnippetsReflection.OpenAPI.Test/{ModelGraphBuilderTests.cs => SnippetCodeGraphTests.cs} (84%) delete mode 100644 CodeSnippetsReflection.OpenAPI/ModelGraph/ModelGraphBuilder.cs diff --git a/CodeSnippetsReflection.OpenAPI.Test/AssertUtil.cs b/CodeSnippetsReflection.OpenAPI.Test/AssertUtil.cs new file mode 100644 index 000000000..d7cf7f26f --- /dev/null +++ b/CodeSnippetsReflection.OpenAPI.Test/AssertUtil.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace CodeSnippetsReflection.OpenAPI.Test +{ + public static class AssertUtil + { + public static void ContainsIgnoreWhiteSpace(string expectedSubstring, string actualString) + { + Xunit.Assert.Contains( + expectedSubstring.Replace(" ", string.Empty).Replace(Environment.NewLine, string.Empty), + actualString.Replace(" ", string.Empty).Replace(Environment.NewLine, string.Empty) + ); + } + } +} diff --git a/CodeSnippetsReflection.OpenAPI.Test/ModelGraphBuilderTests.cs b/CodeSnippetsReflection.OpenAPI.Test/SnippetCodeGraphTests.cs similarity index 84% rename from CodeSnippetsReflection.OpenAPI.Test/ModelGraphBuilderTests.cs rename to CodeSnippetsReflection.OpenAPI.Test/SnippetCodeGraphTests.cs index f84d69661..60566dbef 100644 --- a/CodeSnippetsReflection.OpenAPI.Test/ModelGraphBuilderTests.cs +++ b/CodeSnippetsReflection.OpenAPI.Test/SnippetCodeGraphTests.cs @@ -10,7 +10,7 @@ namespace CodeSnippetsReflection.OpenAPI.Test { - public class ModelGraphBuilderTests + public class SnippetCodeGraphTests { private const string ServiceRootUrl = "https://graph.microsoft.com/v1.0"; private static OpenApiUrlTreeNode _v1TreeNode; @@ -77,7 +77,7 @@ public async Task ParsesHeaders() request.Headers.Add("Prefer", "outlook.timezone=\"Pacific Standard Time\""); var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); - var result = ModelGraphBuilder.BuildCodeGraph(snippetModel); + var result = new SnippetCodeGraph(snippetModel); var header = result.Headers.First(); Assert.True(result.HasHeaders()); @@ -94,7 +94,7 @@ public async Task HasHeadeIsFalseWhenNoneIsInRequest() request.Headers.Add("Host", "graph.microsoft.com"); var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); - var result = ModelGraphBuilder.BuildCodeGraph(snippetModel); + var result = new SnippetCodeGraph(snippetModel); Assert.False(result.HasHeaders()); } @@ -105,7 +105,7 @@ public async Task ParsesParameters() using var request = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/users/19:4b6bed8d24574f6a9e436813cb2617d8?$select=displayName,givenName,postalCode,identities"); var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); - var result = ModelGraphBuilder.BuildCodeGraph(snippetModel); + var result = new SnippetCodeGraph(snippetModel); var parameter = result.Parameters.First(); Assert.True(result.HasParameters()); @@ -121,13 +121,33 @@ public async Task ParsesParameters() Assert.True(result.HasParameters()); } + [Fact] + public async Task ParsesQueryParametersWithSpaces() + { + using var request = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/roleManagement/directory/roleAssignments?$filter=roleDefinitionId eq '62e90394-69f5-4237-9190-012177145e10'&$expand=principal"); + + var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); + var result = new SnippetCodeGraph(snippetModel); + var parameter = result.Parameters.First(); + + Assert.True(result.HasParameters()); + Assert.Equal(2, result.Parameters.Count()); + + var expectedProperty1 = new CodeProperty { Name = "filter", Value = "roleDefinitionId eq '62e90394-69f5-4237-9190-012177145e10'", PropertyType = PropertyType.String, Children = null }; + Assert.Equal(expectedProperty1, result.Parameters.First()); + + var expectedProperty2 = new CodeProperty { Name = "expand", Value = "principal", PropertyType = PropertyType.String, Children = null }; + Assert.Equal(expectedProperty2, result.Parameters.Skip(1).Take(1).First()); + + } + [Fact] public async Task HasParametersIsFalseWhenNoParamterExists() { using var request = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/users/19:4b6bed8d24574f6a9e436813cb2617d8"); var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); - var result = ModelGraphBuilder.BuildCodeGraph(snippetModel); + var result = new SnippetCodeGraph(snippetModel); Assert.False(result.HasParameters()); } @@ -142,7 +162,7 @@ public async Task ParsesBodyTypeBinary() request.Content.Headers.ContentType = new("application/octet-stream"); var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); - var result = ModelGraphBuilder.BuildCodeGraph(snippetModel); + var result = new SnippetCodeGraph(snippetModel); Assert.Equal(PropertyType.Binary, result.Body.PropertyType); } @@ -162,8 +182,7 @@ public async Task ParsesBodyWithoutProperContentType() Content = new StringContent(sampleBody, Encoding.UTF8) // snippet missing content type }; var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); - var result = ModelGraphBuilder.BuildCodeGraph(snippetModel); - ; + var result = new SnippetCodeGraph(snippetModel); var expectedObject = new CodeProperty { Name = "MessagesPostRequestBody", Value = null, PropertyType = PropertyType.Object, Children = new List() }; @@ -196,7 +215,7 @@ public async Task ParsesBodyPropertyTypeString() Content = new StringContent(TypesSample, Encoding.UTF8) }; var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); - var snippetCodeGraph = ModelGraphBuilder.BuildCodeGraph(snippetModel); + var snippetCodeGraph = new SnippetCodeGraph(snippetModel); // meetingDuration should be a string var property = findProperyInSnipet(snippetCodeGraph.Body, "meetingDuration"); @@ -214,7 +233,7 @@ public async Task ParsesBodyPropertyTypeNumber() Content = new StringContent(TypesSample, Encoding.UTF8) }; var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); - var snippetCodeGraph = ModelGraphBuilder.BuildCodeGraph(snippetModel); + var snippetCodeGraph = new SnippetCodeGraph(snippetModel); var property = findProperyInSnipet(snippetCodeGraph.Body, "minimumAttendeePercentage"); @@ -231,7 +250,7 @@ public async Task ParsesBodyPropertyTypeBoolean() Content = new StringContent(TypesSample, Encoding.UTF8) }; var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); - var snippetCodeGraph = ModelGraphBuilder.BuildCodeGraph(snippetModel); + var snippetCodeGraph = new SnippetCodeGraph(snippetModel); var property = findProperyInSnipet(snippetCodeGraph.Body, "suggestLocation"); @@ -248,7 +267,7 @@ public async Task ParsesBodyPropertyTypeObject() Content = new StringContent(TypesSample, Encoding.UTF8) }; var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); - var snippetCodeGraph = ModelGraphBuilder.BuildCodeGraph(snippetModel); + var snippetCodeGraph = new SnippetCodeGraph(snippetModel); var property = findProperyInSnipet(snippetCodeGraph.Body, "locationConstraint"); @@ -264,7 +283,7 @@ public async Task ParsesBodyPropertyTypeArray() Content = new StringContent(TypesSample, Encoding.UTF8) }; var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); - var snippetCodeGraph = ModelGraphBuilder.BuildCodeGraph(snippetModel); + var snippetCodeGraph = new SnippetCodeGraph(snippetModel); var property = findProperyInSnipet(snippetCodeGraph.Body, "attendees"); @@ -280,7 +299,7 @@ public async Task ParsesBodyPropertyTypeMap() Content = new StringContent(TypesSample, Encoding.UTF8) }; var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); - var snippetCodeGraph = ModelGraphBuilder.BuildCodeGraph(snippetModel); + var snippetCodeGraph = new SnippetCodeGraph(snippetModel); var property = findProperyInSnipet(snippetCodeGraph.Body, "additionalData"); diff --git a/CodeSnippetsReflection.OpenAPI.Test/SnippetModelTests.cs b/CodeSnippetsReflection.OpenAPI.Test/SnippetModelTests.cs index 70eb9c947..4c7b90603 100644 --- a/CodeSnippetsReflection.OpenAPI.Test/SnippetModelTests.cs +++ b/CodeSnippetsReflection.OpenAPI.Test/SnippetModelTests.cs @@ -96,4 +96,4 @@ public async Task GetsTheResponseSchema() Assert.NotEmpty(snippetModel.ResponseSchema.Properties); } } -} \ No newline at end of file +} diff --git a/CodeSnippetsReflection.OpenAPI.Test/TypeScriptGeneratorTest.cs b/CodeSnippetsReflection.OpenAPI.Test/TypeScriptGeneratorTest.cs index 9c30dba8a..ab9208acf 100644 --- a/CodeSnippetsReflection.OpenAPI.Test/TypeScriptGeneratorTest.cs +++ b/CodeSnippetsReflection.OpenAPI.Test/TypeScriptGeneratorTest.cs @@ -44,10 +44,7 @@ public async Task GeneratesTheCorrectFluentAPIPath() } "; - Assert.Contains( - expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), - result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) - ); + AssertUtil.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -76,10 +73,7 @@ public async Task GeneratesClassWithDefaultBodyWhenSchemaNotPresent() "; - Assert.Contains( - expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), - result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) - ); + AssertUtil.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -99,10 +93,7 @@ public async Task GeneratesTheCorrectFluentAPIPathForIndexedCollections() "; - Assert.Contains( - expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), - result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) - ); + AssertUtil.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -130,10 +121,7 @@ public async Task GeneratesTheGetMethodCall() "; - Assert.Contains( - expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), - result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) - ); + AssertUtil.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -149,10 +137,7 @@ public async Task GeneratesThePostMethodCall() "; - Assert.Contains( - expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), - result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) - ); + AssertUtil.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -168,10 +153,7 @@ public async Task GeneratesThePatchMethodCall() "; - Assert.Contains( - expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), - result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) - ); + AssertUtil.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -199,10 +181,7 @@ public async Task GeneratesTheDeleteMethodCall() "; - Assert.Contains( - expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), - result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) - ); + AssertUtil.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -250,10 +229,7 @@ public async Task WritesTheRequestPayload() "; - Assert.Contains( - expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), - result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) - ); + AssertUtil.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -299,11 +275,7 @@ public async Task WritesADouble() } "; - - Assert.Contains( - expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), - result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) - ); + AssertUtil.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -327,10 +299,7 @@ public async Task GeneratesABinaryPayload() } "; - Assert.Contains( - expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), - result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) - ); + AssertUtil.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -357,10 +326,7 @@ public async Task GeneratesABase64UrlPayload() "; - Assert.Contains( - expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), - result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) - ); + AssertUtil.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -387,10 +353,7 @@ public async Task GeneratesADatePayload() "; - Assert.Contains( - expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), - result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) - ); + AssertUtil.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -427,11 +390,7 @@ public async Task GeneratesAnArrayPayloadInAdditionalData() }; "; - - Assert.Contains( - expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), - result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) - ); + AssertUtil.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -468,10 +427,7 @@ public async Task GeneratesAnArrayOfObjectsPayloadData() } "; - Assert.Contains( - expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), - result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) - ); + AssertUtil.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -495,11 +451,7 @@ public async Task GeneratesSelectQueryParameters() } "; - - Assert.Contains( - expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), - result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) - ); + AssertUtil.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -525,10 +477,7 @@ public async Task GeneratesCountBooleanQueryParameters() "; - Assert.Contains( - expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), - result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) - ); + AssertUtil.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -551,12 +500,8 @@ public async Task GeneratesSkipQueryParameters() await graphServiceClient.users.get(configuration); } "; - - - Assert.Contains( - expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), - result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) - ); + + AssertUtil.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -592,10 +537,7 @@ public async Task GeneratesRequestHeaders() } "; - Assert.Contains( - expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), - result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) - ); + AssertUtil.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -650,10 +592,7 @@ public async Task GenerateAdditionalData() } "; - Assert.Contains( - expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), - result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) - ); + AssertUtil.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -705,10 +644,7 @@ public async Task GeneratesEnumsWhenVariableIsEnum() } "; - Assert.Contains( - expected.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty), - result.Replace(" ", String.Empty).Replace(Environment.NewLine, String.Empty) - ); + AssertUtil.ContainsIgnoreWhiteSpace(expected, result); } } } diff --git a/CodeSnippetsReflection.OpenAPI/CodeSnippetsReflection.OpenAPI.csproj b/CodeSnippetsReflection.OpenAPI/CodeSnippetsReflection.OpenAPI.csproj index 3fa375f73..d3b8310d4 100644 --- a/CodeSnippetsReflection.OpenAPI/CodeSnippetsReflection.OpenAPI.csproj +++ b/CodeSnippetsReflection.OpenAPI/CodeSnippetsReflection.OpenAPI.csproj @@ -8,7 +8,6 @@ - diff --git a/CodeSnippetsReflection.OpenAPI/LanguageGenerators/TypeScriptGenerator.cs b/CodeSnippetsReflection.OpenAPI/LanguageGenerators/TypeScriptGenerator.cs index a773c8fc4..a7d14ede9 100644 --- a/CodeSnippetsReflection.OpenAPI/LanguageGenerators/TypeScriptGenerator.cs +++ b/CodeSnippetsReflection.OpenAPI/LanguageGenerators/TypeScriptGenerator.cs @@ -33,7 +33,7 @@ public string GenerateCodeSnippet(SnippetModel snippetModel) { if (snippetModel == null) throw new ArgumentNullException("Argument snippetModel cannot be null"); - var codeGraph = ModelGraphBuilder.BuildCodeGraph(snippetModel); + var codeGraph = new SnippetCodeGraph(snippetModel); var snippetBuilder = new StringBuilder( "//THIS SNIPPET IS A PREVIEW FOR THE KIOTA BASED SDK. NON-PRODUCTION USE ONLY" + Environment.NewLine + $"const {ClientVarName} = {ClientVarType}.init({{authProvider}});{Environment.NewLine}{Environment.NewLine}"); @@ -78,7 +78,7 @@ private static void WriteHeader(SnippetCodeGraph codeGraph, StringBuilder builde builder.AppendLine($"{indentManager.GetIndent()}{RequestHeadersVarName} : {{"); indentManager.Indent(); foreach (var param in codeGraph.Headers) - builder.AppendLine($"{indentManager.GetIndent()}\"{param.Name}\": \"{param.Value.Replace("\"", "\\\"")}\","); + builder.AppendLine($"{indentManager.GetIndent()}\"{param.Name}\": \"{param.Value.EscapeQuotes()}\","); indentManager.Unindent(); builder.AppendLine($"{indentManager.GetIndent()}}}"); } @@ -92,7 +92,7 @@ private static void WriteOptions(SnippetCodeGraph codeGraph, StringBuilder build builder.AppendLine($"{indentManager.GetIndent()}{RequestOptionsVarName} : {{"); indentManager.Indent(); foreach (var param in codeGraph.Options) - builder.AppendLine($"{indentManager.GetIndent()}\"{param.Name}\": \"{param.Value.Replace("\"", "\\\"")}\","); + builder.AppendLine($"{indentManager.GetIndent()}\"{param.Name}\": \"{param.Value.EscapeQuotes()}\","); indentManager.Unindent(); builder.AppendLine($"{indentManager.GetIndent()}}}"); } @@ -107,7 +107,7 @@ private static void WriteParameters(SnippetCodeGraph codeGraph, StringBuilder bu builder.AppendLine($"{indentManager.GetIndent()}{RequestParametersVarName} : {{"); indentManager.Indent(); foreach (var param in codeGraph.Parameters) - builder.AppendLine($"{indentManager.GetIndent()}{NormalizeJsonName(param.Name)}: \"{param.Value.Replace("\"", "\\\"")}\","); + builder.AppendLine($"{indentManager.GetIndent()}{NormalizeJsonName(param.Name)}: \"{param.Value.EscapeQuotes()}\","); indentManager.Unindent(); builder.AppendLine($"{indentManager.GetIndent()}}}"); } diff --git a/CodeSnippetsReflection.OpenAPI/ModelGraph/ModelGraphBuilder.cs b/CodeSnippetsReflection.OpenAPI/ModelGraph/ModelGraphBuilder.cs deleted file mode 100644 index 8757e93bf..000000000 --- a/CodeSnippetsReflection.OpenAPI/ModelGraph/ModelGraphBuilder.cs +++ /dev/null @@ -1,246 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using System.Text.RegularExpressions; -using CodeSnippetsReflection.StringExtensions; -using Microsoft.OpenApi.Models; - -namespace CodeSnippetsReflection.OpenAPI.ModelGraph -{ - - public static class ModelGraphBuilder - { - - private static readonly Regex nestedStatementRegex = new(@"(\w+)(\([^)]+\))", RegexOptions.IgnoreCase | RegexOptions.Compiled); - - private static readonly CodeProperty EMPTY_PROPERTY = new() { Name = null , Value = null , Children = null, PropertyType = PropertyType.Default }; - - public static SnippetCodeGraph BuildCodeGraph(SnippetModel snippetModel) - { - return new SnippetCodeGraph { - ResponseSchema = snippetModel.ResponseSchema, - HttpMethod = snippetModel.Method, - Nodes = snippetModel.PathNodes, - Headers = parseHeaders(snippetModel), - Options = Enumerable.Empty(), - Parameters = parseParameters(snippetModel), - Body = parseBody(snippetModel) - }; - } - - /// - /// Parses Headers Filtering Out 'Host' - /// - private static IEnumerable parseHeaders(SnippetModel snippetModel) - { - return snippetModel.RequestHeaders.Where(h => !h.Key.Equals("Host", StringComparison.OrdinalIgnoreCase)) - .Select(h => new CodeProperty { Name = h.Key , Value = h.Value?.FirstOrDefault(), Children = null, PropertyType = PropertyType.String }) - .ToList(); - } - - - private static List parseParameters(SnippetModel snippetModel) - { - var parameters = new List(); - if (!string.IsNullOrEmpty(snippetModel.QueryString)) - { - var (queryString, replacements) = ReplaceNestedOdataQueryParameters(snippetModel.QueryString); - foreach (var queryParam in queryString.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries)) - { - if (queryParam.Contains('=')) - { - var kvPair = queryParam.Split('=', StringSplitOptions.RemoveEmptyEntries); - parameters.Add(new() { Name = NormalizeQueryParameterName(kvPair[0]), Value = GetQueryParameterValue(kvPair[1], replacements), PropertyType = PropertyType.String }); - } - else - parameters.Add(new() { Name = NormalizeQueryParameterName(queryParam), Value = GetQueryParameterValue("undefined", replacements), PropertyType = PropertyType.String }); - } - - } - return parameters; - } - - private static string NormalizeQueryParameterName(string queryParam) => System.Web.HttpUtility.UrlDecode(queryParam.TrimStart('$').ToFirstCharacterLowerCase()); - - private static (string, Dictionary) ReplaceNestedOdataQueryParameters(string queryParams) - { - var replacements = new Dictionary(); - var matches = nestedStatementRegex.Matches(queryParams); - if (matches.Any()) - foreach (Match match in matches) - { - var key = match.Groups[1].Value; - var value = match.Groups[2].Value; - replacements.Add(key, value); - queryParams = queryParams.Replace(value, string.Empty); - } - return (queryParams, replacements); - } - - private static string GetQueryParameterValue(string originalValue, Dictionary replacements) - { - var escapedParam = System.Web.HttpUtility.UrlDecode(originalValue); - if (escapedParam.Equals("true", StringComparison.OrdinalIgnoreCase) || escapedParam.Equals("false", StringComparison.OrdinalIgnoreCase)) - return escapedParam.ToLowerInvariant(); - else if (int.TryParse(escapedParam, out var intValue)) - return intValue.ToString(); - else - { - return escapedParam.Split(',') - .Select(v => replacements.ContainsKey(v) ? v + replacements[v] : v) - .Aggregate((a, b) => $"{a},{b}"); - } - } - - private static CodeProperty parseBody(SnippetModel snippetModel) - { - if (string.IsNullOrWhiteSpace(snippetModel?.RequestBody)) - return EMPTY_PROPERTY; - - switch (snippetModel.ContentType?.Split(';').First().ToLowerInvariant()) - { - case "application/json": - return TryParseBody(snippetModel); - case "application/octet-stream": - return new() { Name = null, Value = snippetModel.RequestBody?.ToString(), Children = null, PropertyType = PropertyType.Binary }; - default: - return TryParseBody(snippetModel);//in case the content type header is missing but we still have a json payload - } - } - - private static string ComputeRequestBody(SnippetModel snippetModel) - { - var nodes = snippetModel.PathNodes; - if (!(nodes?.Any() ?? false)) return string.Empty; - - var nodeName = nodes.Where(x => !x.Segment.IsCollectionIndex()) - .Select(x => - { - if (x.Segment.IsFunction()) - return x.Segment.Split('.').Last(); - else - return x.Segment; - }) - .Last() - .ToFirstCharacterUpperCase(); - - var singularNodeName = nodeName[nodeName.Length - 1] == 's' ? nodeName.Substring(0, nodeName.Length - 1) : nodeName; - - if (nodes.Last()?.Segment?.IsCollectionIndex() == true) - return singularNodeName; - else - return $"{nodeName}PostRequestBody"; - - } - - private static CodeProperty TryParseBody(SnippetModel snippetModel) - { - if (!snippetModel.IsRequestBodyValid) - throw new InvalidOperationException($"Unsupported content type: {snippetModel.ContentType}"); - - using var parsedBody = JsonDocument.Parse(snippetModel.RequestBody, new JsonDocumentOptions { AllowTrailingCommas = true }); - var schema = snippetModel.RequestSchema; - var className = schema.GetSchemaTitle().ToFirstCharacterUpperCase() ?? ComputeRequestBody(snippetModel); - return parseJsonObjectValue(className, parsedBody.RootElement, schema); - } - - private static CodeProperty parseJsonObjectValue(String rootPropertyName, JsonElement value, OpenApiSchema schema) - { - var children = new List(); - - if (value.ValueKind != JsonValueKind.Object) throw new InvalidOperationException($"Expected JSON object and got {value.ValueKind}"); - - var propertiesAndSchema = value.EnumerateObject() - .Select(x => new Tuple(x, schema.GetPropertySchema(x.Name))); - foreach (var propertyAndSchema in propertiesAndSchema.Where(x => x.Item2 != null)) - { - var propertyName = propertyAndSchema.Item1.Name.ToFirstCharacterLowerCase(); - children.Add(parseProperty(propertyName, propertyAndSchema.Item1.Value, propertyAndSchema.Item2)); - } - - var propertiesWithoutSchema = propertiesAndSchema.Where(x => x.Item2 == null).Select(x => x.Item1); - if (propertiesWithoutSchema.Any()) - { - - var additionalChildren = new List(); - foreach (var property in propertiesWithoutSchema) - additionalChildren.Add(parseProperty(property.Name, property.Value, null)); - - if (additionalChildren.Any()) - children.Add(new CodeProperty { Name = "additionalData", PropertyType = PropertyType.Map, Children = additionalChildren }); - } - - return new CodeProperty { Name = rootPropertyName, PropertyType = PropertyType.Object, Children = children }; - } - - private static String escapeSpecialCharacters(string value) - { - return value?.Replace("\"", "\\\"")?.Replace("\n", "\\n")?.Replace("\r", "\\r"); - } - - private static CodeProperty evaluateStringProperty(string propertyName, JsonElement value, OpenApiSchema propSchema) - { - if (propSchema?.Format?.Equals("base64url", StringComparison.OrdinalIgnoreCase) ?? false) - return new CodeProperty { Name = propertyName, Value = value.GetString(), PropertyType = PropertyType.Base64Url, Children = new List() }; - - if (propSchema?.Format?.Equals("date-time", StringComparison.OrdinalIgnoreCase) ?? false) - return new CodeProperty { Name = propertyName, Value = value.GetString(), PropertyType = PropertyType.Date, Children = new List() }; - - - var enumSchema = propSchema?.AnyOf.FirstOrDefault(x => x.Enum.Count > 0); - if (enumSchema == null) - return new CodeProperty { Name = propertyName, Value = escapeSpecialCharacters(value.GetString()), PropertyType = PropertyType.String, Children = new List() }; - - - var propValue = String.IsNullOrWhiteSpace(value.GetString()) ? null : $"{enumSchema.Title.ToFirstCharacterUpperCase()}.{value.GetString().ToFirstCharacterUpperCase()}"; - return new CodeProperty { Name = propertyName, Value = propValue, PropertyType = PropertyType.Enum, Children = new List() }; - } - - private static CodeProperty parseProperty(string propertyName, JsonElement value, OpenApiSchema propSchema) - { - switch (value.ValueKind) - { - case JsonValueKind.String: - return evaluateStringProperty(propertyName, value, propSchema); - case JsonValueKind.Number: - return new CodeProperty { Name = propertyName, Value = $"{value}", PropertyType = PropertyType.Number, Children = new List() }; - case JsonValueKind.False: - case JsonValueKind.True: - return new CodeProperty { Name = propertyName, Value = value.GetBoolean().ToString(), PropertyType = PropertyType.Boolean, Children = new List() }; - case JsonValueKind.Null: - return new CodeProperty { Name = propertyName, Value = "null", PropertyType = PropertyType.Null, Children = new List() }; - case JsonValueKind.Object: - if (propSchema != null) - return parseJsonObjectValue(propertyName, value, propSchema); - else - return parseAnonymousObjectValues(propertyName, value, propSchema); - case JsonValueKind.Array: - return parseJsonArrayValue(propertyName, value, propSchema); - default: - throw new NotImplementedException($"Unsupported JsonValueKind: {value.ValueKind}"); - } - } - - private static CodeProperty parseJsonArrayValue(string propertyName, JsonElement value, OpenApiSchema schema) - { - var children = value.EnumerateArray().Select(item => parseProperty(schema.GetSchemaTitle().ToFirstCharacterUpperCase(), item, schema)).ToList(); - return new CodeProperty { Name = propertyName, Value = null, PropertyType = PropertyType.Array, Children = children }; - } - - private static CodeProperty parseAnonymousObjectValues(string propertyName, JsonElement value, OpenApiSchema schema) - { - if (value.ValueKind != JsonValueKind.Object) throw new InvalidOperationException($"Expected JSON object and got {value.ValueKind}"); - - var children = new List(); - var propertiesAndSchema = value.EnumerateObject() - .Select(x => new Tuple(x, schema.GetPropertySchema(x.Name))); - foreach (var propertyAndSchema in propertiesAndSchema) - { - children.Add(parseProperty(propertyAndSchema.Item1.Name.ToFirstCharacterLowerCase(), propertyAndSchema.Item1.Value, propertyAndSchema.Item2)); - } - - return new CodeProperty { Name = propertyName, Value = null, PropertyType = PropertyType.Object, Children = children }; - } - } -} diff --git a/CodeSnippetsReflection.OpenAPI/ModelGraph/SnippetCodeGraph.cs b/CodeSnippetsReflection.OpenAPI/ModelGraph/SnippetCodeGraph.cs index 3b1551644..b5c65283d 100644 --- a/CodeSnippetsReflection.OpenAPI/ModelGraph/SnippetCodeGraph.cs +++ b/CodeSnippetsReflection.OpenAPI/ModelGraph/SnippetCodeGraph.cs @@ -1,14 +1,35 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; using System.Net.Http; +using System.Text.Json; +using System.Text.RegularExpressions; +using System.Web; +using CodeSnippetsReflection.StringExtensions; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Services; namespace CodeSnippetsReflection.OpenAPI.ModelGraph { - public class SnippetCodeGraph + public record SnippetCodeGraph { + + private static readonly Regex nestedStatementRegex = new(@"(\w+)(\([^)]+\))", RegexOptions.IgnoreCase | RegexOptions.Compiled); + + private static readonly CodeProperty EMPTY_PROPERTY = new() { Name = null, Value = null, Children = null, PropertyType = PropertyType.Default }; + + public SnippetCodeGraph(SnippetModel snippetModel) + { + ResponseSchema = snippetModel.ResponseSchema; + HttpMethod = snippetModel.Method; + Nodes = snippetModel.PathNodes; + Headers = parseHeaders(snippetModel); + Options = Enumerable.Empty(); + Parameters = parseParameters(snippetModel); + Body = parseBody(snippetModel); + } + public OpenApiSchema ResponseSchema { get; set; @@ -45,17 +66,17 @@ public IEnumerable Nodes public Boolean HasHeaders() { - return Headers.Any() == true; + return Headers.Any(); } public Boolean HasOptions() { - return Options.Any() == true; + return Options.Any(); } public Boolean HasParameters() { - return Parameters.Any() == true; + return Parameters.Any(); } public Boolean HasBody() @@ -63,5 +84,216 @@ public Boolean HasBody() return Body.PropertyType != PropertyType.Default; } + + /// + /// Parses Headers Filtering Out 'Host' + /// + private static IEnumerable parseHeaders(SnippetModel snippetModel) + { + return snippetModel.RequestHeaders.Where(h => !h.Key.Equals("Host", StringComparison.OrdinalIgnoreCase)) + .Select(h => new CodeProperty { Name = h.Key, Value = h.Value?.FirstOrDefault(), Children = null, PropertyType = PropertyType.String }) + .ToList(); + } + + + private static List parseParameters(SnippetModel snippetModel) + { + var parameters = new List(); + if (!string.IsNullOrEmpty(snippetModel.QueryString)) + { + var (queryString, replacements) = ReplaceNestedOdataQueryParameters(snippetModel.QueryString); + + NameValueCollection queryCollection = HttpUtility.ParseQueryString(queryString); + foreach (String key in queryCollection.AllKeys) + { + parameters.Add(new() { Name = NormalizeQueryParameterName(key), Value = GetQueryParameterValue(queryCollection[key], replacements), PropertyType = PropertyType.String }); + } + + } + return parameters; + } + + private static string NormalizeQueryParameterName(string queryParam) => System.Web.HttpUtility.UrlDecode(queryParam.TrimStart('$').ToFirstCharacterLowerCase()); + + private static (string, Dictionary) ReplaceNestedOdataQueryParameters(string queryParams) + { + var replacements = new Dictionary(); + var matches = nestedStatementRegex.Matches(queryParams); + if (matches.Any()) + foreach (Match match in matches) + { + var key = match.Groups[1].Value; + var value = match.Groups[2].Value; + replacements.Add(key, value); + queryParams = queryParams.Replace(value, string.Empty); + } + return (queryParams, replacements); + } + + private static string GetQueryParameterValue(string originalValue, Dictionary replacements) + { + var escapedParam = System.Web.HttpUtility.UrlDecode(originalValue); + if (escapedParam.Equals("true", StringComparison.OrdinalIgnoreCase) || escapedParam.Equals("false", StringComparison.OrdinalIgnoreCase)) + return escapedParam.ToLowerInvariant(); + else if (int.TryParse(escapedParam, out var intValue)) + return intValue.ToString(); + else + { + return escapedParam.Split(',') + .Select(v => replacements.ContainsKey(v) ? v + replacements[v] : v) + .Aggregate((a, b) => $"{a},{b}"); + } + } + + private static CodeProperty parseBody(SnippetModel snippetModel) + { + if (string.IsNullOrWhiteSpace(snippetModel?.RequestBody)) + return EMPTY_PROPERTY; + + switch (snippetModel.ContentType?.Split(';').First().ToLowerInvariant()) + { + case "application/json": + return TryParseBody(snippetModel); + case "application/octet-stream": + return new() { Name = null, Value = snippetModel.RequestBody?.ToString(), Children = null, PropertyType = PropertyType.Binary }; + default: + return TryParseBody(snippetModel);//in case the content type header is missing but we still have a json payload + } + } + + private static string ComputeRequestBody(SnippetModel snippetModel) + { + var nodes = snippetModel.PathNodes; + if (!(nodes?.Any() ?? false)) return string.Empty; + + var nodeName = nodes.Where(x => !x.Segment.IsCollectionIndex()) + .Select(x => + { + if (x.Segment.IsFunction()) + return x.Segment.Split('.').Last(); + else + return x.Segment; + }) + .Last() + .ToFirstCharacterUpperCase(); + + var singularNodeName = nodeName[nodeName.Length - 1] == 's' ? nodeName.Substring(0, nodeName.Length - 1) : nodeName; + + if (nodes.Last()?.Segment?.IsCollectionIndex() == true) + return singularNodeName; + else + return $"{nodeName}PostRequestBody"; + + } + + private static CodeProperty TryParseBody(SnippetModel snippetModel) + { + if (!snippetModel.IsRequestBodyValid) + throw new InvalidOperationException($"Unsupported content type: {snippetModel.ContentType}"); + + using var parsedBody = JsonDocument.Parse(snippetModel.RequestBody, new JsonDocumentOptions { AllowTrailingCommas = true }); + var schema = snippetModel.RequestSchema; + var className = schema.GetSchemaTitle().ToFirstCharacterUpperCase() ?? ComputeRequestBody(snippetModel); + return parseJsonObjectValue(className, parsedBody.RootElement, schema); + } + + private static CodeProperty parseJsonObjectValue(String rootPropertyName, JsonElement value, OpenApiSchema schema) + { + var children = new List(); + + if (value.ValueKind != JsonValueKind.Object) throw new InvalidOperationException($"Expected JSON object and got {value.ValueKind}"); + + var propertiesAndSchema = value.EnumerateObject() + .Select(x => new Tuple(x, schema.GetPropertySchema(x.Name))); + foreach (var propertyAndSchema in propertiesAndSchema.Where(x => x.Item2 != null)) + { + var propertyName = propertyAndSchema.Item1.Name.ToFirstCharacterLowerCase(); + children.Add(parseProperty(propertyName, propertyAndSchema.Item1.Value, propertyAndSchema.Item2)); + } + + var propertiesWithoutSchema = propertiesAndSchema.Where(x => x.Item2 == null).Select(x => x.Item1); + if (propertiesWithoutSchema.Any()) + { + + var additionalChildren = new List(); + foreach (var property in propertiesWithoutSchema) + additionalChildren.Add(parseProperty(property.Name, property.Value, null)); + + if (additionalChildren.Any()) + children.Add(new CodeProperty { Name = "additionalData", PropertyType = PropertyType.Map, Children = additionalChildren }); + } + + return new CodeProperty { Name = rootPropertyName, PropertyType = PropertyType.Object, Children = children }; + } + + private static String escapeSpecialCharacters(string value) + { + return value?.Replace("\"", "\\\"")?.Replace("\n", "\\n")?.Replace("\r", "\\r"); + } + + private static CodeProperty evaluateStringProperty(string propertyName, JsonElement value, OpenApiSchema propSchema) + { + if (propSchema?.Format?.Equals("base64url", StringComparison.OrdinalIgnoreCase) ?? false) + return new CodeProperty { Name = propertyName, Value = value.GetString(), PropertyType = PropertyType.Base64Url, Children = new List() }; + + if (propSchema?.Format?.Equals("date-time", StringComparison.OrdinalIgnoreCase) ?? false) + return new CodeProperty { Name = propertyName, Value = value.GetString(), PropertyType = PropertyType.Date, Children = new List() }; + + + var enumSchema = propSchema?.AnyOf.FirstOrDefault(x => x.Enum.Count > 0); + if (enumSchema == null) + return new CodeProperty { Name = propertyName, Value = escapeSpecialCharacters(value.GetString()), PropertyType = PropertyType.String, Children = new List() }; + + + var propValue = String.IsNullOrWhiteSpace(value.GetString()) ? null : $"{enumSchema.Title.ToFirstCharacterUpperCase()}.{value.GetString().ToFirstCharacterUpperCase()}"; + return new CodeProperty { Name = propertyName, Value = propValue, PropertyType = PropertyType.Enum, Children = new List() }; + } + + private static CodeProperty parseProperty(string propertyName, JsonElement value, OpenApiSchema propSchema) + { + switch (value.ValueKind) + { + case JsonValueKind.String: + return evaluateStringProperty(propertyName, value, propSchema); + case JsonValueKind.Number: + return new CodeProperty { Name = propertyName, Value = $"{value}", PropertyType = PropertyType.Number, Children = new List() }; + case JsonValueKind.False: + case JsonValueKind.True: + return new CodeProperty { Name = propertyName, Value = value.GetBoolean().ToString(), PropertyType = PropertyType.Boolean, Children = new List() }; + case JsonValueKind.Null: + return new CodeProperty { Name = propertyName, Value = "null", PropertyType = PropertyType.Null, Children = new List() }; + case JsonValueKind.Object: + if (propSchema != null) + return parseJsonObjectValue(propertyName, value, propSchema); + else + return parseAnonymousObjectValues(propertyName, value, propSchema); + case JsonValueKind.Array: + return parseJsonArrayValue(propertyName, value, propSchema); + default: + throw new NotImplementedException($"Unsupported JsonValueKind: {value.ValueKind}"); + } + } + + private static CodeProperty parseJsonArrayValue(string propertyName, JsonElement value, OpenApiSchema schema) + { + var children = value.EnumerateArray().Select(item => parseProperty(schema.GetSchemaTitle().ToFirstCharacterUpperCase(), item, schema)).ToList(); + return new CodeProperty { Name = propertyName, Value = null, PropertyType = PropertyType.Array, Children = children }; + } + + private static CodeProperty parseAnonymousObjectValues(string propertyName, JsonElement value, OpenApiSchema schema) + { + if (value.ValueKind != JsonValueKind.Object) throw new InvalidOperationException($"Expected JSON object and got {value.ValueKind}"); + + var children = new List(); + var propertiesAndSchema = value.EnumerateObject() + .Select(x => new Tuple(x, schema.GetPropertySchema(x.Name))); + foreach (var propertyAndSchema in propertiesAndSchema) + { + children.Add(parseProperty(propertyAndSchema.Item1.Name.ToFirstCharacterLowerCase(), propertyAndSchema.Item1.Value, propertyAndSchema.Item2)); + } + + return new CodeProperty { Name = propertyName, Value = null, PropertyType = PropertyType.Object, Children = children }; + } } + } diff --git a/CodeSnippetsReflection/StringExtensions/StringExtensions.cs b/CodeSnippetsReflection/StringExtensions/StringExtensions.cs index dbec33690..3f8e1a9dd 100644 --- a/CodeSnippetsReflection/StringExtensions/StringExtensions.cs +++ b/CodeSnippetsReflection/StringExtensions/StringExtensions.cs @@ -34,5 +34,10 @@ public static string ToFirstCharacterUpperCaseAfterCharacter(this string stringV if (charIndex < 0) return stringValue; return stringValue[0..charIndex] + char.ToUpper(stringValue[charIndex + 1]) + stringValue[(charIndex + 2)..].ToFirstCharacterUpperCaseAfterCharacter(character); } + + public static string EscapeQuotes(this string stringValue) + { + return stringValue.Replace("\"", "\\\""); + } } } From 46f91f48884d2a465764cceefd49e52bff06cddd Mon Sep 17 00:00:00 2001 From: Ronald K <43806892+rkodev@users.noreply.github.com> Date: Tue, 31 May 2022 16:06:45 +0300 Subject: [PATCH 09/14] Update CodeSnippetsReflection.OpenAPI/ModelGraph/SnippetCodeGraph.cs Co-authored-by: Vincent Biret --- .../ModelGraph/SnippetCodeGraph.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CodeSnippetsReflection.OpenAPI/ModelGraph/SnippetCodeGraph.cs b/CodeSnippetsReflection.OpenAPI/ModelGraph/SnippetCodeGraph.cs index b5c65283d..03627ff25 100644 --- a/CodeSnippetsReflection.OpenAPI/ModelGraph/SnippetCodeGraph.cs +++ b/CodeSnippetsReflection.OpenAPI/ModelGraph/SnippetCodeGraph.cs @@ -120,10 +120,10 @@ private static (string, Dictionary) ReplaceNestedOdataQueryParam var replacements = new Dictionary(); var matches = nestedStatementRegex.Matches(queryParams); if (matches.Any()) - foreach (Match match in matches) + foreach (var groups in matches.Select(m => m.Groups))) { - var key = match.Groups[1].Value; - var value = match.Groups[2].Value; + var key = groups[1].Value; + var value = groups[2].Value; replacements.Add(key, value); queryParams = queryParams.Replace(value, string.Empty); } From 9289f85429445fe9e77a2296a06e3cc1b9d7f4b4 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Tue, 31 May 2022 09:13:11 -0400 Subject: [PATCH 10/14] - additional parenthesis fix --- CodeSnippetsReflection.OpenAPI/ModelGraph/SnippetCodeGraph.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CodeSnippetsReflection.OpenAPI/ModelGraph/SnippetCodeGraph.cs b/CodeSnippetsReflection.OpenAPI/ModelGraph/SnippetCodeGraph.cs index 03627ff25..0233a00b4 100644 --- a/CodeSnippetsReflection.OpenAPI/ModelGraph/SnippetCodeGraph.cs +++ b/CodeSnippetsReflection.OpenAPI/ModelGraph/SnippetCodeGraph.cs @@ -120,7 +120,7 @@ private static (string, Dictionary) ReplaceNestedOdataQueryParam var replacements = new Dictionary(); var matches = nestedStatementRegex.Matches(queryParams); if (matches.Any()) - foreach (var groups in matches.Select(m => m.Groups))) + foreach (var groups in matches.Select(m => m.Groups)) { var key = groups[1].Value; var value = groups[2].Value; From 3483edd3bfd90008f86f388a792a3a61248e675d Mon Sep 17 00:00:00 2001 From: Ronald K <43806892+rkodev@users.noreply.github.com> Date: Tue, 31 May 2022 16:23:39 +0300 Subject: [PATCH 11/14] Update CodeSnippetsReflection.OpenAPI.Test/SnippetCodeGraphTests.cs Co-authored-by: Vincent Biret --- CodeSnippetsReflection.OpenAPI.Test/SnippetCodeGraphTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CodeSnippetsReflection.OpenAPI.Test/SnippetCodeGraphTests.cs b/CodeSnippetsReflection.OpenAPI.Test/SnippetCodeGraphTests.cs index 60566dbef..55d36fed1 100644 --- a/CodeSnippetsReflection.OpenAPI.Test/SnippetCodeGraphTests.cs +++ b/CodeSnippetsReflection.OpenAPI.Test/SnippetCodeGraphTests.cs @@ -199,8 +199,7 @@ public async Task ParsesBodyWithoutProperContentType() { foreach (var param in codeProperty.Children) { - var result = findProperyInSnipet(param, name); - if (result != null) return result; + if(findProperyInSnipet(param, name) is CodeProperty result) return result; } } From f5483d4fc4084b34b194254db20d1a7fd7e964b7 Mon Sep 17 00:00:00 2001 From: Ronald K <43806892+rkodev@users.noreply.github.com> Date: Thu, 2 Jun 2022 12:50:20 +0300 Subject: [PATCH 12/14] Update CodeSnippetsReflection.OpenAPI.Test/SnippetCodeGraphTests.cs Co-authored-by: Mustafa Zengin --- CodeSnippetsReflection.OpenAPI.Test/SnippetCodeGraphTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CodeSnippetsReflection.OpenAPI.Test/SnippetCodeGraphTests.cs b/CodeSnippetsReflection.OpenAPI.Test/SnippetCodeGraphTests.cs index 55d36fed1..59916fbaa 100644 --- a/CodeSnippetsReflection.OpenAPI.Test/SnippetCodeGraphTests.cs +++ b/CodeSnippetsReflection.OpenAPI.Test/SnippetCodeGraphTests.cs @@ -88,7 +88,7 @@ public async Task ParsesHeaders() } [Fact] - public async Task HasHeadeIsFalseWhenNoneIsInRequest() + public async Task HasHeadersIsFalseWhenNoneIsInRequest() { using var request = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/users"); request.Headers.Add("Host", "graph.microsoft.com"); From 035fea4da1e1e3e3db1e7ea46b11ec4a7c746746 Mon Sep 17 00:00:00 2001 From: Ronald K <43806892+rkodev@users.noreply.github.com> Date: Thu, 2 Jun 2022 12:50:31 +0300 Subject: [PATCH 13/14] Rename AssertUtil to AssertExtenstions --- .../{AssertUtil.cs => AssertExtensions.cs} | 2 +- .../SnippetCodeGraphTests.cs | 46 ++++++++----------- .../TypeScriptGeneratorTest.cs | 40 ++++++++-------- 3 files changed, 41 insertions(+), 47 deletions(-) rename CodeSnippetsReflection.OpenAPI.Test/{AssertUtil.cs => AssertExtensions.cs} (93%) diff --git a/CodeSnippetsReflection.OpenAPI.Test/AssertUtil.cs b/CodeSnippetsReflection.OpenAPI.Test/AssertExtensions.cs similarity index 93% rename from CodeSnippetsReflection.OpenAPI.Test/AssertUtil.cs rename to CodeSnippetsReflection.OpenAPI.Test/AssertExtensions.cs index d7cf7f26f..d45d4fae1 100644 --- a/CodeSnippetsReflection.OpenAPI.Test/AssertUtil.cs +++ b/CodeSnippetsReflection.OpenAPI.Test/AssertExtensions.cs @@ -7,7 +7,7 @@ namespace CodeSnippetsReflection.OpenAPI.Test { - public static class AssertUtil + public static class AssertExtensions { public static void ContainsIgnoreWhiteSpace(string expectedSubstring, string actualString) { diff --git a/CodeSnippetsReflection.OpenAPI.Test/SnippetCodeGraphTests.cs b/CodeSnippetsReflection.OpenAPI.Test/SnippetCodeGraphTests.cs index 59916fbaa..94c3fc62f 100644 --- a/CodeSnippetsReflection.OpenAPI.Test/SnippetCodeGraphTests.cs +++ b/CodeSnippetsReflection.OpenAPI.Test/SnippetCodeGraphTests.cs @@ -137,12 +137,12 @@ public async Task ParsesQueryParametersWithSpaces() Assert.Equal(expectedProperty1, result.Parameters.First()); var expectedProperty2 = new CodeProperty { Name = "expand", Value = "principal", PropertyType = PropertyType.String, Children = null }; - Assert.Equal(expectedProperty2, result.Parameters.Skip(1).Take(1).First()); + Assert.Equal(expectedProperty2, result.Parameters.Skip(1).First()); } [Fact] - public async Task HasParametersIsFalseWhenNoParamterExists() + public async Task HasParametersIsFalseWhenNoParameterExists() { using var request = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/users/19:4b6bed8d24574f6a9e436813cb2617d8"); @@ -191,7 +191,7 @@ public async Task ParsesBodyWithoutProperContentType() Assert.Equal(expectedObject.PropertyType, result.Body.PropertyType); } - private CodeProperty? findProperyInSnipet(CodeProperty codeProperty, string name) + private CodeProperty FindPropertyInSnippet(CodeProperty codeProperty, string name) { if (codeProperty.Name == name) return codeProperty; @@ -199,11 +199,11 @@ public async Task ParsesBodyWithoutProperContentType() { foreach (var param in codeProperty.Children) { - if(findProperyInSnipet(param, name) is CodeProperty result) return result; + if(FindPropertyInSnippet(param, name) is CodeProperty result) return result; } } - return null; + throw new ArgumentException("Property does not containt child " + name); } [Fact] @@ -217,11 +217,10 @@ public async Task ParsesBodyPropertyTypeString() var snippetCodeGraph = new SnippetCodeGraph(snippetModel); // meetingDuration should be a string - var property = findProperyInSnipet(snippetCodeGraph.Body, "meetingDuration"); + var property = FindPropertyInSnippet(snippetCodeGraph.Body, "meetingDuration"); - Assert.NotNull(property); - Assert.Equal(PropertyType.String, property?.PropertyType); - Assert.Equal("PT1H", property?.Value); + Assert.Equal(PropertyType.String, property.PropertyType); + Assert.Equal("PT1H", property.Value); } [Fact] @@ -234,11 +233,10 @@ public async Task ParsesBodyPropertyTypeNumber() var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); var snippetCodeGraph = new SnippetCodeGraph(snippetModel); - var property = findProperyInSnipet(snippetCodeGraph.Body, "minimumAttendeePercentage"); + var property = FindPropertyInSnippet(snippetCodeGraph.Body, "minimumAttendeePercentage"); - Assert.NotNull(property); - Assert.Equal(PropertyType.Number, property?.PropertyType); - Assert.Equal("100" , property?.Value); + Assert.Equal(PropertyType.Number, property.PropertyType); + Assert.Equal("100" , property.Value); } [Fact] @@ -251,11 +249,10 @@ public async Task ParsesBodyPropertyTypeBoolean() var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); var snippetCodeGraph = new SnippetCodeGraph(snippetModel); - var property = findProperyInSnipet(snippetCodeGraph.Body, "suggestLocation"); + var property = FindPropertyInSnippet(snippetCodeGraph.Body, "suggestLocation"); - Assert.NotNull(property); - Assert.Equal(PropertyType.Boolean, property?.PropertyType); - Assert.Equal("False", property?.Value); + Assert.Equal(PropertyType.Boolean, property.PropertyType); + Assert.Equal("False", property.Value); } [Fact] @@ -268,10 +265,9 @@ public async Task ParsesBodyPropertyTypeObject() var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); var snippetCodeGraph = new SnippetCodeGraph(snippetModel); - var property = findProperyInSnipet(snippetCodeGraph.Body, "locationConstraint"); + var property = FindPropertyInSnippet(snippetCodeGraph.Body, "locationConstraint"); - Assert.NotNull(property); - Assert.Equal(PropertyType.Object, property?.PropertyType); + Assert.Equal(PropertyType.Object, property.PropertyType); } [Fact] @@ -284,10 +280,9 @@ public async Task ParsesBodyPropertyTypeArray() var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); var snippetCodeGraph = new SnippetCodeGraph(snippetModel); - var property = findProperyInSnipet(snippetCodeGraph.Body, "attendees"); + var property = FindPropertyInSnippet(snippetCodeGraph.Body, "attendees"); - Assert.NotNull(property); - Assert.Equal(PropertyType.Array, property?.PropertyType); + Assert.Equal(PropertyType.Array, property.PropertyType); } [Fact] @@ -300,10 +295,9 @@ public async Task ParsesBodyPropertyTypeMap() var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); var snippetCodeGraph = new SnippetCodeGraph(snippetModel); - var property = findProperyInSnipet(snippetCodeGraph.Body, "additionalData"); + var property = FindPropertyInSnippet(snippetCodeGraph.Body, "additionalData"); - Assert.NotNull(property); - Assert.Equal(PropertyType.Map, property?.PropertyType); + Assert.Equal(PropertyType.Map, property.PropertyType); } } diff --git a/CodeSnippetsReflection.OpenAPI.Test/TypeScriptGeneratorTest.cs b/CodeSnippetsReflection.OpenAPI.Test/TypeScriptGeneratorTest.cs index ab9208acf..363752e49 100644 --- a/CodeSnippetsReflection.OpenAPI.Test/TypeScriptGeneratorTest.cs +++ b/CodeSnippetsReflection.OpenAPI.Test/TypeScriptGeneratorTest.cs @@ -44,7 +44,7 @@ public async Task GeneratesTheCorrectFluentAPIPath() } "; - AssertUtil.ContainsIgnoreWhiteSpace(expected, result); + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -73,7 +73,7 @@ public async Task GeneratesClassWithDefaultBodyWhenSchemaNotPresent() "; - AssertUtil.ContainsIgnoreWhiteSpace(expected, result); + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -93,7 +93,7 @@ public async Task GeneratesTheCorrectFluentAPIPathForIndexedCollections() "; - AssertUtil.ContainsIgnoreWhiteSpace(expected, result); + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -121,7 +121,7 @@ public async Task GeneratesTheGetMethodCall() "; - AssertUtil.ContainsIgnoreWhiteSpace(expected, result); + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -137,7 +137,7 @@ public async Task GeneratesThePostMethodCall() "; - AssertUtil.ContainsIgnoreWhiteSpace(expected, result); + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -153,7 +153,7 @@ public async Task GeneratesThePatchMethodCall() "; - AssertUtil.ContainsIgnoreWhiteSpace(expected, result); + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -181,7 +181,7 @@ public async Task GeneratesTheDeleteMethodCall() "; - AssertUtil.ContainsIgnoreWhiteSpace(expected, result); + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -229,7 +229,7 @@ public async Task WritesTheRequestPayload() "; - AssertUtil.ContainsIgnoreWhiteSpace(expected, result); + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -275,7 +275,7 @@ public async Task WritesADouble() } "; - AssertUtil.ContainsIgnoreWhiteSpace(expected, result); + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -299,7 +299,7 @@ public async Task GeneratesABinaryPayload() } "; - AssertUtil.ContainsIgnoreWhiteSpace(expected, result); + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -326,7 +326,7 @@ public async Task GeneratesABase64UrlPayload() "; - AssertUtil.ContainsIgnoreWhiteSpace(expected, result); + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -353,7 +353,7 @@ public async Task GeneratesADatePayload() "; - AssertUtil.ContainsIgnoreWhiteSpace(expected, result); + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -390,7 +390,7 @@ public async Task GeneratesAnArrayPayloadInAdditionalData() }; "; - AssertUtil.ContainsIgnoreWhiteSpace(expected, result); + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -427,7 +427,7 @@ public async Task GeneratesAnArrayOfObjectsPayloadData() } "; - AssertUtil.ContainsIgnoreWhiteSpace(expected, result); + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -451,7 +451,7 @@ public async Task GeneratesSelectQueryParameters() } "; - AssertUtil.ContainsIgnoreWhiteSpace(expected, result); + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -477,7 +477,7 @@ public async Task GeneratesCountBooleanQueryParameters() "; - AssertUtil.ContainsIgnoreWhiteSpace(expected, result); + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -501,7 +501,7 @@ public async Task GeneratesSkipQueryParameters() } "; - AssertUtil.ContainsIgnoreWhiteSpace(expected, result); + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -537,7 +537,7 @@ public async Task GeneratesRequestHeaders() } "; - AssertUtil.ContainsIgnoreWhiteSpace(expected, result); + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -592,7 +592,7 @@ public async Task GenerateAdditionalData() } "; - AssertUtil.ContainsIgnoreWhiteSpace(expected, result); + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -644,7 +644,7 @@ public async Task GeneratesEnumsWhenVariableIsEnum() } "; - AssertUtil.ContainsIgnoreWhiteSpace(expected, result); + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } } } From f5548a02a883ce889646eba33d94c3cfb3083c6e Mon Sep 17 00:00:00 2001 From: rkodev <43806892+rkodev@users.noreply.github.com> Date: Thu, 2 Jun 2022 15:07:20 +0300 Subject: [PATCH 14/14] Fix broken tests --- .../SnippetCodeGraphTests.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/CodeSnippetsReflection.OpenAPI.Test/SnippetCodeGraphTests.cs b/CodeSnippetsReflection.OpenAPI.Test/SnippetCodeGraphTests.cs index 94c3fc62f..1adf45e2f 100644 --- a/CodeSnippetsReflection.OpenAPI.Test/SnippetCodeGraphTests.cs +++ b/CodeSnippetsReflection.OpenAPI.Test/SnippetCodeGraphTests.cs @@ -191,7 +191,7 @@ public async Task ParsesBodyWithoutProperContentType() Assert.Equal(expectedObject.PropertyType, result.Body.PropertyType); } - private CodeProperty FindPropertyInSnippet(CodeProperty codeProperty, string name) + private CodeProperty? FindPropertyInSnippet(CodeProperty codeProperty, string name) { if (codeProperty.Name == name) return codeProperty; @@ -203,7 +203,7 @@ private CodeProperty FindPropertyInSnippet(CodeProperty codeProperty, string nam } } - throw new ArgumentException("Property does not containt child " + name); + return null; } [Fact] @@ -217,7 +217,7 @@ public async Task ParsesBodyPropertyTypeString() var snippetCodeGraph = new SnippetCodeGraph(snippetModel); // meetingDuration should be a string - var property = FindPropertyInSnippet(snippetCodeGraph.Body, "meetingDuration"); + var property = FindPropertyInSnippet(snippetCodeGraph.Body, "meetingDuration").Value; Assert.Equal(PropertyType.String, property.PropertyType); Assert.Equal("PT1H", property.Value); @@ -233,7 +233,7 @@ public async Task ParsesBodyPropertyTypeNumber() var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); var snippetCodeGraph = new SnippetCodeGraph(snippetModel); - var property = FindPropertyInSnippet(snippetCodeGraph.Body, "minimumAttendeePercentage"); + var property = FindPropertyInSnippet(snippetCodeGraph.Body, "minimumAttendeePercentage").Value; Assert.Equal(PropertyType.Number, property.PropertyType); Assert.Equal("100" , property.Value); @@ -249,7 +249,7 @@ public async Task ParsesBodyPropertyTypeBoolean() var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); var snippetCodeGraph = new SnippetCodeGraph(snippetModel); - var property = FindPropertyInSnippet(snippetCodeGraph.Body, "suggestLocation"); + var property = FindPropertyInSnippet(snippetCodeGraph.Body, "suggestLocation").Value; Assert.Equal(PropertyType.Boolean, property.PropertyType); Assert.Equal("False", property.Value); @@ -265,7 +265,7 @@ public async Task ParsesBodyPropertyTypeObject() var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); var snippetCodeGraph = new SnippetCodeGraph(snippetModel); - var property = FindPropertyInSnippet(snippetCodeGraph.Body, "locationConstraint"); + var property = FindPropertyInSnippet(snippetCodeGraph.Body, "locationConstraint").Value; Assert.Equal(PropertyType.Object, property.PropertyType); } @@ -280,7 +280,7 @@ public async Task ParsesBodyPropertyTypeArray() var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); var snippetCodeGraph = new SnippetCodeGraph(snippetModel); - var property = FindPropertyInSnippet(snippetCodeGraph.Body, "attendees"); + var property = FindPropertyInSnippet(snippetCodeGraph.Body, "attendees").Value; Assert.Equal(PropertyType.Array, property.PropertyType); } @@ -295,7 +295,7 @@ public async Task ParsesBodyPropertyTypeMap() var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); var snippetCodeGraph = new SnippetCodeGraph(snippetModel); - var property = FindPropertyInSnippet(snippetCodeGraph.Body, "additionalData"); + var property = FindPropertyInSnippet(snippetCodeGraph.Body, "additionalData").Value; Assert.Equal(PropertyType.Map, property.PropertyType); }