Skip to content

Commit 02c3274

Browse files
authored
Merge pull request #25 from MobileTeleSystems/feature/aa_discriminator
AsyncApi. Fix discriminator generation
2 parents 5a88cd2 + a5240ee commit 02c3274

File tree

3 files changed

+270
-9
lines changed

3 files changed

+270
-9
lines changed

src/ApiCodeGenerator.AsyncApi/DOM/AsyncApiDocument.cs

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Newtonsoft.Json;
22
using Newtonsoft.Json.Linq;
33
using NJsonSchema;
4+
using NJsonSchema.CodeGeneration;
45
using NJsonSchema.Generation;
56
using NJsonSchema.Yaml;
67
using YamlDotNet.Serialization;
@@ -59,11 +60,13 @@ public static Task<AsyncApiDocument> FromJsonAsync(string data)
5960
/// <param name="data">JSON text.</param>
6061
/// <param name="documentPath"> Path to document. </param>
6162
/// <returns>AsyncApi document object model.</returns>
62-
public static Task<AsyncApiDocument> FromJsonAsync(string data, string? documentPath)
63+
public static async Task<AsyncApiDocument> FromJsonAsync(string data, string? documentPath)
6364
{
6465
var document = JsonConvert.DeserializeObject<AsyncApiDocument>(data, JSONSERIALIZERSETTINGS)!;
6566
document.DocumentPath = documentPath;
66-
return UpdateSchemaReferencesAsync(document);
67+
await UpdateSchemaReferencesAsync(document);
68+
BuildAsyncApiDescriminatorMapping(document);
69+
return document;
6770
}
6871

6972
/// <summary>
@@ -80,7 +83,7 @@ public static Task<AsyncApiDocument> FromYamlAsync(string data)
8083
/// <param name="data">YAML text.</param>
8184
/// <param name="documentPath"> Path to document. </param>
8285
/// <returns>AsyncApi document object model.</returns>
83-
public static Task<AsyncApiDocument> FromYamlAsync(string data, string? documentPath)
86+
public static async Task<AsyncApiDocument> FromYamlAsync(string data, string? documentPath)
8487
{
8588
var deserializer = new DeserializerBuilder().Build();
8689
using var reader = new StringReader(data);
@@ -90,15 +93,40 @@ public static Task<AsyncApiDocument> FromYamlAsync(string data, string? document
9093
var serializer = JsonSerializer.Create(JSONSERIALIZERSETTINGS);
9194
var doc = jObject.ToObject<AsyncApiDocument>(serializer)!;
9295
doc.DocumentPath = documentPath;
93-
return UpdateSchemaReferencesAsync(doc);
96+
await UpdateSchemaReferencesAsync(doc);
97+
BuildAsyncApiDescriminatorMapping(doc);
98+
return doc;
9499
}
95100

96-
private static async Task<AsyncApiDocument> UpdateSchemaReferencesAsync(AsyncApiDocument document)
101+
private static Task UpdateSchemaReferencesAsync(AsyncApiDocument document)
97102
{
98-
await JsonSchemaReferenceUtilities.UpdateSchemaReferencesAsync(
103+
return JsonSchemaReferenceUtilities.UpdateSchemaReferencesAsync(
99104
document,
100105
new JsonAndYamlReferenceResolver(new AsyncApiSchemaResolver(document, new SystemTextJsonSchemaGeneratorSettings())));
101-
return document;
106+
}
107+
108+
private static void BuildAsyncApiDescriminatorMapping(AsyncApiDocument document)
109+
{
110+
foreach (var schema in document.Components?.Schemas.Values ?? [])
111+
{
112+
var discriminatorPropName = schema.DiscriminatorObject?.PropertyName;
113+
if (discriminatorPropName != null)
114+
{
115+
var derivedSchemas = schema.GetDerivedSchemas(document);
116+
foreach (var item in derivedSchemas)
117+
{
118+
var derivedSchema = item.Key;
119+
if ((derivedSchema.Properties.TryGetValue(discriminatorPropName, out var discriminatorProp)
120+
|| derivedSchema.AllOf?.FirstOrDefault(i => i != schema && i.Properties.ContainsKey(discriminatorPropName))?.Properties.TryGetValue(discriminatorPropName, out discriminatorProp) == true)
121+
&& discriminatorProp.ExtensionData?.TryGetValue("const", out var constValue) == true)
122+
{
123+
var constValueStr = constValue!.ToString();
124+
discriminatorProp.ParentSchema!.Properties.Remove(discriminatorPropName);
125+
schema.DiscriminatorObject!.Mapping.Add(constValueStr, derivedSchema);
126+
}
127+
}
128+
}
129+
}
102130
}
103131
}
104132
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.

test/ApiCodeGenerator.AsyncApi.Tests/FunctionalTests.cs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,97 @@ public async Task GenerateMultipleClients()
169169
Assert.AreEqual(expected, actual);
170170
}
171171

