Skip to content

Commit

Permalink
- adds support for go snippets generation (#749)
Browse files Browse the repository at this point in the history
* - draft go snippets generation

Signed-off-by: Vincent Biret <[email protected]>

* - additional unit tests fixes for go snippets generation

Signed-off-by: Vincent Biret <[email protected]>

* - fixes object generation for go snippets

Signed-off-by: Vincent Biret <[email protected]>

* - code linting

Signed-off-by: Vincent Biret <[email protected]>

* - fixes debugging experience for API

* - adds go to the list of languages in snipet generation description

* - fixes a bug where empty bodies would be added for go generation, code linting

* - fixes a bug where openapi snippet generation would fail for old odata index format

* - fixes a bug where missing content type could derail go snippet generation
- fixes a bug where empty collections could derail go snippets generation

Signed-off-by: Vincent Biret <[email protected]>

* - replicates json payload tolerance change to CSharp and TypeScript snippets generators

* - fixes a bug where go snippets generation would fail on terminal slash
- fixes a bug where go snippets generation would fail on path casing
- fixes a bug where go snippets generation would fail on odata filter functions

Signed-off-by: Vincent Biret <[email protected]>

* - adds go to the list of snippets to generate

* - adds a bypass for go snippets generation to use openapi

Signed-off-by: Vincent Biret <[email protected]>

* - fixes supported languages validation

Signed-off-by: Vincent Biret <[email protected]>

* Update CodeSnippetsReflection.OpenAPI/LanguageGenerators/GoGenerator.cs

Co-authored-by: Eastman <[email protected]>

* Update CodeSnippetsReflection.OpenAPI/LanguageGenerators/GoGenerator.cs

Co-authored-by: Eastman <[email protected]>

* - code linting: removes constants

Signed-off-by: Vincent Biret <[email protected]>

* consistent whitespacing

Co-authored-by: Eastman <[email protected]>
Co-authored-by: Andrew Omondi <[email protected]>
  • Loading branch information
3 people authored Nov 15, 2021
1 parent d216534 commit 92ca8f8
Show file tree
Hide file tree
Showing 18 changed files with 765 additions and 57 deletions.
20 changes: 20 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,26 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": ".NET Core Launch (web)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/GraphWebApi/bin/Debug/net5.0/GraphWebApi.dll",
"args": [],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
},
{
"name": ".NET Core Launch (console)",
"type": "coreclr",
Expand Down
1 change: 1 addition & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"type": "shell",
"args": [
"build",
"${workspaceFolder}/MSGraphWebApi.sln",
// Ask dotnet build to generate full paths for file names.
"/property:GenerateFullPaths=true",
// Do not generate summary otherwise it leads to duplicate errors in Problems panel
Expand Down
10 changes: 8 additions & 2 deletions CodeSnippetsReflection.App/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ static void Main(string[] args)
// splits language list into supported and unsupported languages
// where key "true" holds supported and key "false" holds unsupported languages
var languageGroups = languages
.GroupBy(l => ODataSnippetsGenerator.SupportedLanguages.Contains(l.ToLowerInvariant()))
.GroupBy(l => ODataSnippetsGenerator.SupportedLanguages.Contains(l) || OpenApiSnippetsGenerator.SupportedLanguages.Contains(l))
.ToDictionary(g => g.Key, g => g.ToList());

var supportedLanguages = languageGroups.ContainsKey(true) ? languageGroups[true] : null;
Expand All @@ -85,13 +85,19 @@ static void Main(string[] args)
if(string.IsNullOrEmpty(generation))
generation = "odata";

var generator = GetSnippetsGenerator(generation, customMetadataPathArg);
var files = Directory.EnumerateFiles(httpSnippetsDir, "*-httpSnippet");

Console.WriteLine($"Running snippet generation for these languages: {string.Join(" ", supportedLanguages)}");

var originalGeneration = generation;

Parallel.ForEach(supportedLanguages, language =>
{
if(language.Equals("go", StringComparison.OrdinalIgnoreCase))
generation = "openapi";
else
generation = originalGeneration;
var generator = GetSnippetsGenerator(generation, customMetadataPathArg);
Parallel.ForEach(files, file =>
{
ProcessFile(generator, language, file);
Expand Down
2 changes: 1 addition & 1 deletion CodeSnippetsReflection.OData/ODataSnippetsGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public class ODataSnippetsGenerator : IODataSnippetsGenerator
private CSharpExpressions CSharpExpressions { get; }
private ObjectiveCExpressions ObjectiveCExpressions { get; }
private JavaExpressions JavaExpressions { get; }
public static HashSet<string> SupportedLanguages { get; set; } = new()
public static HashSet<string> SupportedLanguages { get; set; } = new(StringComparer.OrdinalIgnoreCase)
{
"c#",
"javascript",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ public async Task GeneratesABinaryPayload() {
using var requestPayload = 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 })
};
requestPayload.Content.Headers.ContentType = new ("application/octect-stream");
requestPayload.Content.Headers.ContentType = new ("application/octet-stream");
var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode());
var result = _generator.GenerateCodeSnippet(snippetModel);
Assert.Contains("new MemoryStream", result);
Expand Down
274 changes: 274 additions & 0 deletions CodeSnippetsReflection.OpenAPI.Test/GoGeneratorTests.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ public async Task GeneratesABinaryPayload()
{
Content = new ByteArrayContent(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10 })
};
requestPayload.Content.Headers.ContentType = new("application/octect-stream");
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);
Expand Down
23 changes: 23 additions & 0 deletions CodeSnippetsReflection.OpenAPI/KiotaOpenApiOperationExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// THIS CLASS IS COPIED FROM KIOTA TO GET THE SAME NAMING CONVENTIONS, WE SHOULD FIND A WAY TO MUTUALIZE THE CODE
using System.Collections.Generic;
using System.Linq;
using Microsoft.OpenApi.Models;

namespace CodeSnippetsReflection.OpenAPI {
public static class OpenApiOperationExtensions {
private static readonly HashSet<string> successCodes = new() {"200", "201", "202"}; //204 excluded as it won't have a schema
public static OpenApiSchema GetResponseSchema(this OpenApiOperation operation)
{
// Return Schema that represents all the possible success responses!
// For the moment assume 200s and application/json
var schemas = operation.Responses.Where(r => successCodes.Contains(r.Key))
.SelectMany(re => re.Value.Content)
.Where(c => c.Key == "application/json")
.Select(co => co.Value.Schema)
.Where(s => s is not null);

return schemas.FirstOrDefault();
}
}

}
13 changes: 13 additions & 0 deletions CodeSnippetsReflection.OpenAPI/KiotaOpenApiReferenceExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// THIS CLASS IS COPIED FROM KIOTA TO GET THE SAME NAMING CONVENTIONS, WE SHOULD FIND A WAY TO MUTUALIZE THE CODE
using CodeSnippetsReflection.StringExtensions;
using Microsoft.OpenApi.Models;

namespace CodeSnippetsReflection.OpenAPI {
public static class OpenApiReferenceExtensions {
public static string GetClassName(this OpenApiReference reference) {
var referenceId = reference?.Id;
return referenceId?[((referenceId?.LastIndexOf('.') ?? 0) + 1)..]
?.ToFirstCharacterUpperCase();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// ------------------------------------------------------------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
// ------------------------------------------------------------------------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using CodeSnippetsReflection.StringExtensions;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Services;

// THIS CLASS IS COPIED FROM KIOTA TO GET THE SAME NAMING CONVENTIONS, WE SHOULD FIND A WAY TO MUTUALIZE THE CODE
namespace CodeSnippetsReflection.OpenAPI {

public static class KiotaOpenApiUrlTreeNodeExtensions {
private static readonly Regex PathParametersRegex = new(@"(?:\w+)?=?'?\{(?<paramName>\w+)\}'?,?", RegexOptions.Compiled);
private static readonly char requestParametersChar = '{';
private static readonly char requestParametersEndChar = '}';
private static readonly char requestParametersSectionChar = '(';
private static readonly char requestParametersSectionEndChar = ')';
private static readonly MatchEvaluator requestParametersMatchEvaluator = (match) => {
return "With" + match.Groups["paramName"].Value.ToFirstCharacterUpperCase();
};
private static readonly Regex idClassNameCleanup = new(@"Id\d?$", RegexOptions.Compiled);
///<summary>
/// Returns the class name for the node with more or less precision depending on the provided arguments
///</summary>
public static string GetClassName(this OpenApiUrlTreeNode currentNode, string suffix = default, string prefix = default, OpenApiOperation operation = default) {
var rawClassName = (operation?.GetResponseSchema()?.Reference?.GetClassName() ??
CleanupParametersFromPath(currentNode.Segment)?.ReplaceValueIdentifier())
.TrimEnd(requestParametersEndChar)
.TrimStart(requestParametersChar)
.TrimStart('$') //$ref from OData
.Split('-')
.First();
if((currentNode?.DoesNodeBelongToItemSubnamespace() ?? false) && idClassNameCleanup.IsMatch(rawClassName))
rawClassName = idClassNameCleanup.Replace(rawClassName, string.Empty);
return prefix + rawClassName?.Split('.', StringSplitOptions.RemoveEmptyEntries)?.LastOrDefault() + suffix;
}
public static bool DoesNodeBelongToItemSubnamespace(this OpenApiUrlTreeNode currentNode) => currentNode.IsPathSegmentWithSingleSimpleParameter();
public static bool IsPathSegmentWithSingleSimpleParameter(this OpenApiUrlTreeNode currentNode) =>
currentNode?.Segment.IsPathSegmentWithSingleSimpleParameter() ?? false;
private static bool IsPathSegmentWithSingleSimpleParameter(this string currentSegment)
{
return (currentSegment?.StartsWith(requestParametersChar) ?? false) &&
currentSegment.EndsWith(requestParametersEndChar) &&
currentSegment.Count(x => x == requestParametersChar) == 1;
}
private static string CleanupParametersFromPath(string pathSegment) {
if((pathSegment?.Contains(requestParametersChar) ?? false) ||
(pathSegment?.Contains(requestParametersSectionChar) ?? false))
return PathParametersRegex.Replace(pathSegment, requestParametersMatchEvaluator)
.TrimEnd(requestParametersSectionEndChar)
.Replace(requestParametersSectionChar.ToString(), string.Empty);
return pathSegment;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ public string GenerateCodeSnippet(SnippetModel snippetModel)
if(!string.IsNullOrEmpty(requestHeadersPayload))
snippetBuilder.Append(requestHeadersPayload);
var parametersList = GetActionParametersList(payloadVarName, queryParamsVarName, requestHeadersVarName);
snippetBuilder.AppendLine($"{responseAssignment}await {clientVarName}.{GetFluentApiPath(snippetModel.PathNodes)}.{GetMethodName(snippetModel.Method)}({parametersList});");
var methodName = snippetModel.Method.ToString().ToLower().ToFirstCharacterUpperCase() + "Async";
snippetBuilder.AppendLine($"{responseAssignment}await {clientVarName}.{GetFluentApiPath(snippetModel.PathNodes)}.{methodName}({parametersList});");
return snippetBuilder.ToString();
}
private const string requestHeadersVarName = "headers";
Expand Down Expand Up @@ -105,7 +106,7 @@ private static string GetQueryParameterValue(string originalValue, Dictionary<st
}
}
private static string NormalizeQueryParameterName(string queryParam) => queryParam.TrimStart('$').ToFirstCharacterUpperCase();
private const string requestBodyVarName = "requestBody";
private const string RequestBodyVarName = "requestBody";
private static (string, string) GetRequestPayloadAndVariableName(SnippetModel snippetModel, IndentManager indentManager) {
if(string.IsNullOrWhiteSpace(snippetModel?.RequestBody))
return (default, default);
Expand All @@ -114,25 +115,37 @@ private static (string, string) GetRequestPayloadAndVariableName(SnippetModel sn
var payloadSB = new StringBuilder();
switch (snippetModel.ContentType.Split(';').First().ToLowerInvariant()) {
case "application/json":
if(!string.IsNullOrEmpty(snippetModel.RequestBody) &&
!"undefined".Equals(snippetModel.RequestBody, StringComparison.OrdinalIgnoreCase)) // graph explorer sends "undefined" as request body for some reason
using (var parsedBody = JsonDocument.Parse(snippetModel.RequestBody)) {
var schema = snippetModel.RequestSchema;
var className = schema.GetSchemaTitle().ToFirstCharacterUpperCase();
payloadSB.AppendLine($"var {requestBodyVarName} = new {className}");
payloadSB.AppendLine($"{indentManager.GetIndent()}{{");
WriteJsonObjectValue(payloadSB, parsedBody.RootElement, schema, indentManager);
payloadSB.AppendLine("};");
}
TryParseBody(snippetModel, payloadSB, indentManager);
break;
case "application/octect-stream":
payloadSB.AppendLine($"using var {requestBodyVarName} = new MemoryStream(); //stream to upload");
case "application/octet-stream":
payloadSB.AppendLine($"using var {RequestBodyVarName} = new MemoryStream(); //stream to upload");
break;
default:
throw new InvalidOperationException($"Unsupported content type: {snippetModel.ContentType}");
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}");
}
return (payloadSB.ToString(), requestBodyVarName);
var result = payloadSB.ToString();
return (result, string.IsNullOrEmpty(result) ? string.Empty : RequestBodyVarName);
}
private static bool TryParseBody(SnippetModel snippetModel, StringBuilder payloadSB, IndentManager indentManager) {
if(!string.IsNullOrEmpty(snippetModel.RequestBody) &&
!"undefined".Equals(snippetModel.RequestBody, StringComparison.OrdinalIgnoreCase)) // graph explorer sends "undefined" as request body for some reason
try {
using var parsedBody = JsonDocument.Parse(snippetModel.RequestBody);
var schema = snippetModel.RequestSchema;
var className = schema.GetSchemaTitle().ToFirstCharacterUpperCase();
payloadSB.AppendLine($"var {RequestBodyVarName} = new {className}");
payloadSB.AppendLine($"{indentManager.GetIndent()}{{");
WriteJsonObjectValue(payloadSB, parsedBody.RootElement, schema, indentManager);
payloadSB.AppendLine("};");

} catch (Exception ex) when (ex is JsonException || ex is ArgumentException) {
// the payload wasn't json or poorly formatted
}
return false;
}
private static void WriteJsonObjectValue(StringBuilder payloadSB, JsonElement value, OpenApiSchema schema, IndentManager indentManager, bool includePropertyAssignment = true) {
if (value.ValueKind != JsonValueKind.Object) throw new InvalidOperationException($"Expected JSON object and got {value.ValueKind}");
indentManager.Indent();
Expand Down Expand Up @@ -227,17 +240,5 @@ private static string GetFluentApiPath(IEnumerable<OpenApiUrlTreeNode> nodes) {
return $"{x}{dot}{y}";
});
}
private static string GetMethodName(HttpMethod method) {
// can't use pattern matching with switch as it's not an enum but a bunch of static values
if(method == HttpMethod.Get) return "GetAsync";
else if(method == HttpMethod.Post) return "PostAsync";
else if(method == HttpMethod.Put) return "PutAsync";
else if(method == HttpMethod.Delete) return "DeleteAsync";
else if(method == HttpMethod.Patch) return "PatchAsync";
else if(method == HttpMethod.Head) return "HeadAsync";
else if(method == HttpMethod.Options) return "OptionsAsync";
else if(method == HttpMethod.Trace) return "TraceAsync";
else throw new InvalidOperationException($"Unsupported HTTP method: {method}");
}
}
}
Loading

0 comments on commit 92ca8f8

Please sign in to comment.