Skip to content
Merged
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
170 changes: 170 additions & 0 deletions WoofWare.Myriad.Plugins.Test/TestSwagger/TestSuccessResponse.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
namespace WoofWare.Myriad.Plugins.Test

open FsCheck
open FsCheck.FSharp
open NUnit.Framework
open FsUnitTyped
open WoofWare.Myriad.Plugins
open WoofWare.Myriad.Plugins.SwaggerV2

[<TestFixture>]
module TestSuccessResponse =

/// Distinct definitions, so that we can tell which one came back.
let private defnGen : Gen<Definition> =
[
Definition.String
Definition.Boolean
Definition.Unspecified
Definition.File
Definition.Handle "#/definitions/Error"
Definition.Integer (Some "int64")
]
|> Gen.elements

let private successCodeGen : Gen<ResponseKey> =
Gen.choose (200, 299) |> Gen.map ResponseKey.Code

/// Any key which is neither an explicit 2xx code nor `default`.
let private errorCodeGen : Gen<ResponseKey> =
Gen.oneof [ Gen.choose (100, 199) ; Gen.choose (300, 599) ]
|> Gen.map ResponseKey.Code

let private mapGen (keys : Gen<ResponseKey>) : Gen<Map<ResponseKey, Definition>> =
gen {
let! keys = Gen.listOf keys
let keys = List.distinct keys
let! values = Gen.listOfLength keys.Length defnGen
return List.zip keys values |> Map.ofList
}

/// An arbitrary response map: some 2xx codes, some error codes, maybe a `default`.
let private responsesGen : Gen<Map<ResponseKey, Definition>> =
gen {
let! successes = mapGen successCodeGen
let! errors = mapGen errorCodeGen
let! deflt = Gen.optionOf defnGen

let deflt =
match deflt with
| None -> Map.empty
| Some d -> Map.ofList [ ResponseKey.Default, d ]

return
Seq.concat [ Map.toSeq successes ; Map.toSeq errors ; Map.toSeq deflt ]
|> Map.ofSeq
}

[<Test>]
let ``an Exactly result is always one of the declared schemas`` () : unit =
let property (responses : Map<ResponseKey, Definition>) : bool =
match SwaggerClientGenerator.successResponse responses with
| SuccessResponse.Exactly defn -> responses |> Map.exists (fun _ v -> v = defn)
| SuccessResponse.Ambiguous
| SuccessResponse.Missing -> true

Prop.forAll (Arb.fromGen responsesGen) property |> Check.QuickThrowOnFailure

[<Test>]
let ``responses for non-2xx codes never affect the result`` () : unit =
let property (responses : Map<ResponseKey, Definition>, errors : Map<ResponseKey, Definition>) : bool =
let withoutErrors =
responses
|> Map.filter (fun k _ ->
match k with
| ResponseKey.Code code -> 200 <= code && code < 300
| ResponseKey.Default -> true
)

let withErrors =
(withoutErrors, Map.toSeq errors)
||> Seq.fold (fun acc (k, v) -> Map.add k v acc)

SwaggerClientGenerator.successResponse withErrors = SwaggerClientGenerator.successResponse withoutErrors

Prop.forAll (Arb.fromGen (Gen.zip responsesGen (mapGen errorCodeGen))) property
|> Check.QuickThrowOnFailure

[<Test>]
let ``the default response is ignored whenever an explicit 2xx response exists`` () : unit =
let property (responses : Map<ResponseKey, Definition>, deflt : Definition) : bool =
let hasSuccess =
responses
|> Map.exists (fun k _ ->
match k with
| ResponseKey.Code code -> 200 <= code && code < 300
| ResponseKey.Default -> false
)

if not hasSuccess then
true
else

let withoutDefault = Map.remove ResponseKey.Default responses
let withDefault = Map.add ResponseKey.Default deflt responses

SwaggerClientGenerator.successResponse withDefault = SwaggerClientGenerator.successResponse withoutDefault

Prop.forAll (Arb.fromGen (Gen.zip responsesGen defnGen)) property
|> Check.QuickThrowOnFailure

[<Test>]
let ``the default response is the return type whenever no explicit 2xx response exists`` () : unit =
let property (errors : Map<ResponseKey, Definition>, deflt : Definition) : bool =
let responses = Map.add ResponseKey.Default deflt errors

SwaggerClientGenerator.successResponse responses = SuccessResponse.Exactly deflt

Prop.forAll (Arb.fromGen (Gen.zip (mapGen errorCodeGen) defnGen)) property
|> Check.QuickThrowOnFailure