172+
[Test]
173+
public async Task GenerateDiscriminator()
174+
{
175+
var yaml = """
176+
asyncapi: 2.0
177+
info: { title: 'dd', version: '1.0' }
178+
components:
179+
schemas:
180+
Pet:
181+
additionalProperties: false
182+
type: object
183+
discriminator: petType
184+
properties:
185+
name:
186+
type: string
187+
petType:
188+
type: string
189+
required:
190+
- name
191+
- petType
192+
Cat:
193+
allOf:
194+
- $ref: '#/components/schemas/Pet'
195+
- type: object
196+
properties:
197+
huntingSkill:
198+
type: string
199+
required:
200+
- huntingSkill
201+
additionalProperties: false
202+
StickInsect:
203+
allOf:
204+
- $ref: '#/components/schemas/Pet'
205+
- type: object
206+
properties:
207+
petType:
208+
const: StickBug
209+
color:
210+
type: string
211+
required:
212+
- color
213+
additionalProperties: false
214+
""";
215+
var settingsJson = $$"""
216+
{
217+
"Namespace": "TestNS",
218+
"GenerateDataAnnotations": false,
219+
"GenerateClientClasses": false
220+
}
221+
""";
222+
var generationContext = CreateContext(settingsJson, new StringReader(yaml));
223+
var generator = await CSharpClientContentGenerator.CreateAsync(generationContext);
224+
var actual = generator.Generate();
225+
226+
var expectedDto = $$"""
227+
[Newtonsoft.Json.JsonConverter(typeof(JsonInheritanceConverter), "petType")]
228+
[JsonInheritanceAttribute("StickBug", typeof(StickInsect))]
229+
[JsonInheritanceAttribute("Cat", typeof(Cat))]
230+
{{GENERATED_CODE}}
231+
public partial class Pet
232+
{
233+
[Newtonsoft.Json.JsonProperty("name", Required = Newtonsoft.Json.Required.Always)]
234+
public string Name { get; set; }
235+
236+
237+
}
238+
239+
{{GENERATED_CODE}}
240+
public partial class Cat : Pet
241+
{
242+
[Newtonsoft.Json.JsonProperty("huntingSkill", Required = Newtonsoft.Json.Required.Always)]
243+
public string HuntingSkill { get; set; }
244+
245+
246+
}
247+
248+
{{GENERATED_CODE}}
249+
public partial class StickInsect : Pet
250+
{
251+
[Newtonsoft.Json.JsonProperty("color", Required = Newtonsoft.Json.Required.Always)]
252+
public string Color { get; set; }
253+
254+
255+
}
256+
257+
{{JSON_INHERITANCE_CONVERTER}}
258+
""".Replace("\r", string.Empty);
259+
var expected = GetExpectedCode(null, expectedDto);
260+
Assert.AreEqual(expected, actual);
261+
}
262+
172263
[TestCaseSource(nameof(TemplateDirectorySource))]
173264
public void TemplateDirectory<T>(T settings)
174265
where T : CSharpGeneratorBaseSettings

test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/TestHelpers.cs

Lines changed: 144 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,146 @@ internal static partial class TestHelpers
1414
public static readonly string GENERATED_CODE = "[System.CodeDom.Compiler.GeneratedCode(\"NJsonSchema\", \"" + APICODEGEN_VERSION + "\")]";
1515
public static readonly string GENERATED_CODE_ATTRIBUTE = "[System.CodeDom.Compiler.GeneratedCode(\"ApiCodeGenerator.AsyncApi\", \"" + APICODEGEN_VERSION + "\")]";
1616

