Skip to content

Commit

Permalink
feat: Sanitize Property Names (#16)
Browse files Browse the repository at this point in the history
* sanitize generated property names, may be overly strict

added new tests that pull an unofficial generated openapi document describing the Twitch API

* Update src/libs/OpenApiGenerator.Core/Models/PropertyData.cs

Co-authored-by: Konstantin S. <[email protected]>

* add UnsanitaryName init property helper to PropertyData for use in `PropertyData.Default with { ... }` patterns

* add opt-out word separation handling step to SanitizeName for use by the UnsanitaryName init helper

* unify code that handles of word separators in PropertyData to reduce maintenance

* add post-HandleWordSeparators empty string scenario

comment out inspection helper from OpenApiGenerator.IntegrationTests.Twitch.Data.csproj

accept changes for Properties (NOTE: EnumValues unsanitized)

---------

Co-authored-by: Konstantin S. <[email protected]>
  • Loading branch information
Tyler-IN and HavenDV authored May 27, 2024
1 parent 95c2372 commit 1ac62a8
Show file tree
Hide file tree
Showing 10 changed files with 232 additions and 17 deletions.
14 changes: 14 additions & 0 deletions OpenApiGenerator.sln
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApiGenerator.Cli", "src
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApiGenerator.IntegrationTests.Data", "src\tests\OpenApiGenerator.IntegrationTests.Data\OpenApiGenerator.IntegrationTests.Data.csproj", "{522FA7E8-2FF4-4381-83E9-ADB1851AC26E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApiGenerator.IntegrationTests.Twitch", "src\tests\OpenApiGenerator.IntegrationTests.Twitch\OpenApiGenerator.IntegrationTests.Twitch.csproj", "{CFB5BF63-EE61-43F8-B39B-7ECB51967210}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApiGenerator.IntegrationTests.Twitch.Data", "src\tests\OpenApiGenerator.IntegrationTests.Twitch.Data\OpenApiGenerator.IntegrationTests.Twitch.Data.csproj", "{1C0BC591-1413-4710-BF9F-DEC8E142DD16}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -74,6 +78,14 @@ Global
{522FA7E8-2FF4-4381-83E9-ADB1851AC26E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{522FA7E8-2FF4-4381-83E9-ADB1851AC26E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{522FA7E8-2FF4-4381-83E9-ADB1851AC26E}.Release|Any CPU.Build.0 = Release|Any CPU
{CFB5BF63-EE61-43F8-B39B-7ECB51967210}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CFB5BF63-EE61-43F8-B39B-7ECB51967210}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CFB5BF63-EE61-43F8-B39B-7ECB51967210}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CFB5BF63-EE61-43F8-B39B-7ECB51967210}.Release|Any CPU.Build.0 = Release|Any CPU
{1C0BC591-1413-4710-BF9F-DEC8E142DD16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1C0BC591-1413-4710-BF9F-DEC8E142DD16}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1C0BC591-1413-4710-BF9F-DEC8E142DD16}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1C0BC591-1413-4710-BF9F-DEC8E142DD16}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -87,6 +99,8 @@ Global
{88BE4AC5-6E7B-46FA-B656-5CCE8BF9F18E} = {9CAA231D-7BE1-46C9-ACD6-EB2E569CEBEA}
{CF8CA4E0-E90D-44FB-A9D5-6296A036BB24} = {6E5BF389-3D3F-4D74-9DD0-3B199CB529C5}
{522FA7E8-2FF4-4381-83E9-ADB1851AC26E} = {9CAA231D-7BE1-46C9-ACD6-EB2E569CEBEA}
{CFB5BF63-EE61-43F8-B39B-7ECB51967210} = {9CAA231D-7BE1-46C9-ACD6-EB2E569CEBEA}
{1C0BC591-1413-4710-BF9F-DEC8E142DD16} = {9CAA231D-7BE1-46C9-ACD6-EB2E569CEBEA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {1493AEE4-9211-46E9-BFE6-8F629EAC5693}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ public static PropertyData ToEnumValue(
return PropertyData.Default with
{
Id = id,
Name = name,
UnsanitaryName = name,
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/libs/OpenApiGenerator.Core/Generation/Data.Methods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public static ImmutableArray<EndPoint> PrepareData(
? [
.. includedTags.Select(x => PropertyData.Default with
{
Name = x.Name.ToClassName(),
UnsanitaryName = x.Name.ToClassName(),
Type = TypeData.Default with
{
CSharpType = $"{x.Name.ToClassName()}Client",
Expand Down
87 changes: 74 additions & 13 deletions src/libs/OpenApiGenerator.Core/Models/PropertyData.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Diagnostics;
using System.Text;
using Microsoft.OpenApi.Models;
using OpenApiGenerator.Core.Extensions;
using OpenApiGenerator.Core.Json;
Expand Down Expand Up @@ -44,23 +46,15 @@ public static PropertyData FromSchema(
parents = parents ?? throw new ArgumentNullException(nameof(parents));

var name = schema.Key.ToPropertyName();
name = name
.ReplacePlusAndMinusOnStart()
.UseWordSeparator('_', '+', '-', '/')
.Replace(".", "_")
.Replace(":", "_")
.Replace("[", string.Empty)
.Replace("]", string.Empty);

name = HandleWordSeparators(name);

if (name.Length > 0 &&
name[0] is not ('_' or >= 'A' and <= 'Z' or >= 'a' and <= 'z'))
{
name = $"_{name}";
}
if (parents.Length != 0)
{
name = name.FixPropertyName(parents.Last().ClassName);
}

name = SanitizeName(name, true);

return new PropertyData(
Id: schema.Key,
Expand All @@ -80,14 +74,81 @@ name[0] is not ('_' or >= 'A' and <= 'Z' or >= 'a' and <= 'z'))
}, parents).CSharpType),
Summary: schema.Value.GetSummary());
}


private static string SanitizeName(string? name, bool skipHandlingWordSeparators = false)
{
static bool InvalidFirstChar(char ch)
=> ch is not ('_' or >= 'A' and <= 'Z' or >= 'a' and <= 'z');

static bool InvalidSubsequentChar(char ch)
=> ch is not (
'_'
or >= 'A' and <= 'Z'
or >= 'a' and <= 'z'
or >= '0' and <= '9'
);

if (name is null || name.Length == 0)
{
return "";
}

if (!skipHandlingWordSeparators)
{
name = HandleWordSeparators(name);
}

if (name is null || name.Length == 0)
{
return "_";
}

if (InvalidFirstChar(name[0]))
{
name = $"_{name}";
}

if (!name.Skip(1).Any(InvalidSubsequentChar))
{
return name;
}

Span<char> buf = stackalloc char[name.Length];
name.AsSpan().CopyTo(buf);

for (var i = 1; i < buf.Length; i++)
{
if (InvalidSubsequentChar(buf[i]))
{
buf[i] = '_';
}
}

// Span<char>.ToString implementation checks for char type, new string(&buf[0], buf.length)
return buf.ToString();
}

private static string HandleWordSeparators(string name)
{
name = name
.ReplacePlusAndMinusOnStart()
.UseWordSeparator('_', '+', '-', '/')
.UseWordSeparator('(', '[', ']', ')');
return name;
}

public string ParameterName => Name
.Replace(".", string.Empty)
.ToParameterName()
.ReplaceIfEquals("ref", "@ref")
.ReplaceIfEquals("base", "@base")
.ReplaceIfEquals("protected", "@protected");

public string UnsanitaryName

Check warning on line 147 in src/libs/OpenApiGenerator.Core/Models/PropertyData.cs

View workflow job for this annotation

GitHub Actions / Build, test and publish / Build, test and publish

Because property UnsanitaryName is write-only, either add a property getter with an accessibility that is greater than or equal to its setter or convert this property into a method (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1044)

Check warning on line 147 in src/libs/OpenApiGenerator.Core/Models/PropertyData.cs

View workflow job for this annotation

GitHub Actions / Build, test and publish / Build, test and publish

Because property UnsanitaryName is write-only, either add a property getter with an accessibility that is greater than or equal to its setter or convert this property into a method (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1044)

Check warning on line 147 in src/libs/OpenApiGenerator.Core/Models/PropertyData.cs

View workflow job for this annotation

GitHub Actions / Build, test and publish / Build, test and publish

Because property UnsanitaryName is write-only, either add a property getter with an accessibility that is greater than or equal to its setter or convert this property into a method (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1044)

Check warning on line 147 in src/libs/OpenApiGenerator.Core/Models/PropertyData.cs

View workflow job for this annotation

GitHub Actions / Build, test and publish / Build, test and publish

Because property UnsanitaryName is write-only, either add a property getter with an accessibility that is greater than or equal to its setter or convert this property into a method (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1044)

Check warning on line 147 in src/libs/OpenApiGenerator.Core/Models/PropertyData.cs

View workflow job for this annotation

GitHub Actions / Build, test and publish / Build, test and publish

Because property UnsanitaryName is write-only, either add a property getter with an accessibility that is greater than or equal to its setter or convert this property into a method (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1044)

Check warning on line 147 in src/libs/OpenApiGenerator.Core/Models/PropertyData.cs

View workflow job for this annotation

GitHub Actions / Build, test and publish / Build, test and publish

Because property UnsanitaryName is write-only, either add a property getter with an accessibility that is greater than or equal to its setter or convert this property into a method (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1044)
{
init => Name = SanitizeName(value);
}

public string ArgumentName
{
get
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>

<Import Project="../../libs/OpenApiGenerator/OpenApiGenerator.props" />

<PropertyGroup>
<OpenApiGenerator_Namespace>OpenApiGenerator.IntegrationTests.Twitch</OpenApiGenerator_Namespace>
<OpenApiGenerator_GenerateSdk>false</OpenApiGenerator_GenerateSdk>
<OpenApiGenerator_GenerateModels>true</OpenApiGenerator_GenerateModels>
<OpenApiGenerator_GenerateSuperTypeForJsonSerializerContext>true</OpenApiGenerator_GenerateSuperTypeForJsonSerializerContext>
</PropertyGroup>

<ItemGroup>
<AdditionalFiles Include="Resources\openapi.json" />
</ItemGroup>

<!-- for inspection -->
<!--<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Generated\**\*" />
<None Include="Generated\**\*" />
</ItemGroup>-->


<Target Name="FetchTwitchOpenApiJson" BeforeTargets="Build;BeforeCompile;CompileDesignTime">
<!-- Download the OpenAPI JSON file (if expired) -->
<DownloadFile
SourceUrl="https://twitch-api-swagger.surge.sh/openapi.json"
DestinationFolder="$(ProjectDir)Resources"
DestinationFileName="openapi.json"
SkipUnchangedFiles="true"
Retries="2" />
</Target>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
<PackageReference Include="Microsoft.OpenApi" Version="1.6.14" />
<PackageReference Include="Microsoft.OpenApi.Readers" Version="1.6.14" />
<PackageReference Include="SharpYaml" Version="2.1.1" />
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\libs\OpenApiGenerator.Core\OpenApiGenerator.Core.csproj" OutputItemType="Analyzer" />
<ProjectReference Include="..\..\libs\OpenApiGenerator\OpenApiGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>

</Project>

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>

<Import Project="../../libs/OpenApiGenerator/OpenApiGenerator.props" />

<PropertyGroup>
<OpenApiGenerator_GenerateSdk>false</OpenApiGenerator_GenerateSdk>
<OpenApiGenerator_GenerateMethods>true</OpenApiGenerator_GenerateMethods>
<OpenApiGenerator_GenerateConstructors>true</OpenApiGenerator_GenerateConstructors>
<OpenApiGenerator_JsonSerializerContext>OpenApiGenerator.IntegrationTests.Twitch.SourceGenerationContext</OpenApiGenerator_JsonSerializerContext>
</PropertyGroup>

<ItemGroup Label="Base packages">
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.4.0" />
<PackageReference Include="MSTest.TestFramework" Version="3.4.0" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
</ItemGroup>

<ItemGroup Label="GlobalUsings">
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
<Using Include="FluentAssertions" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="H.Resources.Generator" Version="1.6.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
<PackageReference Include="Microsoft.OpenApi" Version="1.6.14" />
<PackageReference Include="Microsoft.OpenApi.Readers" Version="1.6.14" />
<PackageReference Include="SharpYaml" Version="2.1.1" />
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\libs\OpenApiGenerator.Core\OpenApiGenerator.Core.csproj" OutputItemType="Analyzer" />
<ProjectReference Include="..\..\libs\OpenApiGenerator\OpenApiGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\OpenApiGenerator.IntegrationTests.Data\OpenApiGenerator.IntegrationTests.Data.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using System.Text.Json.Serialization;

namespace OpenApiGenerator.IntegrationTests.Twitch;

[JsonSourceGenerationOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
[JsonSerializable(typeof(OpenApiGeneratorTrimmableSupport))]
internal sealed partial class SourceGenerationContext : JsonSerializerContext;
28 changes: 28 additions & 0 deletions src/tests/OpenApiGenerator.IntegrationTests.Twitch/Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Text.Json;

namespace OpenApiGenerator.IntegrationTests.Twitch;

[TestClass]
public class NSwagGeneratorTests
{
[TestMethod]
public void Models()
{
var json = JsonSerializer.Serialize(new Error
{
Error1 = new()
{
Title = "title",
Message = "message",
}
});

var error = JsonSerializer.Deserialize<Error>(json);
error.Should().NotBeNull();
error!.Error1.Should().BeEquivalentTo(new ErrorError
{
Title = "title",
Message = "message",
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6643,8 +6643,8 @@ Your input file must be formatted as a [JSONL file](/docs/api-reference/batch/re
IsArray: false,
IsEnum: true,
Properties: [
/v1/chat/completions,
/v1/embeddings
V1ChatCompletions,
V1Embeddings
],
EnumValues: [
/v1/chat/completions,
Expand Down

0 comments on commit 1ac62a8

Please sign in to comment.