Skip to content

Commit 9b9054e

Browse files
matherm-aboehmunchase
authored andcommitted
chore: Add sample implementation for processing OneOf inheritance as a NSwag OperationProcessor
1 parent 9f76695 commit 9b9054e

2 files changed

Lines changed: 335 additions & 0 deletions

File tree

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
using System.Linq;
5+
using System.Net.Mime;
6+
using System.Reflection;
7+
using System.Runtime.Serialization;
8+
using Microsoft.AspNetCore.Mvc;
9+
using Namotion.Reflection;
10+
using Newtonsoft.Json;
11+
using NJsonSchema;
12+
using NJsonSchema.Converters;
13+
using NSwag;
14+
using NSwag.Generation.Processors;
15+
using NSwag.Generation.Processors.Contexts;
16+
17+
namespace Unchase.OpenAPI.ConnectedService.CodeGeneration
18+
{
19+
20+
/// <summary>
21+
/// Decorates the Open API specification document with known inherited class types using "OneOf" and "AnyOf".
22+
/// Source from: https://gist.github.com/icnocop/10de946939e5046190219cc8817356c4
23+
/// Mentioned here: https://github.com/RicoSuter/NJsonSchema/pull/839
24+
/// </summary>
25+
/// <seealso cref="NSwag.Generation.Processors.IOperationProcessor" />
26+
public class OneOfOperationProcessor : IOperationProcessor
27+
{
28+
private const string mediaTypeName = "application/json"; // MediaTypeNames.Application.Json; only available with .Net Core 2.1+
29+
30+
/// <inheritdoc/>
31+
public bool Process(OperationProcessorContext context)
32+
{
33+
this.SetRequests(context);
34+
35+
this.SetResponses(context);
36+
37+
return true;
38+
}
39+
40+
private static JsonSchema GetSchemaForType(
41+
OperationProcessorContext context,
42+
Type type)
43+
{
44+
if (!context.SchemaResolver.HasSchema(type, false))
45+
{
46+
return null;
47+
}
48+
49+
JsonSchema schema = context.SchemaResolver.GetSchema(type, false);
50+
51+
return new JsonSchema
52+
{
53+
Reference = schema,
54+
};
55+
}
56+
57+
private void SetRequests(OperationProcessorContext context)
58+
{
59+
if (context.OperationDescription.Operation.RequestBody == null)
60+
{
61+
return;
62+
}
63+
64+
if (!context.OperationDescription.Operation.RequestBody.Content.ContainsKey(mediaTypeName))
65+
{
66+
return;
67+
}
68+
69+
var mediaType = context.OperationDescription.Operation.RequestBody.Content[mediaTypeName];
70+
var apiParameter = context.OperationDescription.Operation.Parameters.Single(x => x.Kind == OpenApiParameterKind.Body);
71+
72+
var parameter = context.Parameters.SingleOrDefault(x => x.Value.Name == apiParameter.Name);
73+
if (parameter.Equals(default(KeyValuePair<ParameterInfo, OpenApiParameter>)))
74+
{
75+
return;
76+
}
77+
78+
var parameterType = parameter.Key.ParameterType;
79+
80+
var newSchema = this.GenerateSchemaWithInheritanceForType(context, parameterType, false);
81+
if (newSchema != null)
82+
{
83+
mediaType.Schema = newSchema;
84+
}
85+
}
86+
87+
private void SetResponses(OperationProcessorContext context)
88+
{
89+
var attributes = context.MethodInfo.GetCustomAttributes<ProducesResponseTypeAttribute>(true);
90+
foreach (var apiResponse in context.OperationDescription.Operation.Responses)
91+
{
92+
if (!apiResponse.Value.Content.ContainsKey(mediaTypeName))
93+
{
94+
continue;
95+
}
96+
97+
var mediaType = apiResponse.Value.Content[mediaTypeName];
98+
99+
if (!int.TryParse(apiResponse.Key, out int responseStatusCode))
100+
{
101+
continue;
102+
}
103+
104+
var attribute = attributes.SingleOrDefault(x => x.StatusCode == responseStatusCode);
105+
if (attribute == null)
106+
{
107+
continue;
108+
}
109+
110+
var responseType = attribute.Type;
111+
112+
var newSchema = this.GenerateSchemaWithInheritanceForType(context, responseType, false);
113+
if (newSchema != null)
114+
{
115+
mediaType.Schema = newSchema;
116+
}
117+
}
118+
}
119+
120+
private JsonSchema GenerateSchemaWithInheritanceForType(
121+
OperationProcessorContext context,
122+
Type type,
123+
bool includeBaseReference = true)
124+
{
125+
if (type.IsGenericType)
126+
{
127+
Type[] genericArguments = type.GetGenericArguments();
128+
129+
if (genericArguments.Length == 1)
130+
{
131+
Type genericArgumentType = genericArguments[0];
132+
133+
Type enumerableType = typeof(IEnumerable<>).MakeGenericType(genericArguments);
134+
if (enumerableType.IsAssignableFrom(type))
135+
{
136+
return new JsonSchema
137+
{
138+
Type = JsonObjectType.Array,
139+
Item = this.GenerateSchemaWithInheritanceForType(
140+
context,
141+
genericArgumentType,
142+
false),
143+
IsAbstract = true,
144+
};
145+
}
146+
else
147+
{
148+
return null;
149+
}
150+
}
151+
}
152+
153+
var knownTypeAttributes = type.GetCustomAttributes<KnownTypeAttribute>(true);
154+
if (!knownTypeAttributes.Any())
155+
{
156+
return null;
157+
}
158+
159+
JsonSchema baseTypeSchema;
160+
161+
if (includeBaseReference)
162+
{
163+
baseTypeSchema = GetSchemaForType(context, type);
164+
if (baseTypeSchema == null)
165+
{
166+
return null;
167+
}
168+
169+
if (baseTypeSchema.OneOf.Any())
170+
{
171+
return baseTypeSchema;
172+
}
173+
174+
baseTypeSchema.Title = null;
175+
baseTypeSchema.Type = JsonObjectType.None;
176+
}
177+
else
178+
{
179+
baseTypeSchema = new JsonSchema();
180+
}
181+
182+
var discriminatorConverter = this.TryGetInheritanceDiscriminatorConverter(type);
183+
var discriminatorName = this.TryGetInheritanceDiscriminatorName(discriminatorConverter);
184+
185+
JsonSchema typeSchema = GetSchemaForType(context, type);
186+
if (typeSchema == null)
187+
{
188+
return null;
189+
}
190+
191+
this.GenerateInheritanceDiscriminator(
192+
baseTypeSchema,
193+
discriminatorConverter,
194+
discriminatorName,
195+
type,
196+
typeSchema);
197+
198+
baseTypeSchema.OneOf.Add(new JsonSchema
199+
{
200+
Reference = typeSchema,
201+
});
202+
203+
foreach (var attribute in knownTypeAttributes)
204+
{
205+
var knownTypeSchema = GetSchemaForType(context, attribute.Type);
206+
if (knownTypeSchema == null)
207+
{
208+
continue;
209+
}
210+
211+
baseTypeSchema.OneOf.Add(new JsonSchema
212+
{
213+
Reference = knownTypeSchema,
214+
});
215+
216+
// apply to properties
217+
foreach (PropertyInfo propertyInfo in attribute.Type.GetProperties())
218+
{
219+
var propertyType = propertyInfo.PropertyType;
220+
if (!context.SchemaResolver.HasSchema(propertyType, false))
221+
{
222+
continue;
223+
}
224+
225+
if (context.Document.Components.Schemas.ContainsKey(propertyType.Name))
226+
{
227+
JsonSchema propertyTypeSchema = context.SchemaResolver.GetSchema(propertyType, false);
228+
if ((propertyTypeSchema == null)
229+
|| propertyTypeSchema.AnyOf.Any())
230+
{
231+
continue;
232+
}
233+
234+
var newSchema = this.GenerateSchemaWithInheritanceForType(context, propertyType, false);
235+
if (newSchema != null)
236+
{
237+
var propertyTypeName = propertyType.Name;
238+
foreach (var schema in newSchema.OneOf.Skip(1))
239+
{
240+
context.Document.Components.Schemas[propertyTypeName].AnyOf.Add(
241+
new JsonSchema
242+
{
243+
Reference = schema,
244+
});
245+
}
246+
}
247+
}
248+
}
249+
}
250+
251+
return baseTypeSchema;
252+
}
253+
254+
private void GenerateInheritanceDiscriminator(
255+
JsonSchema baseSchema,
256+
object discriminatorConverter,
257+
string discriminatorName,
258+
Type knownType,
259+
JsonSchema knownTypeSchema)
260+
{
261+
this.AddDiscriminatorObject(baseSchema, discriminatorConverter, discriminatorName);
262+
this.AddDiscriminatorObject(knownTypeSchema, discriminatorConverter, discriminatorName);
263+
264+
var baseDiscriminator = baseSchema.ResponsibleDiscriminatorObject ?? baseSchema.ActualTypeSchema.ResponsibleDiscriminatorObject;
265+
baseDiscriminator?.AddMapping(knownType, knownTypeSchema);
266+
}
267+
268+
private void AddDiscriminatorObject(
269+
JsonSchema schema,
270+
object discriminatorConverter,
271+
string discriminatorName)
272+
{
273+
if (schema.DiscriminatorObject != null)
274+
{
275+
return;
276+
}
277+
278+
var discriminator = new OpenApiDiscriminator
279+
{
280+
JsonInheritanceConverter = discriminatorConverter,
281+
PropertyName = discriminatorName,
282+
};
283+
284+
schema.DiscriminatorObject = discriminator;
285+
286+
if (schema.Properties.ContainsKey(discriminatorName))
287+
{
288+
return;
289+
}
290+
291+
schema.Properties[discriminatorName] = new JsonSchemaProperty
292+
{
293+
Type = JsonObjectType.String,
294+
IsRequired = true,
295+
MinLength = 1,
296+
};
297+
}
298+
299+
private object TryGetInheritanceDiscriminatorConverter(Type type)
300+
{
301+
var typeAttributes = type.GetTypeInfo().GetCustomAttributes(false).OfType<Attribute>();
302+
303+
dynamic jsonConverterAttribute = typeAttributes.FirstAssignableToTypeNameOrDefault(nameof(JsonConverterAttribute), TypeNameStyle.Name);
304+
if (jsonConverterAttribute != null)
305+
{
306+
var converterType = (Type)jsonConverterAttribute.ConverterType;
307+
if (converterType != null && (
308+
309+
// Newtonsoft's converter
310+
converterType.IsAssignableToTypeName(nameof(JsonInheritanceConverter), TypeNameStyle.Name)
311+
312+
// System.Text.Json's converter
313+
|| converterType.IsAssignableToTypeName(nameof(JsonInheritanceConverter) + "`1", TypeNameStyle.Name)))
314+
{
315+
return ObjectExtensions.HasProperty(jsonConverterAttribute, "ConverterParameters") &&
316+
jsonConverterAttribute.ConverterParameters != null &&
317+
jsonConverterAttribute.ConverterParameters.Length > 0 ?
318+
Activator.CreateInstance(jsonConverterAttribute.ConverterType, jsonConverterAttribute.ConverterParameters) :
319+
Activator.CreateInstance(jsonConverterAttribute.ConverterType);
320+
}
321+
}
322+
323+
return null;
324+
}
325+
326+
private string TryGetInheritanceDiscriminatorName(object jsonInheritanceConverter)
327+
{
328+
return ObjectExtensions.TryGetPropertyValue(
329+
jsonInheritanceConverter,
330+
nameof(JsonInheritanceConverter.DiscriminatorName),
331+
JsonInheritanceConverter.DefaultDiscriminatorName);
332+
}
333+
}
334+
}

src/Unchase.OpenAPI.Connectedservice.Shared/Unchase.OpenAPI.Connectedservice.Shared.projitems

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<ItemGroup>
1212
<Compile Include="$(MSBuildThisFileDirectory)CodeGeneration\BaseCodeGenDescriptor.cs" />
1313
<Compile Include="$(MSBuildThisFileDirectory)CodeGeneration\NSwagCodeGenDescriptor.cs" />
14+
<Compile Include="$(MSBuildThisFileDirectory)CodeGeneration\OneOfOperationProcessor.cs" />
1415
<Compile Include="$(MSBuildThisFileDirectory)Commands\DiffSpecificationsCommand.cs" />
1516
<Compile Include="$(MSBuildThisFileDirectory)Commands\OpenWithNSwagStudioCommand.cs" />
1617
<Compile Include="$(MSBuildThisFileDirectory)Commands\OpenWithNSwagStudioCommandPackage.cs" />

0 commit comments

Comments
 (0)