Skip to content

Commit 7a92599

Browse files
committed
Merge branch 'master' into feat/deps
2 parents 562ece6 + 2d6f5c7 commit 7a92599

File tree

8 files changed

+90
-3
lines changed

8 files changed

+90
-3
lines changed

docs/RELEASE_NOTES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
- Removed dependency on Microsoft.OpenApi.Readers
55
- Dependencies update
66

7+
#### 2.3.2 - Oct 11, 2025
8+
9+
- Added support for `text/*` media type (#270) by @spaasis
10+
711
#### 2.3.1 - May 13, 2025
812

913
- Performance improvement: ProvidedTypes update to the most recent version (#267)

src/SwaggerProvider.DesignTime/v3/OperationCompiler.fs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ type OperationCompiler(schema: OpenApiDocument, defCompiler: DefinitionCompiler,
7777
| true, mediaTyObj -> Some mediaTyObj
7878
| _ -> None
7979

80+
let (|TextReturn|_|)(input: string) =
81+
if input.StartsWith("text/") then Some(input) else None
82+
83+
let (|TextMediaType|_|)(content: IDictionary<string, OpenApiMediaType>) =
84+
content.Keys |> Seq.tryPick (|TextReturn|_|)
85+
8086
let (|NoMediaType|_|)(content: IDictionary<string, OpenApiMediaType>) =
8187
if content.Count = 0 then Some() else None
8288

@@ -181,6 +187,7 @@ type OperationCompiler(schema: OpenApiDocument, defCompiler: DefinitionCompiler,
181187
defCompiler.CompileTy providedMethodName "Response" mediaTy.Schema true
182188

183189
Some(MediaTypes.ApplicationOctetStream, ty)
190+
| TextMediaType mediaTy -> Some(mediaTy, typeof<string>)
184191
| _ -> None)
185192

186193
let retMime = retMimeAndTy |> Option.map fst |> Option.defaultValue null
@@ -378,6 +385,17 @@ type OperationCompiler(schema: OpenApiDocument, defCompiler: DefinitionCompiler,
378385
}
379386
@>
380387

388+
let responseString =
389+
<@
390+
let x = %action
391+
392+
task {
393+
let! response = x
394+
let! data = response.ReadAsStringAsync()
395+
return data
396+
}
397+
@>
398+
381399
let responseUnit =
382400
<@
383401
let x = %action
@@ -394,15 +412,21 @@ type OperationCompiler(schema: OpenApiDocument, defCompiler: DefinitionCompiler,
394412
match retTy with
395413
| None -> responseUnit.Raw
396414
| Some t when t = typeof<IO.Stream> -> <@ %responseStream @>.Raw
397-
| Some t -> Expr.Coerce(<@ RuntimeHelpers.taskCast t %responseObj @>, overallReturnType)
415+
| Some t ->
416+
match retMime with
417+
| TextReturn _ -> <@ %responseString @>.Raw
418+
| _ -> Expr.Coerce(<@ RuntimeHelpers.taskCast t %responseObj @>, overallReturnType)
398419
else
399420
let awaitTask t =
400421
<@ Async.AwaitTask(%t) @>
401422

402423
match retTy with
403424
| None -> (awaitTask responseUnit).Raw
404425
| Some t when t = typeof<IO.Stream> -> <@ %(awaitTask responseStream) @>.Raw
405-
| Some t -> Expr.Coerce(<@ RuntimeHelpers.asyncCast t %(awaitTask responseObj) @>, overallReturnType)
426+
| Some t ->
427+
match retMime with
428+
| TextReturn _ -> <@ %(awaitTask responseString) @>.Raw
429+
| _ -> Expr.Coerce(<@ RuntimeHelpers.asyncCast t %(awaitTask responseObj) @>, overallReturnType)
406430
)
407431

408432
if not <| String.IsNullOrEmpty(operation.Summary) then

src/SwaggerProvider.Runtime/RuntimeHelpers.fs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ module MediaTypes =
1818
[<Literal>]
1919
let MultipartFormData = "multipart/form-data"
2020

21-
2221
type AsyncExtensions() =
2322
static member cast<'t> asyncOp =
2423
async {

tests/SwaggerProvider.ProviderTests/SwaggerProvider.ProviderTests.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
<Compile Include="v3\Swagger.I0181.Tests.fs" />
2727
<Compile Include="v3\Swagger.I0219.Tests.fs" />
2828
<Compile Include="v3\Swashbuckle.ReturnControllers.Tests.fs" />
29+
<Compile Include="v3\Swashbuckle.ReturnTextControllers.Tests.fs" />
2930
<Compile Include="v3\Swashbuckle.UpdateControllers.Tests.fs" />
3031
<Compile Include="v3\Swashbuckle.ResourceControllers.Tests.fs" />
3132
<Compile Include="v3\Swashbuckle.FileController.Tests.fs" />
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module Swashbuckle.v3.ReturnTextControllersTests
2+
3+
open Xunit
4+
open FsUnitTyped
5+
open SwaggerProvider
6+
open System
7+
open System.Net.Http
8+
9+
open Swashbuckle.v3.ReturnControllersTests
10+
11+
[<Fact>]
12+
let ``Return text/plain GET Test``() =
13+
api.GetApiReturnPlain() |> asyncEqual "Hello world"
14+
15+
[<Fact>]
16+
let ``Return text/csv GET Test``() =
17+
api.GetApiReturnCsv() |> asyncEqual "Hello,world"
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
namespace Swashbuckle.WebApi.Server.Controllers
2+
3+
open System.Text
4+
open Microsoft.AspNetCore.Mvc
5+
open Microsoft.AspNetCore.Mvc.Formatters
6+
open Swagger.Internal
7+
8+
[<Route("api/[controller]")>]
9+
[<ApiController>]
10+
type ReturnPlainController() =
11+
[<HttpGet; Produces("text/plain")>]
12+
member this.Get() =
13+
"Hello world" |> ActionResult<string>
14+
15+
[<Route("api/[controller]")>]
16+
[<ApiController>]
17+
type ReturnCsvController() =
18+
[<HttpGet; Produces("text/csv")>]
19+
member this.Get() =
20+
"Hello,world" |> ActionResult<string>
21+
22+
// Simple CSV output formatter
23+
// This formatter assumes the controller returns a string (already CSV-formatted)
24+
type CsvOutputFormatter() as this =
25+
inherit TextOutputFormatter()
26+
27+
do
28+
this.SupportedMediaTypes.Add("text/csv")
29+
this.SupportedEncodings.Add(Encoding.UTF8)
30+
this.SupportedEncodings.Add(Encoding.Unicode)
31+
32+
override _.CanWriteType(t) =
33+
// Accept string type only (for simplicity)
34+
t = typeof<string>
35+
36+
override _.WriteResponseBodyAsync(context, encoding) =
37+
let response = context.HttpContext.Response
38+
let value = context.Object :?> string
39+
let bytes = encoding.GetBytes(value)
40+
response.Body.WriteAsync(bytes, 0, bytes.Length)

tests/Swashbuckle.WebApi.Server/Startup.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type Startup private () =
2424
let converters = options.JsonSerializerOptions.Converters
2525
converters.Add(JsonFSharpConverter())
2626
converters.Add(JsonStringEnumConverter()))
27+
.AddMvcOptions(_.OutputFormatters.Add(CsvOutputFormatter()))
2728
|> ignore
2829

2930
// Register the Swagger & OpenApi services

tests/Swashbuckle.WebApi.Server/Swashbuckle.WebApi.Server.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<ItemGroup>
1010
<Compile Include="Controllers\Types.fs" />
1111
<Compile Include="Controllers\ReturnControllers.fs" />
12+
<Compile Include="Controllers\ReturnTextControllers.fs" />
1213
<Compile Include="Controllers\UpdateControllers.fs" />
1314
<Compile Include="Controllers\ResourceControllers.fs" />
1415
<Compile Include="Controllers\ValuesController.fs" />

0 commit comments

Comments
 (0)