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

Fix import wrong request builder and request body import paths #2019

Merged
merged 11 commits into from
Apr 19, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

namespace CodeSnippetsReflection.OpenAPI.Test;


public class PythonImportTests : OpenApiSnippetGeneratorTestBase
{
private readonly PythonGenerator _generator = new();
Expand Down Expand Up @@ -74,4 +75,43 @@ public async Task GenerateComplexModelImports(){
Assert.Contains("from msgraph.generated.models.extension import Extension", result);
Assert.Contains("from msgraph.generated.models.open_type_extension import OpenTypeExtension", result);
}

[Fact]
public async Task GenerateNestedRequestBuilderImports()
{
using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/applications(appId={{application-id}})?$select=id,appId,displayName,requiredResourceAccess");
var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata());
var result = _generator.GenerateCodeSnippet(snippetModel);
Assert.Contains("from msgraph import GraphServiceClient", result);
Assert.Contains("from msgraph.generated.applications_with_app_id.applications_with_app_id_request_builder import ApplicationsWithAppIdRequestBuilder", result);
}
[Fact]
public async Task GenerateRequestBodyImports()
{
string bodyContent = @"
{
""passwordCredential"": {
""displayName"": ""Test-Password friendly name""
}
}";

using var requestPayload = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/applications/{{application-id}}/addPassword"){
Content = new StringContent(bodyContent, Encoding.UTF8, "application/json")
};
var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata());
var result = _generator.GenerateCodeSnippet(snippetModel);
Assert.Contains("from msgraph import GraphServiceClient", result);
Assert.Contains("from msgraph.generated.models.password_credential import PasswordCredential", result);
Assert.Contains("from msgraph.generated.applications.item.add_password.add_password_post_request_body import AddPasswordPostRequestBody", result);
}
[Fact]
public async Task GeneratesImportsWithoutFilterAttrbutesInPath()
{
using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/servicePrincipals/$count");
requestPayload.Headers.Add("ConsistencyLevel", "eventual");
var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata());
var result = _generator.GenerateCodeSnippet(snippetModel);
Assert.Contains("from msgraph import GraphServiceClient", result);
Assert.Contains("from msgraph.generated.service_principals.count.count_request_builder import CountRequestBuilder", result);
}
}
11 changes: 11 additions & 0 deletions CodeSnippetsReflection.OpenAPI.Test/SnippetImportTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,15 @@ public async Task TestGenerateImportTemplatesForRequestBuilderImports()
Assert.NotNull(result[0].Path);
Assert.NotNull(result[0].RequestBuilderName);
}
[Fact]
public async Task TestGenerateImportTemplatesForNestedRequestBuilderImports()
{
using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/applications(appId={{application-id}})?$select=id,appId,displayName,requiredResourceAccess");
var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata());
var result = ImportsGenerator.GenerateImportTemplates(snippetModel);
Assert.NotNull(result);
Assert.IsType<List<ImportTemplate>>(result);
Assert.NotNull(result[0].Path);
Assert.NotNull(result[0].RequestBuilderName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using CodeSnippetsReflection.OpenAPI.ModelGraph;
using CodeSnippetsReflection.StringExtensions;
using Microsoft.OpenApi.Services;
using System.Text.RegularExpressions;


namespace CodeSnippetsReflection.OpenAPI.LanguageGenerators
{
Expand Down Expand Up @@ -73,7 +75,7 @@
snippetBuilder.Insert(0, string.Join(Environment.NewLine, importStatements));
return snippetBuilder.ToString();
}
private static HashSet<string> GetImportStatements(SnippetModel snippetModel)

Check warning on line 78 in CodeSnippetsReflection.OpenAPI/LanguageGenerators/PythonGenerator.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 29 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
{
const string modelImportPrefix = "from msgraph.generated.models";
const string requestBuilderImportPrefix = "from msgraph.generated";
Expand All @@ -89,10 +91,18 @@
{
case ImportKind.Model:
var typeDefinition = import.ModelProperty.TypeDefinition;
if (typeDefinition != null)
{
snippetImports.Add($"{modelImportPrefix}.{typeDefinition.ToSnakeCase()} import {typeDefinition}");
if (typeDefinition != null){
if(typeDefinition.EndsWith("RequestBody")){
shemogumbe marked this conversation as resolved.
Show resolved Hide resolved
var namespaceParts = import.ModelProperty.NamespaceName.Split('.').Select((s, i) => i == import.ModelProperty.NamespaceName.Split('.').Length - 1 ? s.ToSnakeCase() : s.ToLowerInvariant());
var importString = $"{requestBuilderImportPrefix}.{string.Join(".", namespaceParts)}.{typeDefinition.ToSnakeCase()} import {typeDefinition}";
snippetImports.Add($"{importString.Replace(".me.", ".users.item.")}");
}
else{
snippetImports.Add($"{modelImportPrefix}.{typeDefinition.ToSnakeCase()} import {typeDefinition}");
}
}


break;
case ImportKind.RequestBuilder:
if (!string.IsNullOrEmpty(import.ModelProperty.Name))
Expand All @@ -102,11 +112,13 @@
snippetImports.Add(importString.Replace(".me.", ".users.item."));
}
break;


case ImportKind.Path:
if (import.Path != null && import.RequestBuilderName != null)
{
//construct path to request builder
snippetImports.Add($"{requestBuilderImportPrefix}{import.Path.Replace(".me.", ".users.item.")}.{import.RequestBuilderName.ToSnakeCase()} import {import.RequestBuilderName}");
snippetImports.Add($"{requestBuilderImportPrefix}{Regex.Replace(import.Path.Replace(".me.", ".users.item."), @"(\B[A-Z])", "_$1", RegexOptions.Compiled, TimeSpan.FromSeconds(60)).ToLower()}.{import.RequestBuilderName.ToSnakeCase()} import {import.RequestBuilderName}");
}
break;
}
Expand Down Expand Up @@ -303,9 +315,9 @@
var enumValues = codeProperty.Value.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(x =>
{
var enumHint = x.Split('.').Last().Trim();

Check warning on line 318 in CodeSnippetsReflection.OpenAPI/LanguageGenerators/PythonGenerator.cs

View workflow job for this annotation

GitHub Actions / Build

Indexing at Count-1 should be used instead of the "Enumerable" extension method "Last" (https://rules.sonarsource.com/csharp/RSPEC-6608)
// the enum member may be invalid so default to generating the first value in case a look up fails.
var enumMember = codeProperty.Children.FirstOrDefault(member => member.Value.Equals(enumHint, StringComparison.OrdinalIgnoreCase)).Value ?? codeProperty.Children.FirstOrDefault().Value ?? enumHint;

Check warning on line 320 in CodeSnippetsReflection.OpenAPI/LanguageGenerators/PythonGenerator.cs

View workflow job for this annotation

GitHub Actions / Build

"Find" method should be used instead of the "FirstOrDefault" extension method. (https://rules.sonarsource.com/csharp/RSPEC-6602)
return $"{enumTypeString.TrimEnd('?')}.{enumMember.ToFirstCharacterUpperCase()}";
})
.Aggregate(static (x, y) => $"{x} | {y}");
Expand Down Expand Up @@ -350,7 +362,7 @@
case PropertyType.Map:
return "{";
case PropertyType.Enum:
return $"{ReplaceIfReservedTypeName(typeString.Split('.').First())}?";

Check warning on line 365 in CodeSnippetsReflection.OpenAPI/LanguageGenerators/PythonGenerator.cs

View workflow job for this annotation

GitHub Actions / Build

Indexing at 0 should be used instead of the "Enumerable" extension method "First" (https://rules.sonarsource.com/csharp/RSPEC-6608)
default:
return string.Empty;
}
Expand All @@ -358,7 +370,7 @@
private static string ReplaceIfReservedTypeName(string originalString, string suffix = "_")
=> ReservedTypeNames.Contains(originalString) ? $"{originalString}{suffix}" : originalString;

private static string GetFluentApiPath(IEnumerable<OpenApiUrlTreeNode> nodes, SnippetCodeGraph snippetCodeGraph, bool useIndexerNamespaces = false)

Check warning on line 373 in CodeSnippetsReflection.OpenAPI/LanguageGenerators/PythonGenerator.cs

View workflow job for this annotation

GitHub Actions / Build

Remove this unused method parameter 'useIndexerNamespaces'. (https://rules.sonarsource.com/csharp/RSPEC-1172)
{
if (!(nodes?.Any() ?? false))
return string.Empty;
Expand Down Expand Up @@ -404,7 +416,7 @@
})
.Aggregate(static (x, y) =>
{
var dot = y.StartsWith("[") ? string.Empty : ".";

Check warning on line 419 in CodeSnippetsReflection.OpenAPI/LanguageGenerators/PythonGenerator.cs

View workflow job for this annotation

GitHub Actions / Build

"StartsWith" overloads that take a "char" should be used (https://rules.sonarsource.com/csharp/RSPEC-6610)
return $"{x}{dot}{y}";
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
{
Path,
RequestBuilder,

RequestBody,
Model
}

Expand All @@ -34,6 +36,11 @@
{
get; set;
}
public string RequestBodyName
{
get; set;
}

public string HttpMethod
{
get; set;
Expand All @@ -42,28 +49,29 @@

public static class ImportsGenerator
{
public static List<ImportTemplate> imports = new();

Check warning on line 52 in CodeSnippetsReflection.OpenAPI/LanguageGenerators/SnippetImports.cs

View workflow job for this annotation

GitHub Actions / Build

Use an immutable collection or reduce the accessibility of the public static field 'imports'. (https://rules.sonarsource.com/csharp/RSPEC-2386)

Check warning on line 52 in CodeSnippetsReflection.OpenAPI/LanguageGenerators/SnippetImports.cs

View workflow job for this annotation

GitHub Actions / Build

Make this field 'private' and encapsulate it in a 'public' property. (https://rules.sonarsource.com/csharp/RSPEC-1104)

Check warning on line 52 in CodeSnippetsReflection.OpenAPI/LanguageGenerators/SnippetImports.cs

View workflow job for this annotation

GitHub Actions / Build

Change the visibility of 'imports' or make it 'const' or 'readonly'. (https://rules.sonarsource.com/csharp/RSPEC-2223)

public static List<ImportTemplate> GenerateImportTemplates(SnippetModel snippetModel)
{
var codeGraph = new SnippetCodeGraph(snippetModel);
var imports = new List<ImportTemplate>();

Check warning on line 57 in CodeSnippetsReflection.OpenAPI/LanguageGenerators/SnippetImports.cs

View workflow job for this annotation

GitHub Actions / Build

Rename 'imports' which hides the field with the same name. (https://rules.sonarsource.com/csharp/RSPEC-1117)
if (codeGraph.HasHeaders() || codeGraph.HasParameters() || codeGraph.HasOptions())
{
var className = codeGraph.Nodes.Last().GetClassName().ToFirstCharacterUpperCase();
var itemSuffix = codeGraph.Nodes.Last().Segment.IsCollectionIndex() ? "Item" : string.Empty;
var requestBuilderName = $"{className}{itemSuffix}RequestBuilder";
var requestBuilderName = $"{className}{itemSuffix}RequestBuilder";
if (codeGraph.Nodes.Last().Path != null)
{
var path = codeGraph.Nodes.Last().Path;
imports.Add(new ImportTemplate
{
Kind = ImportKind.Path,
Path = Regex.Replace(path.Replace("\\", ".").Replace("()", ""), @"\{[^}]*-id\}", "item", RegexOptions.Compiled, TimeSpan.FromSeconds(60)),
Path = Regex.Replace(path.Replace("\\", ".").Replace("()", ""), @"\{[^}]*-id\}", "item", RegexOptions.Compiled, TimeSpan.FromSeconds(60)).CleanUpImportPath().Replace("__", "_"),
RequestBuilderName = requestBuilderName,
HttpMethod = codeGraph.HttpMethod.ToString()
});
}

}
AddModelImportTemplates(codeGraph.Body, imports);
return imports;
Expand Down
23 changes: 23 additions & 0 deletions CodeSnippetsReflection/StringExtensions/StringExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Text;
using System.Linq;
using System;
using System.Text.RegularExpressions;


namespace CodeSnippetsReflection.StringExtensions
{
Expand Down Expand Up @@ -87,6 +89,27 @@ public static string ToSnakeCase(this string str)
return snakeCaseBuilder.ToString();
}

public static string CleanUpImportPath(this string input)
shemogumbe marked this conversation as resolved.
Show resolved Hide resolved
{
string pattern = @"(\w+)[A-Z]?(\w*)\((\w+Id)='(\{[^{}]+\})'\)";

string result = Regex.Replace(input, pattern, m =>
shemogumbe marked this conversation as resolved.
Show resolved Hide resolved
{
string firstPart = m.Groups[1].Value;
string secondPart = m.Groups[2].Value;
string idPart = m.Groups[3].Value;

// Given Id e.g appIdd, groupID - convert to snake case
secondPart = Regex.Replace(secondPart, @"(\B[A-Z])", x => "_" + x.Value.ToLower(), RegexOptions.Compiled, TimeSpan.FromSeconds(60));
idPart = Regex.Replace(idPart, @"(\B[A-Z])", x => "_" + x.Value.ToLower(), RegexOptions.Compiled, TimeSpan.FromSeconds(60));

return $"{firstPart}_{secondPart}_with_{idPart}";
}, RegexOptions.Compiled, TimeSpan.FromSeconds(60));
result = result.Replace("$", "");

return result;
}


public static string EscapeQuotes(this string stringValue)
{
Expand Down
Loading