Skip to content

Commit

Permalink
Merge pull request #1602 from microsoft/feature/request-unknown-types
Browse files Browse the repository at this point in the history
aligns request body and response schema election
  • Loading branch information
baywet authored Jun 2, 2022
2 parents 6515dd5 + 1704c85 commit 5b00886
Show file tree
Hide file tree
Showing 21 changed files with 335 additions and 170 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Added a parameter to specify why mime types to evaluate for models. [#134](https://github.com/microsoft/kiota/issues/134)
- Added an explicit error message for external references in the schema. [#1580](https://github.com/microsoft/kiota/issues/1580)

### Changed

- Aligned mime types model generation behaviour for request bodies on response content. [#134](https://github.com/microsoft/kiota/issues/134)

## [0.2.1] - 2022-05-30

### Added
Expand Down
19 changes: 19 additions & 0 deletions docs/using.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ kiota (--openapi | -d) <path>
[(--serializer | -s) <classes>]
[(--deserializer | --ds) <classes>]
[--clean-output | --co]
[(--structured-mime-types | -m) <mime-types>]
```

## Mandatory parameters
Expand Down Expand Up @@ -170,6 +171,24 @@ One or more module names that implements `ISerializationWriterFactory`.
kiota --serializer Contoso.Json.CustomSerializer
```

### `--structured-mime-types (-m)`

The MIME types to use for structured data model generation. Accepts multiple values.

Default values :

- `application/json`
- `application/xml`
- `text/plain`
- `text/xml`
- `text/yaml`

> Note: Only request body types or response types with a defined schema will generate models, other entries will default back to stream/byte array.
#### Accepted values

Any valid MIME type which will match a request body type or a response type in the OpenAPI description.

## Examples

```shell
Expand Down
4 changes: 2 additions & 2 deletions src/Kiota.Builder/CodeDOM/CodeMethod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ public void AddPathQueryOrHeaderParameter(params CodeParameter[] parameters)
public bool IsAccessor {
get => IsOfKind(CodeMethodKind.Getter, CodeMethodKind.Setter);
}
public List<string> SerializerModules { get; set; }
public List<string> DeserializerModules { get; set; }
public HashSet<string> SerializerModules { get; set; }
public HashSet<string> DeserializerModules { get; set; }
/// <summary>
/// Indicates whether this method is an overload for another method.
/// </summary>
Expand Down
21 changes: 8 additions & 13 deletions src/Kiota.Builder/Extensions/OpenApiOperationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,23 @@
namespace Kiota.Builder.Extensions {
public static class OpenApiOperationExtensions {
private static readonly HashSet<string> successCodes = new(StringComparer.OrdinalIgnoreCase) {"200", "201", "202"}; //204 excluded as it won't have a schema
private static readonly HashSet<string> structuredMimeTypes = new (StringComparer.OrdinalIgnoreCase) {
"application/json",
"application/xml",
"text/plain",
"text/xml",
"text/yaml",
};
/// <summary>
/// cleans application/vnd.github.mercy-preview+json to application/json
/// </summary>
private static readonly Regex vendorSpecificCleanup = new(@"[^/]+\+", RegexOptions.Compiled);
public static OpenApiSchema GetResponseSchema(this OpenApiOperation operation)
public static OpenApiSchema GetResponseSchema(this OpenApiOperation operation, HashSet<string> structuredMimeTypes)
{
// Return Schema that represents all the possible success responses!
var schemas = operation.Responses.Where(r => successCodes.Contains(r.Key))
.SelectMany(re => re.Value.GetResponseSchemas());
.SelectMany(re => re.Value.Content.GetValidSchemas(structuredMimeTypes));

return schemas.FirstOrDefault();
}
public static IEnumerable<OpenApiSchema> GetResponseSchemas(this OpenApiResponse response)
public static IEnumerable<OpenApiSchema> GetValidSchemas(this IDictionary<string, OpenApiMediaType> source, HashSet<string> structuredMimeTypes)
{
var schemas = response.Content
if(!(structuredMimeTypes?.Any() ?? false))
throw new ArgumentNullException(nameof(structuredMimeTypes));
var schemas = source
.Where(c => !string.IsNullOrEmpty(c.Key))
.Select(c => (Key: c.Key.Split(';', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(), c.Value))
.Where(c => structuredMimeTypes.Contains(c.Key) || structuredMimeTypes.Contains(vendorSpecificCleanup.Replace(c.Key, string.Empty)))
Expand All @@ -37,9 +32,9 @@ public static IEnumerable<OpenApiSchema> GetResponseSchemas(this OpenApiResponse

return schemas;
}
public static OpenApiSchema GetResponseSchema(this OpenApiResponse response)
public static OpenApiSchema GetResponseSchema(this OpenApiResponse response, HashSet<string> structuredMimeTypes)
{
return response.GetResponseSchemas().FirstOrDefault();
return response.Content.GetValidSchemas(structuredMimeTypes).FirstOrDefault();
}
}
}
6 changes: 3 additions & 3 deletions src/Kiota.Builder/Extensions/OpenApiUrlTreeNodeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,10 @@ public static IEnumerable<OpenApiParameter> GetPathParametersForCurrentSegment(t
///<summary>
/// Returns the class name for the node with more or less precision depending on the provided arguments
///</summary>
public static string GetClassName(this OpenApiUrlTreeNode currentNode, string suffix = default, string prefix = default, OpenApiOperation operation = default, OpenApiResponse response = default, OpenApiSchema schema = default) {
public static string GetClassName(this OpenApiUrlTreeNode currentNode, HashSet<string> structuredMimeTypes, string suffix = default, string prefix = default, OpenApiOperation operation = default, OpenApiResponse response = default, OpenApiSchema schema = default) {
var rawClassName = schema?.Reference?.GetClassName() ??
response?.GetResponseSchema()?.Reference?.GetClassName() ??
operation?.GetResponseSchema()?.Reference?.GetClassName() ??
response?.GetResponseSchema(structuredMimeTypes)?.Reference?.GetClassName() ??
operation?.GetResponseSchema(structuredMimeTypes)?.Reference?.GetClassName() ??
CleanupParametersFromPath(currentNode.Segment)?.ReplaceValueIdentifier();
if((currentNode?.DoesNodeBelongToItemSubnamespace() ?? false) && idClassNameCleanup.IsMatch(rawClassName)) {
rawClassName = idClassNameCleanup.Replace(rawClassName, string.Empty);
Expand Down
17 changes: 15 additions & 2 deletions src/Kiota.Builder/GenerationConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,14 @@ public class GenerationConfiguration {
public string ApiRootUrl { get; set; }
public string[] PropertiesPrefixToStrip { get; set; } = new string[] { "@odata."};
public bool UsesBackingStore { get; set; }
public List<string> Serializers { get; set; } = new();
public List<string> Deserializers { get; set; } = new();
public HashSet<string> Serializers { get; set; } = new(StringComparer.OrdinalIgnoreCase){
"Microsoft.Kiota.Serialization.Json.JsonSerializationWriterFactory",
"Microsoft.Kiota.Serialization.Text.TextSerializationWriterFactory"
};
public HashSet<string> Deserializers { get; set; } = new(StringComparer.OrdinalIgnoreCase) {
"Microsoft.Kiota.Serialization.Json.JsonParseNodeFactory",
"Microsoft.Kiota.Serialization.Text.TextParseNodeFactory"
};
public bool ShouldWriteNamespaceIndices { get { return BarreledLanguages.Contains(Language); } }
public bool ShouldWriteBarrelsIfClassExists { get { return BarreledLanguagesWithConstantFileName.Contains(Language); } }
public bool ShouldRenderMethodsOutsideOfClasses { get { return MethodOutsideOfClassesLanguages.Contains(Language); } }
Expand All @@ -30,5 +36,12 @@ public class GenerationConfiguration {
GenerationLanguage.TypeScript
};
public bool CleanOutput { get; set;}
public HashSet<string> StructuredMimeTypes { get; set; } = new(StringComparer.OrdinalIgnoreCase) {
"application/json",
"application/xml",
"text/plain",
"text/xml",
"text/yaml",
};
}
}
25 changes: 11 additions & 14 deletions src/Kiota.Builder/KiotaBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ private void CreateRequestBuilderClass(CodeNamespace currentNamespace, OpenApiUr
else
{
var targetNS = currentNode.DoesNodeBelongToItemSubnamespace() ? currentNamespace.EnsureItemNamespace() : currentNamespace;
var className = currentNode.DoesNodeBelongToItemSubnamespace() ? currentNode.GetClassName(itemRequestBuilderSuffix) :currentNode.GetClassName(requestBuilderSuffix);
var className = currentNode.DoesNodeBelongToItemSubnamespace() ? currentNode.GetClassName(config.StructuredMimeTypes, itemRequestBuilderSuffix) :currentNode.GetClassName(config.StructuredMimeTypes, requestBuilderSuffix);
codeClass = targetNS.AddClass(new CodeClass {
Name = className.CleanupSymbolName(),
Kind = CodeClassKind.RequestBuilder,
Expand All @@ -315,7 +315,7 @@ private void CreateRequestBuilderClass(CodeNamespace currentNamespace, OpenApiUr
// Add properties for children
foreach (var child in currentNode.Children)
{
var propIdentifier = child.Value.GetClassName();
var propIdentifier = child.Value.GetClassName(config.StructuredMimeTypes);
var propType = child.Value.DoesNodeBelongToItemSubnamespace() ? propIdentifier + itemRequestBuilderSuffix : propIdentifier + requestBuilderSuffix;
if (child.Value.IsPathSegmentWithSingleSimpleParameter())
{
Expand Down Expand Up @@ -620,7 +620,6 @@ private static CodeType GetPrimitiveType(OpenApiSchema typeSchema, string childT
IsExternal = isExternal,
};
}
private const string RequestBodyBinaryContentType = "application/octet-stream";
private const string RequestBodyPlainTextContentType = "text/plain";
private static readonly HashSet<string> noContentStatusCodes = new() { "201", "202", "204" };
private static readonly HashSet<string> errorStatusCodes = new(Enumerable.Range(400, 599).Select(x => x.ToString())
Expand All @@ -629,7 +628,7 @@ private static CodeType GetPrimitiveType(OpenApiSchema typeSchema, string childT
private void AddErrorMappingsForExecutorMethod(OpenApiUrlTreeNode currentNode, OpenApiOperation operation, CodeMethod executorMethod) {
foreach(var response in operation.Responses.Where(x => errorStatusCodes.Contains(x.Key))) {
var errorCode = response.Key.ToUpperInvariant();
var errorSchema = response.Value.GetResponseSchema();
var errorSchema = response.Value.GetResponseSchema(config.StructuredMimeTypes);
if(errorSchema != null) {
var parentElement = string.IsNullOrEmpty(response.Value.Reference?.Id) && string.IsNullOrEmpty(errorSchema?.Reference?.Id)
? executorMethod as CodeElement
Expand All @@ -654,7 +653,7 @@ private void CreateOperationMethods(OpenApiUrlTreeNode currentNode, OperationTyp
Description = "Configuration for the request such as headers, query parameters, and middleware options.",
}).First();

var schema = operation.GetResponseSchema();
var schema = operation.GetResponseSchema(config.StructuredMimeTypes);
var method = (HttpMethod)Enum.Parse(typeof(HttpMethod), operationType.ToString());
var executorMethod = parentClass.AddMethod(new CodeMethod {
Name = operationType.ToString(),
Expand Down Expand Up @@ -748,10 +747,8 @@ private static void SetPathAndQueryParameters(CodeMethod target, OpenApiUrlTreeN
}

private void AddRequestBuilderMethodParameters(OpenApiUrlTreeNode currentNode, OperationType operationType, OpenApiOperation operation, CodeClass parameterClass, CodeClass requestConfigClass, CodeMethod method) {
var nonBinaryRequestBody = operation.RequestBody?.Content?.FirstOrDefault(x => !RequestBodyBinaryContentType.Equals(x.Key, StringComparison.OrdinalIgnoreCase));
if (nonBinaryRequestBody.HasValue && nonBinaryRequestBody.Value.Value != null)
if (operation.RequestBody?.Content?.GetValidSchemas(config.StructuredMimeTypes)?.FirstOrDefault() is OpenApiSchema requestBodySchema)
{
var requestBodySchema = nonBinaryRequestBody.Value.Value.Schema;
var requestBodyType = CreateModelDeclarations(currentNode, requestBodySchema, operation, method, $"{operationType}RequestBody");
method.AddParameter(new CodeParameter {
Name = "body",
Expand All @@ -760,8 +757,8 @@ private void AddRequestBuilderMethodParameters(OpenApiUrlTreeNode currentNode, O
Kind = CodeParameterKind.RequestBody,
Description = requestBodySchema.Description.CleanupDescription()
});
method.ContentType = nonBinaryRequestBody.Value.Key;
} else if (operation.RequestBody?.Content?.ContainsKey(RequestBodyBinaryContentType) ?? false) {
method.ContentType = operation.RequestBody.Content.First(x => x.Value.Schema == requestBodySchema).Key;
} else if (operation.RequestBody?.Content?.Any() ?? false) {
var nParam = new CodeParameter {
Name = "body",
Optional = false,
Expand Down Expand Up @@ -817,7 +814,7 @@ private string GetModelsNamespaceNameFromReferenceId(string referenceId) {
return $"{modelsNamespace.Name}{namespaceSuffix}";
}
private CodeType CreateModelDeclarationAndType(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, OpenApiOperation operation, CodeNamespace codeNamespace, string classNameSuffix = "", OpenApiResponse response = default, string typeNameForInlineSchema = "") {
var className = string.IsNullOrEmpty(typeNameForInlineSchema) ? currentNode.GetClassName(operation: operation, suffix: classNameSuffix, response: response, schema: schema).CleanupSymbolName() : typeNameForInlineSchema;
var className = string.IsNullOrEmpty(typeNameForInlineSchema) ? currentNode.GetClassName(config.StructuredMimeTypes, operation: operation, suffix: classNameSuffix, response: response, schema: schema).CleanupSymbolName() : typeNameForInlineSchema;
var codeDeclaration = AddModelDeclarationIfDoesntExist(currentNode, schema, className, codeNamespace);
return new CodeType {
TypeDefinition = codeDeclaration,
Expand All @@ -835,7 +832,7 @@ private CodeTypeBase CreateInheritedModelDeclaration(OpenApiUrlTreeNode currentN
var shortestNamespace = string.IsNullOrEmpty(referenceId) ? codeNamespaceFromParent : rootNamespace.FindNamespaceByName(shortestNamespaceName);
if(shortestNamespace == null)
shortestNamespace = rootNamespace.AddNamespace(shortestNamespaceName);
className = (currentSchema.GetSchemaName() ?? currentNode.GetClassName(operation: operation, schema: schema)).CleanupSymbolName();
className = (currentSchema.GetSchemaName() ?? currentNode.GetClassName(config.StructuredMimeTypes, operation: operation, schema: schema)).CleanupSymbolName();
codeDeclaration = AddModelDeclarationIfDoesntExist(currentNode, currentSchema, className, shortestNamespace, codeDeclaration as CodeClass);
}

Expand All @@ -862,7 +859,7 @@ private static string GetReferenceIdFromOriginalSchema(OpenApiSchema schema, Ope
?.Reference?.Id;
}
private CodeTypeBase CreateComposedModelDeclaration(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, OpenApiOperation operation, string suffixForInlineSchema, CodeNamespace codeNamespace) {
var typeName = currentNode.GetClassName(operation: operation, suffix: suffixForInlineSchema, schema: schema).CleanupSymbolName();
var typeName = currentNode.GetClassName(config.StructuredMimeTypes, operation: operation, suffix: suffixForInlineSchema, schema: schema).CleanupSymbolName();
var (unionType, schemas) = (schema.IsOneOf(), schema.IsAnyOf()) switch {
(true, false) => (new CodeExclusionType {
Name = typeName,
Expand Down Expand Up @@ -1032,7 +1029,7 @@ private CodeTypeBase GetCodeTypeForMapping(OpenApiUrlTreeNode currentNode, strin
logger.LogWarning("Discriminator {componentKey} not found in the OpenAPI document.", componentKey);
return null;
}
var className = currentNode.GetClassName(schema: discriminatorSchema).CleanupSymbolName();
var className = currentNode.GetClassName(config.StructuredMimeTypes, schema: discriminatorSchema).CleanupSymbolName();
var shouldInherit = discriminatorSchema.AllOf.Any(x => currentSchema.Reference?.Id.Equals(x.Reference?.Id, StringComparison.OrdinalIgnoreCase) ?? false);
var codeClass = AddModelDeclarationIfDoesntExist(currentNode, discriminatorSchema, className, currentNamespace, shouldInherit ? currentClass : null);
return new CodeType {
Expand Down
Loading

0 comments on commit 5b00886

Please sign in to comment.