Skip to content

Commit

Permalink
Generate properties for objects without schema (#3095)
Browse files Browse the repository at this point in the history
Generate properties whenever there are objects without a schema (not only IFormFile or IFormFileCollection).
Resolves #3094.
  • Loading branch information
jgarciadelanoceda authored Oct 2, 2024
1 parent 5e65c23 commit c051ca6
Show file tree
Hide file tree
Showing 7 changed files with 298 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -480,19 +480,15 @@ static OpenApiSchema GenerateSchemaIncludingFormFile(ApiParameterDescription api
if (generatedSchema.Reference is null && apiParameterDescription.IsFromForm())
{
mediaType.Encoding.Add(apiParameterDescription.Name, new OpenApiEncoding { Style = ParameterStyle.Form });
if ((generatedSchema.Type == "string" && generatedSchema.Format == "binary")
|| (generatedSchema.Type == "array" && generatedSchema.Items.Type == "string" && generatedSchema.Items.Format == "binary"))
return new OpenApiSchema()
{
return new OpenApiSchema()
Type = "object",
Properties = new Dictionary<string, OpenApiSchema>()
{
Type = "object",
Properties = new Dictionary<string, OpenApiSchema>()
{
[apiParameterDescription.Name] = generatedSchema
},
Required = apiParameterDescription.IsRequired ? new SortedSet<string>() { apiParameterDescription.Name } : null
};
}
[apiParameterDescription.Name] = generatedSchema
},
Required = apiParameterDescription.IsRequired ? new SortedSet<string>() { apiParameterDescription.Name } : null
};
}
return generatedSchema;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,67 @@
}
}
},
"/WithOpenApi/IFromFileAndString": {
"post": {
"tags": [
"WithOpenApi"
],
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"allOf": [
{
"required": [
"file"
],
"type": "object",
"properties": {
"file": {
"type": "string",
"format": "binary"
}
}
},
{
"required": [
"tags"
],
"type": "object",
"properties": {
"tags": {
"type": "string"
}
}
}
]
},
"encoding": {
"file": {
"style": "form"
},
"tags": {
"style": "form"
}
}
}
},
"required": true
},
"responses": {
"200": {
"description": "OK",
"content": {
"text/plain": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/XmlComments/Car/{id}": {
"get": {
"tags": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,67 @@
}
}
},
"/WithOpenApi/IFromFileAndString": {
"post": {
"tags": [
"WithOpenApi"
],
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"allOf": [
{
"required": [
"file"
],
"type": "object",
"properties": {
"file": {
"type": "string",
"format": "binary"
}
}
},
{
"required": [
"tags"
],
"type": "object",
"properties": {
"tags": {
"type": "string"
}
}
}
]
},
"encoding": {
"file": {
"style": "form"
},
"tags": {
"style": "form"
}
}
}
},
"required": true
},
"responses": {
"200": {
"description": "OK",
"content": {
"text/plain": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/XmlComments/Car/{id}": {
"get": {
"tags": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2260,6 +2260,67 @@ public void GetSwagger_GenerateConsumesSchemas_ForProvidedOpenApiOperationWithIF
Assert.Equal(ParameterStyle.Form, content.Value.Encoding["param"].Style);
}

[Fact]
public void GetSwagger_GenerateConsumesSchemas_ForProvidedOpenApiOperationWithStringFromForm()
{
var methodInfo = typeof(FakeController).GetMethod(nameof(FakeController.ActionWithConsumesAttribute));
var actionDescriptor = new ActionDescriptor
{
EndpointMetadata =
[
new OpenApiOperation
{
OperationId = "OperationIdSetInMetadata",
RequestBody = new()
{
Content = new Dictionary<string, OpenApiMediaType>()
{
["application/someMediaType"] = new()
}
}
}
],
RouteValues = new Dictionary<string, string>
{
["controller"] = methodInfo.DeclaringType.Name.Replace("Controller", string.Empty)
}
};
var subject = Subject(
apiDescriptions:
[
ApiDescriptionFactory.Create(
actionDescriptor,
methodInfo,
groupName: "v1",
httpMethod: "POST",
relativePath: "resource",
parameterDescriptions:
[
new ApiParameterDescription()
{
Name = "param",
Source = BindingSource.Form,
ModelMetadata = ModelMetadataFactory.CreateForType(typeof(string))
}
]),
]
);

var document = subject.GetSwagger("v1");

Assert.Equal("OperationIdSetInMetadata", document.Paths["/resource"].Operations[OperationType.Post].OperationId);
var content = Assert.Single(document.Paths["/resource"].Operations[OperationType.Post].RequestBody.Content);
Assert.Equal("application/someMediaType", content.Key);
Assert.NotNull(content.Value.Schema);
Assert.Equal("object", content.Value.Schema.Type);
Assert.NotEmpty(content.Value.Schema.Properties);
Assert.NotNull(content.Value.Schema.Properties["param"]);
Assert.Equal("string", content.Value.Schema.Properties["param"].Type);
Assert.NotNull(content.Value.Encoding);
Assert.NotNull(content.Value.Encoding["param"]);
Assert.Equal(ParameterStyle.Form, content.Value.Encoding["param"].Style);
}

private static SwaggerGenerator Subject(
IEnumerable<ApiDescription> apiDescriptions,
SwaggerGeneratorOptions options = null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
Info: {
Title: Test API,
Version: V1
},
Paths: {
/resource: {
Operations: {
Post: {
OperationId: OperationIdSetInMetadata,
RequestBody: {
UnresolvedReference: false,
Required: false,
Content: {
application/someMediaType: {
Schema: {
Type: object,
ReadOnly: false,
WriteOnly: false,
Properties: {
param: {
Type: string,
ReadOnly: false,
WriteOnly: false,
AdditionalPropertiesAllowed: true,
Nullable: false,
Deprecated: false,
UnresolvedReference: false
}
},
AdditionalPropertiesAllowed: true,
Nullable: false,
Deprecated: false,
UnresolvedReference: false
},
Encoding: {
param: {
Style: Form
}
}
}
}
},
Deprecated: false
}
},
UnresolvedReference: false
}
},
Components: {},
HashCode: 7639B8A665AFC72F5C8D9ED02AA2E6416B9F82FDCC86D490FD248D3B657355F3993BD00384468E8D23DC0AC9FACECD425824F9596F6183EBDF974B9343CEDCF7
}
Original file line number Diff line number Diff line change
Expand Up @@ -1310,6 +1310,57 @@ public Task GetSwagger_GenerateConsumesSchemas_ForProvidedOpenApiOperationWithIF
return Verifier.Verify(document);
}

[Fact]
public Task GetSwagger_GenerateConsumesSchemas_ForProvidedOpenApiOperationWithStringFromForm()
{
var methodInfo = typeof(FakeController).GetMethod(nameof(FakeController.ActionWithConsumesAttribute));
var actionDescriptor = new ActionDescriptor
{
EndpointMetadata =
[
new OpenApiOperation
{
OperationId = "OperationIdSetInMetadata",
RequestBody = new()
{
Content = new Dictionary<string, OpenApiMediaType>()
{
["application/someMediaType"] = new()
}
}
}
],
RouteValues = new Dictionary<string, string>
{
["controller"] = methodInfo.DeclaringType.Name.Replace("Controller", string.Empty)
}
};
var subject = Subject(
apiDescriptions:
[
ApiDescriptionFactory.Create(
actionDescriptor,
methodInfo,
groupName: "v1",
httpMethod: "POST",
relativePath: "resource",
parameterDescriptions:
[
new ApiParameterDescription()
{
Name = "param",
Source = BindingSource.Form,
ModelMetadata = ModelMetadataFactory.CreateForType(typeof(string))
}
]),
]
);

var document = subject.GetSwagger("v1");

return Verifier.Verify(document);
}

private static SwaggerGenerator Subject(
IEnumerable<ApiDescription> apiDescriptions,
SwaggerGeneratorOptions options = null,
Expand Down
5 changes: 5 additions & 0 deletions test/WebSites/WebApi/EndPoints/OpenApiEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ public static IEndpointRouteBuilder MapWithOpenApiEndpoints(this IEndpointRouteB
return $"{dto}";
}).WithOpenApi();

group.MapPost("/IFromFileAndString", (IFormFile file, [FromForm] string tags) =>
{
return $"{file.FileName}{tags}";
}).WithOpenApi();

return app;
}
}
Expand Down

0 comments on commit c051ca6

Please sign in to comment.