[<Test>]
let ``exactly one 2xx response is that response`` () : unit =
[
ResponseKey.Code 200, Definition.String
ResponseKey.Default, Definition.Handle "#/definitions/Error"
]
|> Map.ofList
|> SwaggerClientGenerator.successResponse
|> shouldEqual (SuccessResponse.Exactly Definition.String)

[<Test>]
let ``a default-only response map falls back to the default`` () : unit =
[ ResponseKey.Default, Definition.Handle "#/definitions/Error" ]
|> Map.ofList
|> SwaggerClientGenerator.successResponse
|> shouldEqual (SuccessResponse.Exactly (Definition.Handle "#/definitions/Error"))

[<Test>]
let ``errors plus a default falls back to the default`` () : unit =
[
ResponseKey.Code 404, Definition.Unspecified
ResponseKey.Default, Definition.Handle "#/definitions/Error"
]
|> Map.ofList
|> SwaggerClientGenerator.successResponse
|> shouldEqual (SuccessResponse.Exactly (Definition.Handle "#/definitions/Error"))

[<Test>]
let ``no success response and no default is Missing`` () : unit =
[ ResponseKey.Code 404, Definition.Unspecified ]
|> Map.ofList
|> SwaggerClientGenerator.successResponse
|> shouldEqual SuccessResponse.Missing

[<Test>]
let ``an empty response map is Missing`` () : unit =
Map.empty
|> SwaggerClientGenerator.successResponse
|> shouldEqual SuccessResponse.Missing

[<Test>]
let ``multiple 2xx responses are Ambiguous`` () : unit =
[
ResponseKey.Code 200, Definition.String
ResponseKey.Code 201, Definition.Boolean
ResponseKey.Default, Definition.Handle "#/definitions/Error"
]
|> Map.ofList
|> SwaggerClientGenerator.successResponse
|> shouldEqual SuccessResponse.Ambiguous
48 changes: 43 additions & 5 deletions WoofWare.Myriad.Plugins.Test/TestSwagger/TestSwaggerParse.fs
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ module TestSwaggerParse =
|> Some
Responses =
[
204, Definition.Unspecified
303, Definition.Unspecified
404, Definition.Unspecified
ResponseKey.Code 204, Definition.Unspecified
ResponseKey.Code 303, Definition.Unspecified
ResponseKey.Code 404, Definition.Unspecified
]
|> Map.ofList
}
Expand Down Expand Up @@ -118,12 +118,50 @@ module TestSwaggerParse =
endpoint.Responses
|> shouldEqual (
[
200,
ResponseKey.Code 200,
Definition.Array
{
Items = Definition.String
}
403, Definition.Handle "#/responses/forbidden"
ResponseKey.Code 403, Definition.Handle "#/responses/forbidden"
]
|> Map.ofList
)

[<Test>]
let ``Can parse a default response`` () : unit =
let s =
"""{
"tags": [
"pet"
],
"summary": "Returns all pets from the system that the user has access to",
"operationId": "findPets",
"responses": {
"200": {
"description": "pet response",
"schema": {
"type": "string"
}
},
"default": {
"description": "unexpected error",
"schema": {
"$ref": "#/definitions/Error"
}
}
}
}
"""
|> JsonNode.Parse

let endpoint = s.AsObject () |> SwaggerEndpoint.Parse

endpoint.Responses
|> shouldEqual (
[
ResponseKey.Code 200, Definition.String
ResponseKey.Default, Definition.Handle "#/definitions/Error"
]
|> Map.ofList
)
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
<Compile Include="TestArgParser\TestArgParser.fs" />
<Compile Include="TestArgParser\TestArgParserNegation.fs" />
<Compile Include="TestSwagger\TestSwaggerParse.fs" />
<Compile Include="TestSwagger\TestSuccessResponse.fs" />
<Compile Include="TestSwagger\TestOpenApi3Parse.fs" />
<EmbeddedResource Include="TestSwagger\api-with-examples.json" />
<EmbeddedResource Include="TestSwagger\callback-example.json" />
Expand Down
58 changes: 44 additions & 14 deletions WoofWare.Myriad.Plugins/SwaggerClientGenerator.fs
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,50 @@ type internal Types =
ByDefinition : IReadOnlyDictionary<SwaggerV2.Definition, TypeEntry>
}

