Skip to content

Commit a1a4eba

Browse files
spaasissergey-tihonCopilot
authored
fear: add support for text/* media type (#270)
* fixes: #268 * fix: do not compile client twice * Active pattern * an attempt was made * clean * hk: cleanup * fix: add csv output formatter * Update tests/Swashbuckle.WebApi.Server/Controllers/ReturnTextControllers.fs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update tests/SwaggerProvider.ProviderTests/v3/Swashbuckle.ReturnTextControllers.Tests.fs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Sergey Tihon <sergey.tihon@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent db68054 commit a1a4eba

File tree

7 files changed

+86
-3
lines changed

7 files changed

+86
-3
lines changed

src/SwaggerProvider.DesignTime/v3/OperationCompiler.fs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ type OperationCompiler(schema: OpenApiDocument, defCompiler: DefinitionCompiler,
7373
| true, mediaTyObj -> Some(mediaTyObj)
7474
| _ -> None
7575

76+
let (|TextReturn|_|)(input: string) =
77+
if input.StartsWith("text/") then Some(input) else None
78+
79+
let (|TextMediaType|_|)(content: IDictionary<string, OpenApiMediaType>) =
80+
content.Keys |> Seq.tryPick (|TextReturn|_|)
81+
7682
let (|NoMediaType|_|)(content: IDictionary<string, OpenApiMediaType>) =
7783
if content.Count = 0 then Some() else None
7884

@@ -176,6 +182,7 @@ type OperationCompiler(schema: OpenApiDocument, defCompiler: DefinitionCompiler,
176182
defCompiler.CompileTy providedMethodName "Response" mediaTy.Schema true
177183

178184
Some(MediaTypes.ApplicationOctetStream, ty)
185+
| TextMediaType mediaTy -> Some(mediaTy, typeof<string>)
179186
| _ -> None)
180187

181188
let retMime = retMimeAndTy |> Option.map fst |> Option.defaultValue null
@@ -373,6 +380,17 @@ type OperationCompiler(schema: OpenApiDocument, defCompiler: DefinitionCompiler,
373380
}
374381
@>
375382

383+
let responseString =
384+
<@
385+
let x = %action
386+
387+
task {
388+
let! response = x
389+
let! data = response.ReadAsStringAsync()
390+
return data
391+
}
392+
@>
393+
376394
let responseUnit =
377395
<@
378396
let x = %action
@@ -389,15 +407,21 @@ type OperationCompiler(schema: OpenApiDocument, defCompiler: DefinitionCompiler,
389407
match retTy with
390408
| None -> responseUnit.Raw
391409
| Some t when t = typeof<IO.Stream> -> <@ %responseStream @>.Raw
392-
| Some t -> Expr.Coerce(<@ RuntimeHelpers.taskCast t %responseObj @>, overallReturnType)
410+
| Some t ->
411+
match retMime with
412+
| TextReturn _ -> <@ %responseString @>.Raw
413+
| _ -> Expr.Coerce(<@ RuntimeHelpers.taskCast t %responseObj @>, overallReturnType)
393414
else
394415
let awaitTask t =
395416
<@ Async.AwaitTask(%t) @>
396417

397418
match retTy with
398419
| None -> (awaitTask responseUnit).Raw
399420
| Some t when t = typeof<IO.Stream> -> <@ %(awaitTask responseStream) @>.Raw
400-
| Some t -> Expr.Coerce(<@ RuntimeHelpers.asyncCast t %(awaitTask responseObj) @>, overallReturnType)
421+
| Some t ->
422+
match retMime with
423+
| TextReturn _ -> <@ %(awaitTask responseString) @>.Raw
424+
| _ -> Expr.Coerce(<@ RuntimeHelpers.asyncCast t %(awaitTask responseObj) @>, overallReturnType)
401425
)
402426

403427
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
// Register the Swagger & OpenApi services
2930
services.AddSwaggerGen(fun c ->

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)