Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions src/SwaggerProvider.DesignTime/v2/OperationCompiler.fs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ type OperationCompiler(schema: SwaggerObject, defCompiler: DefinitionCompiler, i
// reverse it again so that all required properties come first
|> List.rev

// Append an optional CancellationToken parameter last (after all OpenAPI params).
// Using a UniqueNameGenerator avoids collisions with existing parameter names.
let ctArgIndex = List.length parameters

let parameters =
let scope = UniqueNameGenerator()
parameters |> List.iter(fun p -> scope.MakeUnique p.Name |> ignore)
let ctName = scope.MakeUnique "cancellationToken"

parameters
@ [ ProvidedParameter(ctName, typeof<Threading.CancellationToken>, false, null) ]

// find the inner type value
let retTy =
let okResponse =
Expand Down Expand Up @@ -110,9 +122,15 @@ type OperationCompiler(schema: SwaggerObject, defCompiler: DefinitionCompiler, i
"Content-Type", MediaTypes.ApplicationJson |]
@>

// Locates parameters matching the arguments
// Extract CancellationToken (appended at ctArgIndex) and separate from OpenAPI params.
let allArgs = List.tail args // skip `this` param
let ct = List.item ctArgIndex allArgs |> Expr.Cast<Threading.CancellationToken>

// Locates parameters matching the arguments (excluding CT arg)
let parameters =
List.tail args // skip `this` param
allArgs
|> List.indexed
|> List.choose(fun (i, arg) -> if i = ctArgIndex then None else Some arg)
|> List.map (function
| ShapeVar sVar as expr ->
let param =
Expand Down Expand Up @@ -212,11 +230,7 @@ type OperationCompiler(schema: SwaggerObject, defCompiler: DefinitionCompiler, i
<@
let msg = %httpRequestMessageWithPayload
RuntimeHelpers.fillHeaders msg %heads

task {
let! response = (%this).HttpClient.SendAsync(msg)
return response.EnsureSuccessStatusCode().Content
}
(%this).CallAsync(msg, [||], [||], %ct)
@>

let responseObj =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="v2\Swashbuckle.ReturnControllers.Tests.fs" />
<Compile Include="v2\Swashbuckle.CancellationToken.Tests.fs" />
<Compile Include="v2\Swashbuckle.UpdateControllers.Tests.fs" />
<Compile Include="v2\Swashbuckle.ResourceControllers.Tests.fs" />
<Compile Include="v2\Swashbuckle.NoContentControllers.Tests.fs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
module Swashbuckle.v2.CancellationTokenTests

open Xunit
open FsUnitTyped
open System
open System.Net.Http
open System.Threading
open SwaggerProvider
open Swashbuckle.v2.ReturnControllersTests

type WebAPIAsync =
SwaggerClientProvider<"http://localhost:5000/swagger/v1/swagger.json", IgnoreOperationId=true, SsrfProtection=false, PreferAsync=true>

let apiAsync =
let handler = new HttpClientHandler(UseCookies = false)

let client =
new HttpClient(handler, true, BaseAddress = Uri("http://localhost:5000"))

WebAPIAsync.Client(client)

[<Fact>]
let ``v2 Call generated method without CancellationToken uses default token``() =
task {
let! result = api.GetApiReturnBoolean()
result |> shouldEqual true
}

[<Fact>]
let ``v2 Call generated method with explicit CancellationToken None``() =
task {
let! result = api.GetApiReturnBoolean(CancellationToken.None)
result |> shouldEqual true
}

[<Fact>]
let ``v2 Call generated method with valid CancellationTokenSource token``() =
task {
use cts = new CancellationTokenSource()
let! result = api.GetApiReturnInt32(cts.Token)
result |> shouldEqual 42
}

[<Fact>]
let ``v2 Call generated method with already-cancelled token raises OperationCanceledException``() =
task {
use cts = new CancellationTokenSource()
cts.Cancel()

try
let! _ = api.GetApiReturnString(cts.Token)
failwith "Expected OperationCanceledException"
with
| :? OperationCanceledException -> ()
| :? System.AggregateException as aex when (aex.InnerException :? OperationCanceledException) -> ()
}

[<Fact>]
let ``v2 Call POST generated method with explicit CancellationToken None``() =
task {
let! result = api.PostApiReturnString(CancellationToken.None)
result |> shouldEqual "Hello world"
}

[<Fact>]
let ``v2 Call async generated method without CancellationToken uses default token``() =
async {
let! result = apiAsync.GetApiReturnBoolean()
result |> shouldEqual true
}
|> Async.StartAsTask

[<Fact>]
let ``v2 Call method with required param and explicit CancellationToken``() =
task {
use cts = new CancellationTokenSource()
let! result = api.GetApiUpdateString("Serge", cts.Token)
result |> shouldEqual "Hello, Serge"
}

[<Fact>]
let ``v2 Call method with optional param and explicit CancellationToken``() =
task {
use cts = new CancellationTokenSource()
let! result = api.GetApiUpdateBool(Some true, cts.Token)
result |> shouldEqual false
}

[<Fact>]
let ``v2 Call async generated method with explicit CancellationToken``() =
async {
use cts = new CancellationTokenSource()
let! result = apiAsync.GetApiReturnInt32(cts.Token)
result |> shouldEqual 42
}
|> Async.StartAsTask

[<Fact>]
let ``v2 Call async generated method with already-cancelled token raises OperationCanceledException``() =
async {
use cts = new CancellationTokenSource()
cts.Cancel()

try
let! _ = apiAsync.GetApiReturnString(cts.Token)
failwith "Expected OperationCanceledException"
with
| :? OperationCanceledException -> ()
| :? AggregateException as aex when (aex.InnerException :? OperationCanceledException) -> ()
}
|> Async.StartAsTask

[<Fact>]
let ``v2 Call async POST generated method with explicit CancellationToken``() =
async {
use cts = new CancellationTokenSource()
let! result = apiAsync.PostApiReturnString(cts.Token)
result |> shouldEqual "Hello world"
}
|> Async.StartAsTask
Loading