/// What an endpoint's "responses" object tells us about the body of a successful response.
[<RequireQualifiedAccess>]
type internal SuccessResponse =
/// Exactly one schema can describe the body of a successful response.
| Exactly of SwaggerV2.Definition
/// Several 2xx statuses are declared, and we don't know which one to return.
| Ambiguous
/// Nothing the endpoint declares could describe the body of a successful response.
| Missing

[<RequireQualifiedAccess>]
module internal SwaggerClientGenerator =

let internal log (_ : string) = ()

/// Determine the schema of the body we expect back from a successful call to an endpoint.
let successResponse (responses : Map<SwaggerV2.ResponseKey, SwaggerV2.Definition>) : SuccessResponse =
let successes =
responses
|> Seq.choose (fun (KeyValue (key, defn)) ->
match key with
| SwaggerV2.ResponseKey.Code code when 200 <= code && code < 300 -> Some defn
| SwaggerV2.ResponseKey.Code _
| SwaggerV2.ResponseKey.Default -> None
)
|> Seq.toList

match successes with
| [ defn ] ->
// Strictly, "default" also describes any 2xx status we haven't declared, so a server
// answering with an undeclared 201 here would be described by "default", not by `defn`.
// We assume the server honours the single 2xx status it declares. The alternative,
// calling this shape ambiguous, would discard the return type of nearly every real
// spec, since {200: T, default: Error} is the idiomatic way to write an endpoint.
SuccessResponse.Exactly defn
| [] ->
// The "default" response describes every status code not explicitly listed,
// which includes any undeclared 2xx status: so with no explicit 2xx response,
// it's the only description of a success body we have.
match Map.tryFind SwaggerV2.ResponseKey.Default responses with
| Some defn -> SuccessResponse.Exactly defn
| None -> SuccessResponse.Missing
| _ :: _ :: _ -> SuccessResponse.Ambiguous

let renderType (types : Types) (defn : SwaggerV2.Definition) : SynType option =
match types.ByDefinition.TryGetValue defn with
| true, v -> Some v.Signature
Expand Down Expand Up @@ -653,20 +692,11 @@ module internal SwaggerV2Generator =
failwith $"we don't support multiple Produces right now, at %s{path} (%O{method})"

let returnType =
endpoint.Responses
|> Seq.choose (fun (KeyValue (response, defn)) ->
if 200 <= response && response < 300 then
Some defn
else
None
)
|> Seq.toList

let returnType =
match returnType with
| [ t ] -> Some t
| [] -> failwith $"got no successful response results, %s{path} %O{method}"
| _ ->
match SwaggerClientGenerator.successResponse endpoint.Responses with
| SuccessResponse.Exactly t -> Some t
| SuccessResponse.Missing ->
failwith $"got no successful response results, %s{path} %O{method}"
| SuccessResponse.Ambiguous ->
SwaggerClientGenerator.log
$"Ignoring %s{path} %O{method} due to multiple success responses"
// can't be bothered to work out how to deal with multiple success
Expand Down
24 changes: 20 additions & 4 deletions WoofWare.Myriad.Plugins/SwaggerV2.fs
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,22 @@ type Response =
Schema = schema
}

/// Key into an endpoint's "responses" map: either a specific HTTP status code,
/// or the catch-all "default", which describes all responses whose status codes
/// are not otherwise listed.
type ResponseKey =
/// A specific HTTP status code, e.g. 200.
| Code of int
/// The catch-all "default" response.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

| Default

/// Parse a key of the "responses" object, e.g. "200" or "default".
static member Parse (s : string) : ResponseKey =
if s = "default" then
ResponseKey.Default
else
ResponseKey.Code (Int32.Parse s)

/// An "endpoint" is basically a single HTTP verb, applied to some path.
type SwaggerEndpoint =
{
Expand All @@ -350,9 +366,9 @@ type SwaggerEndpoint =
/// (Each parameter knows how it needs to be supplied: e.g. if it's a query parameter or
/// if it's interpolated into the path.)
Parameters : SwaggerParameter list option
/// Map of HTTP response code to the type that we expect to receive in the body if we
/// get that response code back.
Responses : Map<int, Definition>
/// Map of HTTP response code (or the catch-all "default") to the type that we
/// expect to receive in the body if we get that response code back.
Responses : Map<ResponseKey, Definition>
}

/// Render a JsonObject into this strongly-typed specification.
Expand All @@ -375,7 +391,7 @@ type SwaggerEndpoint =
| Some _ -> Definition.Parse value
| None -> (Response.Parse value).Schema

Int32.Parse key, defn
ResponseKey.Parse key, defn
)
|> Map.ofSeq

Expand Down
Loading