17+
public static readonly string JSON_INHERITANCE_CONVERTER = $$"""
18+
{{GENERATED_CODE}}
19+
[System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Interface, AllowMultiple = true)]
20+
internal class JsonInheritanceAttribute : System.Attribute
21+
{
22+
public JsonInheritanceAttribute(string key, System.Type type)
23+
{
24+
Key = key;
25+
Type = type;
26+
}
27+
28+
public string Key { get; }
29+
30+
public System.Type Type { get; }
31+
}
32+
33+
{{GENERATED_CODE}}
34+
public class JsonInheritanceConverter : Newtonsoft.Json.JsonConverter
35+
{
36+
internal static readonly string DefaultDiscriminatorName = "discriminator";
37+
38+
private readonly string _discriminatorName;
39+
40+
[System.ThreadStatic]
41+
private static bool _isReading;
42+
43+
[System.ThreadStatic]
44+
private static bool _isWriting;
45+
46+
public JsonInheritanceConverter()
47+
{
48+
_discriminatorName = DefaultDiscriminatorName;
49+
}
50+
51+
public JsonInheritanceConverter(string discriminatorName)
52+
{
53+
_discriminatorName = discriminatorName;
54+
}
55+
56+
public string DiscriminatorName { get { return _discriminatorName; } }
57+
58+
public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)
59+
{
60+
try
61+
{
62+
_isWriting = true;
63+
64+
var jObject = Newtonsoft.Json.Linq.JObject.FromObject(value, serializer);
65+
jObject.AddFirst(new Newtonsoft.Json.Linq.JProperty(_discriminatorName, GetSubtypeDiscriminator(value.GetType())));
66+
writer.WriteToken(jObject.CreateReader());
67+
}
68+
finally
69+
{
70+
_isWriting = false;
71+
}
72+
}
73+
74+
public override bool CanWrite
75+
{
76+
get
77+
{
78+
if (_isWriting)
79+
{
80+
_isWriting = false;
81+
return false;
82+
}
83+
return true;
84+
}
85+
}
86+
87+
public override bool CanRead
88+
{
89+
get
90+
{
91+
if (_isReading)
92+
{
93+
_isReading = false;
94+
return false;
95+
}
96+
return true;
97+
}
98+
}
99+
100+
public override bool CanConvert(System.Type objectType)
101+
{
102+
return true;
103+
}
104+
105+
public override object ReadJson(Newtonsoft.Json.JsonReader reader, System.Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer)
106+
{
107+
var jObject = serializer.Deserialize<Newtonsoft.Json.Linq.JObject>(reader);
108+
if (jObject == null)
109+
return null;
110+
111+
var discriminatorValue = jObject.GetValue(_discriminatorName);
112+
var discriminator = discriminatorValue != null ? Newtonsoft.Json.Linq.Extensions.Value<string>(discriminatorValue) : null;
113+
var subtype = GetObjectSubtype(objectType, discriminator);
114+
115+
var objectContract = serializer.ContractResolver.ResolveContract(subtype) as Newtonsoft.Json.Serialization.JsonObjectContract;
116+
if (objectContract == null || System.Linq.Enumerable.All(objectContract.Properties, p => p.PropertyName != _discriminatorName))
117+
{
118+
jObject.Remove(_discriminatorName);
119+
}
120+
121+
try
122+
{
123+
_isReading = true;
124+
return serializer.Deserialize(jObject.CreateReader(), subtype);
125+
}
126+
finally
127+
{
128+
_isReading = false;
129+
}
130+
}
131+
132+
private System.Type GetObjectSubtype(System.Type objectType, string discriminator)
133+
{
134+
foreach (var attribute in System.Reflection.CustomAttributeExtensions.GetCustomAttributes<JsonInheritanceAttribute>(System.Reflection.IntrospectionExtensions.GetTypeInfo(objectType), true))
135+
{
136+
if (attribute.Key == discriminator)
137+
return attribute.Type;
138+
}
139+
140+
return objectType;
141+
}
142+
143+
private string GetSubtypeDiscriminator(System.Type objectType)
144+
{
145+
foreach (var attribute in System.Reflection.CustomAttributeExtensions.GetCustomAttributes<JsonInheritanceAttribute>(System.Reflection.IntrospectionExtensions.GetTypeInfo(objectType), true))
146+
{
147+
if (attribute.Type == objectType)
148+
return attribute.Key;
149+
}
150+
151+
return objectType.Name;
152+
}
153+
}
154+
155+
""".Replace("\r", string.Empty);
156+
17157
public static string GetAsyncApiPath(string schemaFile) => Path.Combine("asyncApi", schemaFile);
18158

19159
public static async Task<TextReader> LoadApiDocumentAsync(string fileName)
@@ -41,10 +181,9 @@ public static async Task RunTest(CSharpClientGeneratorSettings settings, string
41181
Assert.That(actual, Is.EqualTo(expected));
42182
}
43183

44-
public static GeneratorContext CreateContext(string settingsJson, string schemaFile, Core.ExtensionManager.Extensions? extensions = null)
184+
public static GeneratorContext CreateContext(string settingsJson, TextReader docReader, Core.ExtensionManager.Extensions? extensions = null)
45185
{
46186
var jReader = new JsonTextReader(new StringReader(settingsJson));
47-
var docReader = File.OpenText(GetAsyncApiPath(schemaFile));
48187

49188
return new GeneratorContext(
50189
(t, s, v) => s!.Deserialize(jReader, t),
@@ -55,6 +194,9 @@ public static GeneratorContext CreateContext(string settingsJson, string schemaF
55194
};
56195
}
57196

197+
public static GeneratorContext CreateContext(string settingsJson, string schemaFile, Core.ExtensionManager.Extensions? extensions = null)
198+
=> CreateContext(settingsJson, File.OpenText(GetAsyncApiPath(schemaFile)), extensions);
199+
58200
public static string GetExpectedCode(string? expectedClientDeclartion, string? testOperResponseText, string @namespace = "TestNS", string? usings = null)
59201
{
60202
if (!string.IsNullOrWhiteSpace(expectedClientDeclartion) && !expectedClientDeclartion.Contains(GENERATED_CODE_ATTRIBUTE))

0 commit comments

Comments
 (0)