diff --git a/src/SwaggerProvider.DesignTime/DefinitionCompiler.fs b/src/SwaggerProvider.DesignTime/DefinitionCompiler.fs index 818b2063..d33784c4 100644 --- a/src/SwaggerProvider.DesignTime/DefinitionCompiler.fs +++ b/src/SwaggerProvider.DesignTime/DefinitionCompiler.fs @@ -220,11 +220,11 @@ type DefinitionCompiler(schema: OpenApiDocument, provideNullable, useDateOnly: b ty, getterCode = (function - | [ this ] -> Expr.FieldGetUnchecked(this, providedField) + | [ this ] -> Expr.FieldGet(this, providedField) | _ -> failwith "invalid property getter params"), setterCode = (function - | [ this; v ] -> Expr.FieldSetUnchecked(this, providedField, v) + | [ this; v ] -> Expr.FieldSet(this, providedField, v) | _ -> failwith "invalid property setter params") ) @@ -441,6 +441,17 @@ type DefinitionCompiler(schema: OpenApiDocument, provideNullable, useDateOnly: b else DefinitionPath.DefinitionPrefix + refId + // Helper for the allOf/oneOf/anyOf single-ref collapse pattern. + // When `schemas` has exactly one entry that is a $ref and the outer schema has no + // own properties, collapse directly to the referenced type; otherwise fall back to + // compileNewObject so composite/inline schemas are handled as usual. + let compileSingleRefOrNewObject(schemas: System.Collections.Generic.IList) = + match schemas.[0] with + | :? OpenApiSchemaReference as schemaRef when not(isNull schemaRef.Reference) -> + ns.ReleaseNameReservation tyName + compileByPath <| getFullPath schemaRef.Reference.Id + | _ -> compileNewObject() + let tyType = match schemaObj with | null -> failwithf $"Cannot compile object '%s{tyName}' when schema is 'null'" @@ -472,39 +483,26 @@ type DefinitionCompiler(schema: OpenApiDocument, provideNullable, useDateOnly: b compileBySchema ns (ns.ReserveUniqueName tyName "Item") elSchema true ns.RegisterType false ProvidedTypeBuilder.MakeGenericType(typedefof>, [ typeof; elTy ]) - // Handle allOf with single reference (e.g., nullable reference to another type) + // Handle allOf/oneOf/anyOf with a single $ref and no own properties: + // collapse the wrapper to the referenced type directly. | _ when not(isNull schemaObj.AllOf) && schemaObj.AllOf.Count = 1 && (schemaObj.Properties |> isNull || schemaObj.Properties.Count = 0) -> - match schemaObj.AllOf.[0] with - | :? OpenApiSchemaReference as schemaRef when not(isNull schemaRef.Reference) -> - ns.ReleaseNameReservation tyName - compileByPath <| getFullPath schemaRef.Reference.Id - | _ -> compileNewObject() - // Handle oneOf with single reference (resolves to the referenced type) + compileSingleRefOrNewObject schemaObj.AllOf | _ when not(isNull schemaObj.OneOf) && schemaObj.OneOf.Count = 1 && (schemaObj.Properties |> isNull || schemaObj.Properties.Count = 0) -> - match schemaObj.OneOf.[0] with - | :? OpenApiSchemaReference as schemaRef when not(isNull schemaRef.Reference) -> - ns.ReleaseNameReservation tyName - compileByPath <| getFullPath schemaRef.Reference.Id - | _ -> compileNewObject() - // Handle anyOf with single reference (resolves to the referenced type) + compileSingleRefOrNewObject schemaObj.OneOf | _ when not(isNull schemaObj.AnyOf) && schemaObj.AnyOf.Count = 1 && (schemaObj.Properties |> isNull || schemaObj.Properties.Count = 0) -> - match schemaObj.AnyOf.[0] with - | :? OpenApiSchemaReference as schemaRef when not(isNull schemaRef.Reference) -> - ns.ReleaseNameReservation tyName - compileByPath <| getFullPath schemaRef.Reference.Id - | _ -> compileNewObject() + compileSingleRefOrNewObject schemaObj.AnyOf | _ when resolvedType.IsNone || resolvedType = Some JsonSchemaType.Object diff --git a/tests/SwaggerProvider.Tests/Schema.OperationCompilationTests.fs b/tests/SwaggerProvider.Tests/Schema.OperationCompilationTests.fs index 954725b4..5554b819 100644 --- a/tests/SwaggerProvider.Tests/Schema.OperationCompilationTests.fs +++ b/tests/SwaggerProvider.Tests/Schema.OperationCompilationTests.fs @@ -1112,3 +1112,159 @@ let ``ignoreOperationId=true does not generate the original operationId as metho let methodNames = allMethods |> List.map(fun m -> m.Name) methodNames |> shouldNotContain "ListAllPets" methodNames |> shouldNotContain "GetPetById" + +// ── text/plain request body ─────────────────────────────────────────────────── + +let private textPlainBodySchema = + """openapi: "3.0.0" +info: + title: TextPlainBodyTest + version: "1.0.0" +paths: + /echo: + post: + operationId: echoText + requestBody: + required: true + content: + text/plain: + schema: + type: string + responses: + "200": + description: OK +components: + schemas: {} +""" + +[] +let ``text/plain request body generates a method``() = + let types = compileTaskSchema textPlainBodySchema + let method = findMethod types "EchoText" + method.IsSome |> shouldEqual true + +[] +let ``text/plain request body parameter is named textPlain``() = + let types = compileTaskSchema textPlainBodySchema + let method = (findMethod types "EchoText").Value + let paramNames = method.GetParameters() |> Array.map(fun p -> p.Name) + paramNames |> shouldContain "textPlain" + +[] +let ``text/plain request body has CancellationToken as last parameter``() = + let types = compileTaskSchema textPlainBodySchema + let method = (findMethod types "EchoText").Value + let lastParam = method.GetParameters() |> Array.last + lastParam.ParameterType |> shouldEqual typeof + +// ── application/octet-stream response ──────────────────────────────────────── + +let private octetStreamResponseSchema = + """openapi: "3.0.0" +info: + title: OctetStreamResponseTest + version: "1.0.0" +paths: + /file: + get: + operationId: downloadFile + responses: + "200": + description: File contents + content: + application/octet-stream: + schema: + type: string + format: binary +components: + schemas: {} +""" + +[] +let ``octet-stream response generates a method``() = + let types = compileTaskSchema octetStreamResponseSchema + let method = findMethod types "DownloadFile" + method.IsSome |> shouldEqual true + +[] +let ``octet-stream response produces Task return type``() = + let types = compileTaskSchema octetStreamResponseSchema + let method = (findMethod types "DownloadFile").Value + method.ReturnType.IsGenericType |> shouldEqual true + + method.ReturnType.GetGenericTypeDefinition() + |> shouldEqual typedefof> + + method.ReturnType.GetGenericArguments()[0] + |> shouldEqual typeof + +[] +let ``octet-stream response has CancellationToken as its only parameter``() = + let types = compileTaskSchema octetStreamResponseSchema + let method = (findMethod types "DownloadFile").Value + let parameters = method.GetParameters() + parameters.Length |> shouldEqual 1 + parameters[0].ParameterType |> shouldEqual typeof + +// ── Path-level parameters (inherited from PathItem) ─────────────────────────── + +let private pathLevelParamSchema = + """openapi: "3.0.0" +info: + title: PathLevelParamTest + version: "1.0.0" +paths: + /users/{userId}/posts: + parameters: + - name: userId + in: path + required: true + schema: + type: integer + get: + operationId: getUserPosts + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + type: string + post: + operationId: createUserPost + requestBody: + required: true + content: + application/json: + schema: + type: string + responses: + "201": + description: Created +components: + schemas: {} +""" + +[] +let ``path-level parameter is inherited by GET operation``() = + let types = compileTaskSchema pathLevelParamSchema + let method = (findMethod types "GetUserPosts").Value + let paramNames = method.GetParameters() |> Array.map(fun p -> p.Name) + paramNames |> shouldContain "userId" + +[] +let ``path-level parameter is required with correct type``() = + let types = compileTaskSchema pathLevelParamSchema + let method = (findMethod types "GetUserPosts").Value + let userIdParam = method.GetParameters() |> Array.find(fun p -> p.Name = "userId") + userIdParam.ParameterType |> shouldEqual typeof + userIdParam.IsOptional |> shouldEqual false + +[] +let ``path-level parameter is inherited by POST operation``() = + let types = compileTaskSchema pathLevelParamSchema + let method = (findMethod types "CreateUserPost").Value + let paramNames = method.GetParameters() |> Array.map(fun p -> p.Name) + paramNames |> shouldContain "userId"