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

Allow annotating schema type as external reference #55

Merged
merged 7 commits into from
Mar 13, 2025
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
6 changes: 3 additions & 3 deletions Bonsai.Sgen.Tests/CompilerTestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ namespace Bonsai.Sgen.Tests
{
internal static class CompilerTestHelper
{
public static void CompileFromSource(string code)
public static void CompileFromSource(params string[] code)
{
var options = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp5);
var syntaxTree = CSharpSyntaxTree.ParseText(code, options);
var syntaxTrees = Array.ConvertAll(code, text => CSharpSyntaxTree.ParseText(text, options));
var serializerDependencies = new[]
{
typeof(YamlDotNet.Core.Parser).Assembly.Location,
Expand All @@ -26,7 +26,7 @@ public static void CompileFromSource(string code)

var compilation = CSharpCompilation.Create(
nameof(CompilerTestHelper),
syntaxTrees: new[] { syntaxTree },
syntaxTrees: syntaxTrees,
references: assemblyReferences,
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
using var memoryStream = new MemoryStream();
Expand Down
127 changes: 127 additions & 0 deletions Bonsai.Sgen.Tests/ExternalTypeGenerationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NJsonSchema;

namespace Bonsai.Sgen.Tests
{
[TestClass]
public class ExternalTypeGenerationTests
{
private static async Task<JsonSchema> CreateCommonDefinitions()
{
return await JsonSchema.FromJsonAsync(@"
{
""$schema"": ""http://json-schema.org/draft-04/schema#"",
""definitions"": {
""CommonType"": {
""type"": ""object"",
""properties"": {
""Bar"": {
""type"": ""integer""
}
}
}
}
}
");
}

[TestMethod]
public void GenerateWithEmptyDefinitions_EmitEmptyCodeBlock()
{
var schema = new JsonSchema();
var generator = TestHelper.CreateGenerator(schema);
var code = generator.GenerateFile();
CompilerTestHelper.CompileFromSource(code);
}

[TestMethod]
public async Task GenerateWithDefinitionsOnly_EmitDefinitionTypes()
{
var schema = await CreateCommonDefinitions();
var generator = TestHelper.CreateGenerator(schema);
var code = generator.GenerateFile();
Assert.IsTrue(code.Contains("public partial class CommonType"), "Missing type definition.");
CompilerTestHelper.CompileFromSource(code);
}

[TestMethod]
public async Task GenerateWithExternalTypeReferenceProperty_OmitExternalTypeDefinition()
{
var schemaA = await CreateCommonDefinitions();
var generatorA = TestHelper.CreateGenerator(schemaA, schemaNamespace: $"{nameof(TestHelper)}.Base");
var codeA = generatorA.GenerateFile();

var schemaB = await JsonSchema.FromJsonAsync(@"
{
""$schema"": ""http://json-schema.org/draft-04/schema#"",
""definitions"": {
""SpecificType"": {
""type"": ""object"",
""properties"": {
""Bar"": {
""$ref"": ""https://schemaA/definitions/CommonType""
},
""Baz"": {
""type"": ""integer""
}
}
}
}
}
",
documentPath: "",
schema => new TestJsonReferenceResolver(
new JsonSchemaAppender(schema, new DefaultTypeNameGenerator()),
schemaA,
generatorA.Settings.Namespace));

var generatorB = TestHelper.CreateGenerator(schemaB, schemaNamespace: $"{nameof(TestHelper)}.Derived");
var codeB = generatorB.GenerateFile();
Assert.IsTrue(codeB.Contains("public TestHelper.Base.CommonType Bar"), "Incorrect type declaration.");
CompilerTestHelper.CompileFromSource(codeA, codeB);
}

[TestMethod]
public async Task GenerateWithExternalBaseTypeReference_OmitExternalTypeDefinition()
{
var schemaA = await CreateCommonDefinitions();
var generatorA = TestHelper.CreateGenerator(schemaA, schemaNamespace: $"{nameof(TestHelper)}.Base");
var codeA = generatorA.GenerateFile();

var schemaB = await JsonSchema.FromJsonAsync(@"
{
""$schema"": ""http://json-schema.org/draft-04/schema#"",
""definitions"": {
""DerivedType"": {
""type"": ""object"",
""properties"": {
""Baz"": {
""type"": [
""null"",
""integer""
]
}
},
""allOf"": [
{
""$ref"": ""https://schemaA/definitions/CommonType""
}
]
}
}
}
",
documentPath: "",
schema => new TestJsonReferenceResolver(
new JsonSchemaAppender(schema, new DefaultTypeNameGenerator()),
schemaA,
generatorA.Settings.Namespace));

var generatorB = TestHelper.CreateGenerator(schemaB, schemaNamespace: $"{nameof(TestHelper)}.Derived");
var codeB = generatorB.GenerateFile();
Assert.IsTrue(codeB.Contains("class DerivedType : TestHelper.Base.CommonType"), "Incorrect type declaration.");
CompilerTestHelper.CompileFromSource(codeA, codeB);
}
}
}
5 changes: 3 additions & 2 deletions Bonsai.Sgen.Tests/TestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ static class TestHelper
{
public static CSharpCodeDomGenerator CreateGenerator(
JsonSchema schema,
SerializerLibraries serializerLibraries = SerializerLibraries.YamlDotNet | SerializerLibraries.NewtonsoftJson)
SerializerLibraries serializerLibraries = SerializerLibraries.YamlDotNet | SerializerLibraries.NewtonsoftJson,
string schemaNamespace = nameof(TestHelper))
{
var settings = new CSharpCodeDomGeneratorSettings
{
Namespace = nameof(TestHelper),
Namespace = schemaNamespace,
SerializerLibraries = serializerLibraries
};
schema = schema.WithCompatibleDefinitions(settings.TypeNameGenerator)
Expand Down
62 changes: 62 additions & 0 deletions Bonsai.Sgen.Tests/TestJsonReferenceResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using NJsonSchema;
using NJsonSchema.References;
using NJsonSchema.Visitors;

namespace Bonsai.Sgen.Tests
{
internal class TestJsonReferenceResolver : JsonReferenceResolver
{
readonly IJsonReference _jsonReference;
readonly IDictionary<string, JsonSchema> _definitions;

public TestJsonReferenceResolver(JsonSchemaAppender schemaAppender, JsonSchema schema, string ns)
: base(schemaAppender)
{
_jsonReference = schema;
_definitions = schema.Definitions;
new ReferenceSchemaVisitor(ns).Visit(_jsonReference);
}

private IJsonReference ResolveLocalReference(string path)
{
var typeName = Path.GetFileName(path);
return _definitions[typeName];
}

public override Task<IJsonReference> ResolveFileReferenceAsync(string filePath, CancellationToken cancellationToken = default)
{
return Task.FromResult(ResolveLocalReference(filePath));
}

public override Task<IJsonReference> ResolveUrlReferenceAsync(string url, CancellationToken cancellationToken = default)
{
var referencePath = new Uri(url).LocalPath;
return Task.FromResult(ResolveLocalReference(referencePath));
}

class ReferenceSchemaVisitor : JsonSchemaVisitorBase
{
readonly string _namespace;

public ReferenceSchemaVisitor(string ns)
{
_namespace = ns;
}

protected override JsonSchema VisitSchema(JsonSchema schema, string path, string typeNameHint)
{
if (!string.IsNullOrEmpty(typeNameHint))
{
schema.ExtensionData ??= new Dictionary<string, object>();
schema.ExtensionData[JsonSchemaExtensions.TypeNameAnnotation] = $"{_namespace}.{typeNameHint}";
}
return schema;
}
}
}
}
15 changes: 12 additions & 3 deletions Bonsai.Sgen/CSharpCodeDomGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ public CSharpCodeDomGenerator(object rootObject, CSharpCodeDomGeneratorSettings

protected override CodeArtifact GenerateType(JsonSchema schema, string typeNameHint)
{
if (schema.TryGetExternalTypeName(out var _))
return new CodeArtifact(typeNameHint, default, default, default, string.Empty);

var typeName = _resolver.GetOrGenerateTypeName(schema, typeNameHint);
if (schema.IsEnumeration)
{
Expand Down Expand Up @@ -80,6 +83,9 @@ private CodeArtifact ReplaceInitOnlyProperties(CodeArtifact modelType)

public override IEnumerable<CodeArtifact> GenerateTypes()
{
if (RootObject is JsonSchema jsonSchema)
_resolver.RegisterSchemaDefinitions(jsonSchema.Definitions);

var types = base.GenerateTypes();
var extraTypes = new List<CodeArtifact>();

Expand Down Expand Up @@ -111,14 +117,14 @@ public override IEnumerable<CodeArtifact> GenerateTypes()
var matchTemplate = new CSharpTypeMatchTemplate(type, _provider, _options, Settings);
extraTypes.Add(GenerateClass(matchTemplate));
}
if (Settings.SerializerLibraries.HasFlag(SerializerLibraries.NewtonsoftJson))
if (Settings.SerializerLibraries.HasFlag(SerializerLibraries.NewtonsoftJson) && classTypes.Count > 0)
{
var serializer = new CSharpJsonSerializerTemplate(classTypes, _provider, _options, Settings);
var deserializer = new CSharpJsonDeserializerTemplate(schema, classTypes, _provider, _options, Settings);
extraTypes.Add(GenerateClass(serializer));
extraTypes.Add(GenerateClass(deserializer));
}
if (Settings.SerializerLibraries.HasFlag(SerializerLibraries.YamlDotNet))
if (Settings.SerializerLibraries.HasFlag(SerializerLibraries.YamlDotNet) && classTypes.Count > 0)
{
if (discriminatorTypes.Count > 0)
{
Expand All @@ -133,7 +139,10 @@ public override IEnumerable<CodeArtifact> GenerateTypes()
extraTypes.Add(GenerateClass(deserializer));
}

return types.Select(ReplaceInitOnlyProperties).Concat(extraTypes);
return types
.Where(type => type.Type != CodeArtifactType.Undefined)
.Select(ReplaceInitOnlyProperties)
.Concat(extraTypes);
}
}
}
6 changes: 5 additions & 1 deletion Bonsai.Sgen/CSharpSerializerTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,14 @@ public override void BuildType(CodeTypeDeclaration type)
new CodeTypeReference(modelType.TypeName))))));
}

var defaultType = _schema.Title;
if (string.IsNullOrEmpty(defaultType))
defaultType = ModelTypes.FirstOrDefault()?.TypeName;

type.Members.Add(new CodeSnippetTypeMember(
@$" public {TypeName}()
{{
Type = new Bonsai.Expressions.TypeMapping<{_schema.Title}>();
Type = new Bonsai.Expressions.TypeMapping<{defaultType}>();
}}

public Bonsai.Expressions.TypeMapping Type {{ get; set; }}
Expand Down
8 changes: 8 additions & 0 deletions Bonsai.Sgen/CSharpTypeNameGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,13 @@ protected override string Generate(JsonSchema schema, string typeNameHint)
var defaultName = base.Generate(schema, typeNameHint);
return CSharpNamingConvention.Instance.Apply(defaultName);
}

public override string Generate(JsonSchema schema, string typeNameHint, IEnumerable<string> reservedTypeNames)
{
if (schema.TryGetExternalTypeName(out string typeName))
return typeName;

return base.Generate(schema, typeNameHint, reservedTypeNames);
}
}
}
15 changes: 15 additions & 0 deletions Bonsai.Sgen/JsonSchemaExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@ namespace Bonsai.Sgen
{
internal static class JsonSchemaExtensions
{
public const string TypeNameAnnotation = "x-sgen-typename";

public static bool TryGetExternalTypeName(this JsonSchema schema, out string typeName)
{
if (schema.ExtensionData?.TryGetValue(TypeNameAnnotation, out object? value) is true &&
value is string annotationValue)
{
typeName = annotationValue;
return true;
}

typeName = string.Empty;
return false;
}

public static JsonSchema WithCompatibleDefinitions(this JsonSchema schema, ITypeNameGenerator typeNameGenerator)
{
var schemaAppender = new JsonSchemaAppender(schema, typeNameGenerator);
Expand Down