Skip to content

Commit c170620

Browse files
authored
Merge pull request #55 from bonsai-rx/external-refs
Allow annotating schema type as external reference
2 parents d5bea4f + 2118b45 commit c170620

8 files changed

+235
-9
lines changed

Bonsai.Sgen.Tests/CompilerTestHelper.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ namespace Bonsai.Sgen.Tests
1010
{
1111
internal static class CompilerTestHelper
1212
{
13-
public static void CompileFromSource(string code)
13+
public static void CompileFromSource(params string[] code)
1414
{
1515
var options = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp5);
16-
var syntaxTree = CSharpSyntaxTree.ParseText(code, options);
16+
var syntaxTrees = Array.ConvertAll(code, text => CSharpSyntaxTree.ParseText(text, options));
1717
var serializerDependencies = new[]
1818
{
1919
typeof(YamlDotNet.Core.Parser).Assembly.Location,
@@ -26,7 +26,7 @@ public static void CompileFromSource(string code)
2626

2727
var compilation = CSharpCompilation.Create(
2828
nameof(CompilerTestHelper),
29-
syntaxTrees: new[] { syntaxTree },
29+
syntaxTrees: syntaxTrees,
3030
references: assemblyReferences,
3131
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
3232
using var memoryStream = new MemoryStream();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
using System.Threading.Tasks;
2+
using Microsoft.VisualStudio.TestTools.UnitTesting;
3+
using NJsonSchema;
4+
5+
namespace Bonsai.Sgen.Tests
6+
{
7+
[TestClass]
8+
public class ExternalTypeGenerationTests
9+
{
10+
private static async Task<JsonSchema> CreateCommonDefinitions()
11+
{
12+
return await JsonSchema.FromJsonAsync(@"
13+
{
14+
""$schema"": ""http://json-schema.org/draft-04/schema#"",
15+
""definitions"": {
16+
""CommonType"": {
17+
""type"": ""object"",
18+
""properties"": {
19+
""Bar"": {
20+
""type"": ""integer""
21+
}
22+
}
23+
}
24+
}
25+
}
26+
");
27+
}
28+
29+
[TestMethod]
30+
public void GenerateWithEmptyDefinitions_EmitEmptyCodeBlock()
31+
{
32+
var schema = new JsonSchema();
33+
var generator = TestHelper.CreateGenerator(schema);
34+
var code = generator.GenerateFile();
35+
CompilerTestHelper.CompileFromSource(code);
36+
}
37+
38+
[TestMethod]
39+
public async Task GenerateWithDefinitionsOnly_EmitDefinitionTypes()
40+
{
41+
var schema = await CreateCommonDefinitions();
42+
var generator = TestHelper.CreateGenerator(schema);
43+
var code = generator.GenerateFile();
44+
Assert.IsTrue(code.Contains("public partial class CommonType"), "Missing type definition.");
45+
CompilerTestHelper.CompileFromSource(code);
46+
}
47+
48+
[TestMethod]
49+
public async Task GenerateWithExternalTypeReferenceProperty_OmitExternalTypeDefinition()
50+
{
51+
var schemaA = await CreateCommonDefinitions();
52+
var generatorA = TestHelper.CreateGenerator(schemaA, schemaNamespace: $"{nameof(TestHelper)}.Base");
53+
var codeA = generatorA.GenerateFile();
54+
55+
var schemaB = await JsonSchema.FromJsonAsync(@"
56+
{
57+
""$schema"": ""http://json-schema.org/draft-04/schema#"",
58+
""definitions"": {
59+
""SpecificType"": {
60+
""type"": ""object"",
61+
""properties"": {
62+
""Bar"": {
63+
""$ref"": ""https://schemaA/definitions/CommonType""
64+
},
65+
""Baz"": {
66+
""type"": ""integer""
67+
}
68+
}
69+
}
70+
}
71+
}
72+
",
73+
documentPath: "",
74+
schema => new TestJsonReferenceResolver(
75+
new JsonSchemaAppender(schema, new DefaultTypeNameGenerator()),
76+
schemaA,
77+
generatorA.Settings.Namespace));
78+
79+
var generatorB = TestHelper.CreateGenerator(schemaB, schemaNamespace: $"{nameof(TestHelper)}.Derived");
80+
var codeB = generatorB.GenerateFile();
81+
Assert.IsTrue(codeB.Contains("public TestHelper.Base.CommonType Bar"), "Incorrect type declaration.");
82+
CompilerTestHelper.CompileFromSource(codeA, codeB);
83+
}
84+
85+
[TestMethod]
86+
public async Task GenerateWithExternalBaseTypeReference_OmitExternalTypeDefinition()
87+
{
88+
var schemaA = await CreateCommonDefinitions();
89+
var generatorA = TestHelper.CreateGenerator(schemaA, schemaNamespace: $"{nameof(TestHelper)}.Base");
90+
var codeA = generatorA.GenerateFile();
91+
92+
var schemaB = await JsonSchema.FromJsonAsync(@"
93+
{
94+
""$schema"": ""http://json-schema.org/draft-04/schema#"",
95+
""definitions"": {
96+
""DerivedType"": {
97+
""type"": ""object"",
98+
""properties"": {
99+
""Baz"": {
100+
""type"": [
101+
""null"",
102+
""integer""
103+
]
104+
}
105+
},
106+
""allOf"": [
107+
{
108+
""$ref"": ""https://schemaA/definitions/CommonType""
109+
}
110+
]
111+
}
112+
}
113+
}
114+
",
115+
documentPath: "",
116+
schema => new TestJsonReferenceResolver(
117+
new JsonSchemaAppender(schema, new DefaultTypeNameGenerator()),
118+
schemaA,
119+
generatorA.Settings.Namespace));
120+
121+
var generatorB = TestHelper.CreateGenerator(schemaB, schemaNamespace: $"{nameof(TestHelper)}.Derived");
122+
var codeB = generatorB.GenerateFile();
123+
Assert.IsTrue(codeB.Contains("class DerivedType : TestHelper.Base.CommonType"), "Incorrect type declaration.");
124+
CompilerTestHelper.CompileFromSource(codeA, codeB);
125+
}
126+
}
127+
}

Bonsai.Sgen.Tests/TestHelper.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ static class TestHelper
66
{
77
public static CSharpCodeDomGenerator CreateGenerator(
88
JsonSchema schema,
9-
SerializerLibraries serializerLibraries = SerializerLibraries.YamlDotNet | SerializerLibraries.NewtonsoftJson)
9+
SerializerLibraries serializerLibraries = SerializerLibraries.YamlDotNet | SerializerLibraries.NewtonsoftJson,
10+
string schemaNamespace = nameof(TestHelper))
1011
{
1112
var settings = new CSharpCodeDomGeneratorSettings
1213
{
13-
Namespace = nameof(TestHelper),
14+
Namespace = schemaNamespace,
1415
SerializerLibraries = serializerLibraries
1516
};
1617
schema = schema.WithCompatibleDefinitions(settings.TypeNameGenerator)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using NJsonSchema;
7+
using NJsonSchema.References;
8+
using NJsonSchema.Visitors;
9+
10+
namespace Bonsai.Sgen.Tests
11+
{
12+
internal class TestJsonReferenceResolver : JsonReferenceResolver
13+
{
14+
readonly IJsonReference _jsonReference;
15+
readonly IDictionary<string, JsonSchema> _definitions;
16+
17+
public TestJsonReferenceResolver(JsonSchemaAppender schemaAppender, JsonSchema schema, string ns)
18+
: base(schemaAppender)
19+
{
20+
_jsonReference = schema;
21+
_definitions = schema.Definitions;
22+
new ReferenceSchemaVisitor(ns).Visit(_jsonReference);
23+
}
24+
25+
private IJsonReference ResolveLocalReference(string path)
26+
{
27+
var typeName = Path.GetFileName(path);
28+
return _definitions[typeName];
29+
}
30+
31+
public override Task<IJsonReference> ResolveFileReferenceAsync(string filePath, CancellationToken cancellationToken = default)
32+
{
33+
return Task.FromResult(ResolveLocalReference(filePath));
34+
}
35+
36+
public override Task<IJsonReference> ResolveUrlReferenceAsync(string url, CancellationToken cancellationToken = default)
37+
{
38+
var referencePath = new Uri(url).LocalPath;
39+
return Task.FromResult(ResolveLocalReference(referencePath));
40+
}
41+
42+
class ReferenceSchemaVisitor : JsonSchemaVisitorBase
43+
{
44+
readonly string _namespace;
45+
46+
public ReferenceSchemaVisitor(string ns)
47+
{
48+
_namespace = ns;
49+
}
50+
51+
protected override JsonSchema VisitSchema(JsonSchema schema, string path, string typeNameHint)
52+
{
53+
if (!string.IsNullOrEmpty(typeNameHint))
54+
{
55+
schema.ExtensionData ??= new Dictionary<string, object>();
56+
schema.ExtensionData[JsonSchemaExtensions.TypeNameAnnotation] = $"{_namespace}.{typeNameHint}";
57+
}
58+
return schema;
59+
}
60+
}
61+
}
62+
}

Bonsai.Sgen/CSharpCodeDomGenerator.cs

+12-3
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ public CSharpCodeDomGenerator(object rootObject, CSharpCodeDomGeneratorSettings
3737

3838
protected override CodeArtifact GenerateType(JsonSchema schema, string typeNameHint)
3939
{
40+
if (schema.TryGetExternalTypeName(out var _))
41+
return new CodeArtifact(typeNameHint, default, default, default, string.Empty);
42+
4043
var typeName = _resolver.GetOrGenerateTypeName(schema, typeNameHint);
4144
if (schema.IsEnumeration)
4245
{
@@ -80,6 +83,9 @@ private CodeArtifact ReplaceInitOnlyProperties(CodeArtifact modelType)
8083

8184
public override IEnumerable<CodeArtifact> GenerateTypes()
8285
{
86+
if (RootObject is JsonSchema jsonSchema)
87+
_resolver.RegisterSchemaDefinitions(jsonSchema.Definitions);
88+
8389
var types = base.GenerateTypes();
8490
var extraTypes = new List<CodeArtifact>();
8591

@@ -111,14 +117,14 @@ public override IEnumerable<CodeArtifact> GenerateTypes()
111117
var matchTemplate = new CSharpTypeMatchTemplate(type, _provider, _options, Settings);
112118
extraTypes.Add(GenerateClass(matchTemplate));
113119
}
114-
if (Settings.SerializerLibraries.HasFlag(SerializerLibraries.NewtonsoftJson))
120+
if (Settings.SerializerLibraries.HasFlag(SerializerLibraries.NewtonsoftJson) && classTypes.Count > 0)
115121
{
116122
var serializer = new CSharpJsonSerializerTemplate(classTypes, _provider, _options, Settings);
117123
var deserializer = new CSharpJsonDeserializerTemplate(schema, classTypes, _provider, _options, Settings);
118124
extraTypes.Add(GenerateClass(serializer));
119125
extraTypes.Add(GenerateClass(deserializer));
120126
}
121-
if (Settings.SerializerLibraries.HasFlag(SerializerLibraries.YamlDotNet))
127+
if (Settings.SerializerLibraries.HasFlag(SerializerLibraries.YamlDotNet) && classTypes.Count > 0)
122128
{
123129
if (discriminatorTypes.Count > 0)
124130
{
@@ -133,7 +139,10 @@ public override IEnumerable<CodeArtifact> GenerateTypes()
133139
extraTypes.Add(GenerateClass(deserializer));
134140
}
135141

136-
return types.Select(ReplaceInitOnlyProperties).Concat(extraTypes);
142+
return types
143+
.Where(type => type.Type != CodeArtifactType.Undefined)
144+
.Select(ReplaceInitOnlyProperties)
145+
.Concat(extraTypes);
137146
}
138147
}
139148
}

Bonsai.Sgen/CSharpSerializerTemplate.cs

+5-1
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,14 @@ public override void BuildType(CodeTypeDeclaration type)
7575
new CodeTypeReference(modelType.TypeName))))));
7676
}
7777

78+
var defaultType = _schema.Title;
79+
if (string.IsNullOrEmpty(defaultType))
80+
defaultType = ModelTypes.FirstOrDefault()?.TypeName;
81+
7882
type.Members.Add(new CodeSnippetTypeMember(
7983
@$" public {TypeName}()
8084
{{
81-
Type = new Bonsai.Expressions.TypeMapping<{_schema.Title}>();
85+
Type = new Bonsai.Expressions.TypeMapping<{defaultType}>();
8286
}}
8387
8488
public Bonsai.Expressions.TypeMapping Type {{ get; set; }}

Bonsai.Sgen/CSharpTypeNameGenerator.cs

+8
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,13 @@ protected override string Generate(JsonSchema schema, string typeNameHint)
99
var defaultName = base.Generate(schema, typeNameHint);
1010
return CSharpNamingConvention.Instance.Apply(defaultName);
1111
}
12+
13+
public override string Generate(JsonSchema schema, string typeNameHint, IEnumerable<string> reservedTypeNames)
14+
{
15+
if (schema.TryGetExternalTypeName(out string typeName))
16+
return typeName;
17+
18+
return base.Generate(schema, typeNameHint, reservedTypeNames);
19+
}
1220
}
1321
}

Bonsai.Sgen/JsonSchemaExtensions.cs

+15
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,21 @@ namespace Bonsai.Sgen
99
{
1010
internal static class JsonSchemaExtensions
1111
{
12+
public const string TypeNameAnnotation = "x-sgen-typename";
13+
14+
public static bool TryGetExternalTypeName(this JsonSchema schema, out string typeName)
15+
{
16+
if (schema.ExtensionData?.TryGetValue(TypeNameAnnotation, out object? value) is true &&
17+
value is string annotationValue)
18+
{
19+
typeName = annotationValue;
20+
return true;
21+
}
22+
23+
typeName = string.Empty;
24+
return false;
25+
}
26+
1227
public static JsonSchema WithCompatibleDefinitions(this JsonSchema schema, ITypeNameGenerator typeNameGenerator)
1328
{
1429
var schemaAppender = new JsonSchemaAppender(schema, typeNameGenerator);

0 commit comments

Comments
 (0)