Skip to content

Commit

Permalink
Support $ref in requestBody (#4917)
Browse files Browse the repository at this point in the history
* Added parsing for referenced request body

* Support OpenApiOperation.ActualRequestBody in OpenApiParameter helpers

* Use ActualRequestBody in code generation

* Added support for "requestBodies" in components

* Fix nullable reference
  • Loading branch information
eatdrinksleepcode committed Jul 17, 2024
1 parent ad81db0 commit 7d6df3a
Show file tree
Hide file tree
Showing 11 changed files with 90 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public CSharpFileTemplateModel(
public bool RequiresFileParameterType =>
_settings.CSharpGeneratorSettings.ExcludedTypeNames?.Contains("FileParameter") != true &&
(_document.Operations.Any(o => o.Operation.ActualParameters.Any(p => p.ActualTypeSchema.IsBinary)) ||
_document.Operations.Any(o => o.Operation?.RequestBody?.Content?.Any(c => c.Value.Schema?.IsBinary == true ||
_document.Operations.Any(o => o.Operation?.ActualRequestBody?.Content?.Any(c => c.Value.Schema?.IsBinary == true ||
c.Value.Schema?.ActualSchema.ActualProperties.Any(p => p.Value.IsBinary ||
p.Value.Item?.IsBinary == true ||
p.Value.Items.Any(i => i.IsBinary)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ public IEnumerable<string> ResponseClassNames
public bool RequiresFileParameterInterface =>
!_settings.TypeScriptGeneratorSettings.ExcludedTypeNames.Contains("FileParameter") &&
(_document.Operations.Any(o => o.Operation.ActualParameters.Any(p => p.ActualTypeSchema.IsBinary)) ||
_document.Operations.Any(o => o.Operation?.RequestBody?.Content?.Any(c => c.Value.Schema?.IsBinary == true ||
_document.Operations.Any(o => o.Operation?.ActualRequestBody?.Content?.Any(c => c.Value.Schema?.IsBinary == true ||
c.Value.Schema?.ActualProperties.Any(p => p.Value.IsBinary ||
p.Value.Item?.IsBinary == true ||
p.Value.Items.Any(i => i.IsBinary)
Expand Down
8 changes: 4 additions & 4 deletions src/NSwag.CodeGeneration/Models/OperationModelBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ public TParameterModel ContentParameter
/// <summary>Gets a value indicating whether the operation consumes 'application/x-www-form-urlencoded'.</summary>
public bool ConsumesFormUrlEncoded =>
_operation.ActualConsumes?.Any(c => c == "application/x-www-form-urlencoded") == true ||
_operation.RequestBody?.Content.Any(mt => mt.Key == "application/x-www-form-urlencoded") == true;
_operation.ActualRequestBody?.Content.Any(mt => mt.Key == "application/x-www-form-urlencoded") == true;

/// <summary>Gets the form parameters.</summary>
public IEnumerable<TParameterModel> FormParameters => Parameters.Where(p => p.Kind == OpenApiParameterKind.FormData);
Expand Down Expand Up @@ -259,7 +259,7 @@ public string Consumes
}

return _operation.ActualConsumes?.FirstOrDefault() ??
_operation.RequestBody?.Content.Keys.FirstOrDefault() ??
_operation.ActualRequestBody?.Content.Keys.FirstOrDefault() ??
"application/json";
}
}
Expand Down Expand Up @@ -350,8 +350,8 @@ protected IList<OpenApiParameter> GetActualParameters()
.ToList();

var formDataSchema =
_operation?.RequestBody?.Content?.ContainsKey("multipart/form-data") == true ?
_operation.RequestBody.Content["multipart/form-data"]?.Schema.ActualSchema: null;
_operation?.ActualRequestBody?.Content?.ContainsKey("multipart/form-data") == true ?
_operation.ActualRequestBody.Content["multipart/form-data"]?.Schema.ActualSchema: null;

if (formDataSchema != null && formDataSchema.ActualProperties.Count > 0)
{
Expand Down
9 changes: 9 additions & 0 deletions src/NSwag.Core.Tests/Serialization/ExternalReferenceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ public async Task When_file_contains_path_reference_to_another_file_it_is_loaded
Assert.Equal("External path", document.Paths.First().Value.ActualPathItem.Values.First().Description);
}

[Fact]
public async Task When_file_contains_requestBody_reference_to_another_file_it_is_loaded()
{
var document = await OpenApiDocument.FromFileAsync("TestFiles/requestBody-reference.json");

Assert.NotNull(document);
Assert.Equal("External request body", document.Paths.First().Value.Values.First().RequestBody.ActualRequestBody.Description);
}

[Fact]
public async Task When_file_contains_response_reference_to_another_file_it_is_loaded()
{
Expand Down
13 changes: 13 additions & 0 deletions src/NSwag.Core.Tests/TestFiles/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,19 @@
"additionalProperties": true
}
},
"requestBodies": {
"TestRequest": {
"description": "External request body",
"required": true,
"content": {
"text/json": {
"schema": {
"$ref": "#/components/schemas/TestObject"
}
}
}
}
},
"responses": {
"TestResponse": {
"description": "External response",
Expand Down
14 changes: 14 additions & 0 deletions src/NSwag.Core.Tests/TestFiles/requestBody-reference.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"openapi": "3.0.2",
"paths": {
"/test": {
"get": {
"description": "Test path",
"requestBody": {
"$ref": "./common.json#/components/requestBodies/TestRequest"
},
"responses": {}
}
}
}
}
14 changes: 14 additions & 0 deletions src/NSwag.Core/OpenApiComponents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ public OpenApiComponents(OpenApiDocument document)
};
Schemas = schemas;

var requestBodies = new ObservableDictionary<string, OpenApiRequestBody>();
requestBodies.CollectionChanged += (sender, args) =>
{
foreach (var path in RequestBodies.Values)
{
path.Parent = document;
}
};
RequestBodies = requestBodies;

var responses = new ObservableDictionary<string, OpenApiResponse>();
responses.CollectionChanged += (sender, args) =>
{
Expand Down Expand Up @@ -86,6 +96,10 @@ public OpenApiComponents(OpenApiDocument document)
[JsonProperty(PropertyName = "schemas", DefaultValueHandling = DefaultValueHandling.Ignore)]
public IDictionary<string, JsonSchema> Schemas { get; }

/// <summary>Gets or sets the responses which can be used for all operations.</summary>
[JsonProperty(PropertyName = "requestBodies", DefaultValueHandling = DefaultValueHandling.Ignore)]
public IDictionary<string, OpenApiRequestBody> RequestBodies { get; }

/// <summary>Gets or sets the responses which can be used for all operations.</summary>
[JsonProperty(PropertyName = "responses", DefaultValueHandling = DefaultValueHandling.Ignore)]
public IDictionary<string, OpenApiResponse> Responses { get; }
Expand Down
4 changes: 2 additions & 2 deletions src/NSwag.Core/OpenApiMediaType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public JsonSchema Schema
set
{
_schema = value;
Parent?.Parent?.UpdateBodyParameter();
Parent?.ParentOperation?.UpdateBodyParameter();
}
}

Expand All @@ -41,7 +41,7 @@ public object Example
set
{
_example = value;
Parent?.Parent?.UpdateBodyParameter();
Parent?.ParentOperation?.UpdateBodyParameter();
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/NSwag.Core/OpenApiOperation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ public IReadOnlyList<OpenApiParameter> ActualParameters
[JsonIgnore]
public ICollection<OpenApiSchema> ActualSchemes => Schemes ?? Parent.Parent.Schemes;

/// <summary>Gets the response body and dereferences it if necessary.</summary>
[JsonIgnore]
public OpenApiRequestBody ActualRequestBody => RequestBody?.ActualRequestBody;

/// <summary>Gets the responses from the operation and from the <see cref="OpenApiDocument"/> and dereferences them if necessary.</summary>
[JsonIgnore]
public IReadOnlyDictionary<string, OpenApiResponse> ActualResponses => Responses.ToDictionary(t => t.Key, t => t.Value.ActualResponse);
Expand Down
6 changes: 3 additions & 3 deletions src/NSwag.Core/OpenApiParameter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ public bool IsXmlBodyParameter
var parent = Parent as OpenApiOperation;
var consumes = parent?.ActualConsumes?.Any() == true ?
parent.ActualConsumes :
parent?.RequestBody?.Content.Keys;
parent?.ActualRequestBody?.Content.Keys;

return consumes?.Any() == true &&
consumes.Any(p => p.Contains("application/xml")) &&
Expand Down Expand Up @@ -256,7 +256,7 @@ public bool IsBinaryBodyParameter
}
else
{
var consumes = parent?.RequestBody?.Content;
var consumes = parent?.ActualRequestBody?.Content;
return (consumes?.Any(p => p.Key == "multipart/form-data") == true ||
consumes?.Any(p => p.Value.Schema?.IsBinary != false) == true) &&
consumes.Any(p => p.Key.Contains("*/*") && p.Value.Schema?.IsBinary != true) == false &&
Expand Down Expand Up @@ -286,7 +286,7 @@ public bool HasBinaryBodyWithMultipleMimeTypes
}
else
{
var consumes = parent?.RequestBody?.Content;
var consumes = parent?.ActualRequestBody?.Content;
return consumes?.Any() == true &&
(consumes.Count() > 1 ||
consumes.Any(p => p.Key.Contains("*")));
Expand Down
32 changes: 25 additions & 7 deletions src/NSwag.Core/OpenApiRequestBody.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@

using System.Collections.Generic;
using Newtonsoft.Json;
using NJsonSchema.References;
using NSwag.Collections;

namespace NSwag
{
/// <summary>The OpenApi request body (OpenAPI only).</summary>
public class OpenApiRequestBody
public class OpenApiRequestBody : JsonReferenceBase<OpenApiRequestBody>, IJsonReference
{
private string _name;
private bool _isRequired;
Expand All @@ -31,13 +32,20 @@ public OpenApiRequestBody()
mediaType.Parent = this;
}
Parent?.UpdateBodyParameter();
ParentOperation?.UpdateBodyParameter();
};
Content = content;
}

[JsonIgnore]
internal OpenApiOperation Parent { get; set; }
internal object Parent { get; set; }

[JsonIgnore]
internal OpenApiOperation ParentOperation => Parent as OpenApiOperation;

/// <summary>Gets the actual request body, either this or the referenced request body.</summary>
[JsonIgnore]
public OpenApiRequestBody ActualRequestBody => Reference ?? this;

/// <summary>Gets or sets the name.</summary>
[JsonProperty(PropertyName = "x-name", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)]
Expand All @@ -47,7 +55,7 @@ public string Name
set
{
_name = value;
Parent?.UpdateBodyParameter();
ParentOperation?.UpdateBodyParameter();
}
}

Expand All @@ -59,7 +67,7 @@ public string Description
set
{
_description = value;
Parent?.UpdateBodyParameter();
ParentOperation?.UpdateBodyParameter();
}
}

Expand All @@ -75,7 +83,7 @@ public bool IsRequired
set
{
_isRequired = value;
Parent?.UpdateBodyParameter();
ParentOperation?.UpdateBodyParameter();
}
}

Expand All @@ -87,12 +95,22 @@ public int? Position
set
{
_position = value;
Parent?.UpdateBodyParameter();
ParentOperation?.UpdateBodyParameter();
}
}

/// <summary>Gets the actual name of the request body parameter.</summary>
[JsonIgnore]
public string ActualName => string.IsNullOrEmpty(Name) ? "body" : Name;

#region Implementation of IJsonReference

[JsonIgnore]
IJsonReference IJsonReference.ActualObject => ActualRequestBody;

[JsonIgnore]
object IJsonReference.PossibleRoot => ParentOperation?.Parent?.Parent;

#endregion
}
}

0 comments on commit 7d6df3a

Please sign in to comment.