Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

- adds support for go snippets generation (#749) to master #785

Merged
merged 1 commit into from
Nov 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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();
}
}

}
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