From 4236b26189f164405eda6323627797708d1f58ae Mon Sep 17 00:00:00 2001 From: Patrick Stevens <3138005+Smaug123@users.noreply.github.com> Date: Wed, 18 Jun 2025 19:00:56 +0100 Subject: [PATCH] Parse OpenAPI 3 files (#392) --- README.md | 4 + WoofWare.Myriad.Plugins.Test/Assembly.fs | 22 + .../TestSwagger/TestOpenApi3Parse.fs | 3503 +++++++++++++++++ .../TestSwagger/api-with-examples.json | 192 + .../TestSwagger/callback-example.json | 82 + .../TestSwagger/link-example.json | 319 ++ .../TestSwagger/non-oauth-scopes.json | 28 + .../TestSwagger/petstore-expanded.json | 235 ++ .../TestSwagger/petstore.json | 177 + .../TestSwagger/tictactoe.json | 261 ++ .../TestSwagger/uspto.json | 241 ++ .../TestSwagger/webhook-example.json | 47 + .../WoofWare.Myriad.Plugins.Test.fsproj | 11 + WoofWare.Myriad.Plugins/OpenApi3.fs | 870 +++- 14 files changed, 5971 insertions(+), 21 deletions(-) create mode 100644 WoofWare.Myriad.Plugins.Test/Assembly.fs create mode 100644 WoofWare.Myriad.Plugins.Test/TestSwagger/TestOpenApi3Parse.fs create mode 100644 WoofWare.Myriad.Plugins.Test/TestSwagger/api-with-examples.json create mode 100644 WoofWare.Myriad.Plugins.Test/TestSwagger/callback-example.json create mode 100644 WoofWare.Myriad.Plugins.Test/TestSwagger/link-example.json create mode 100644 WoofWare.Myriad.Plugins.Test/TestSwagger/non-oauth-scopes.json create mode 100644 WoofWare.Myriad.Plugins.Test/TestSwagger/petstore-expanded.json create mode 100644 WoofWare.Myriad.Plugins.Test/TestSwagger/petstore.json create mode 100644 WoofWare.Myriad.Plugins.Test/TestSwagger/tictactoe.json create mode 100644 WoofWare.Myriad.Plugins.Test/TestSwagger/uspto.json create mode 100644 WoofWare.Myriad.Plugins.Test/TestSwagger/webhook-example.json diff --git a/README.md b/README.md index 9058354..f6f2564 100644 --- a/README.md +++ b/README.md @@ -647,3 +647,7 @@ I'm hopefully going to get round to writing a more powerful source generation sy You should probably add these files to your [fantomasignore](https://github.com/fsprojects/fantomas/blob/a999b77ca5a024fbc3409955faac797e29b39d27/docs/docs/end-users/IgnoreFiles.md) if you use Fantomas to format your repo; the alternative is to manually reformat every time Myriad changes the generated files. + +# Licence + +The code is MIT-licenced, except for the Swagger API examples in WoofWare.Myriad.Plugins.Test, which are [CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/), copyright 2023 by the OpenAPI Initiative, and obtained from https://learn.openapis.org/examples/ with no changes made. diff --git a/WoofWare.Myriad.Plugins.Test/Assembly.fs b/WoofWare.Myriad.Plugins.Test/Assembly.fs new file mode 100644 index 0000000..f5861b4 --- /dev/null +++ b/WoofWare.Myriad.Plugins.Test/Assembly.fs @@ -0,0 +1,22 @@ +namespace WoofWare.Myriad.Plugins.Test + +open System +open System.IO +open System.Reflection + +[] +module Assembly = + + let getEmbeddedResource (assembly : Assembly) (name : string) : string = + let names = assembly.GetManifestResourceNames () + + let names = + names |> Seq.filter (fun s -> s.EndsWith (name, StringComparison.Ordinal)) + + use s = + names + |> Seq.exactlyOne + |> assembly.GetManifestResourceStream + |> fun s -> new StreamReader (s) + + s.ReadToEnd () diff --git a/WoofWare.Myriad.Plugins.Test/TestSwagger/TestOpenApi3Parse.fs b/WoofWare.Myriad.Plugins.Test/TestSwagger/TestOpenApi3Parse.fs new file mode 100644 index 0000000..add81e7 --- /dev/null +++ b/WoofWare.Myriad.Plugins.Test/TestSwagger/TestOpenApi3Parse.fs @@ -0,0 +1,3503 @@ +namespace WoofWare.Myriad.Plugins.Test + +open WoofWare.Myriad.Plugins.OpenApi3 +open System.Text.Json.Nodes +open NUnit.Framework +open WoofWare.Expect + +[] +[] +module TestOpenApi3Parse = + [] + let ``Prepare to bulk-update tests`` () = + // GlobalBuilderConfig.enterBulkUpdateMode () + () + + [] + let ``Update all tests`` () = + GlobalBuilderConfig.updateAllSnapshots () + + type Dummy = class end + + [] + let ``API with examples`` () = + let resource = + Assembly.getEmbeddedResource typeof.Assembly "api-with-examples.json" + |> JsonNode.Parse + |> _.AsObject() + + let actual = OpenApiSpec.Parse resource + + expect { + snapshotJson + @"{ + ""OpenApi"": ""3.0.0"", + ""Info"": { + ""Title"": ""Simple API overview"", + ""Description"": null, + ""TermsOfService"": null, + ""Contact"": null, + ""License"": null, + ""Version"": ""2.0.0"" + }, + ""Servers"": null, + ""Paths"": { + ""Fields"": { + ""/"": { + ""Ref"": null, + ""Summary"": null, + ""Description"": null, + ""Get"": { + ""Tags"": null, + ""Summary"": ""List API versions"", + ""Description"": null, + ""ExternalDocs"": null, + ""OperationId"": ""listVersionsv2"", + ""Parameters"": null, + ""RequestBody"": null, + ""Responses"": { + ""Default"": null, + ""Patterns"": { + ""200"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""200 response"", + ""Headers"": null, + ""Content"": { + ""application/json"": { + ""Schema"": null, + ""Example"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""foo"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Summary"": null, + ""Description"": null, + ""Value"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""versions"": [ + { + ""status"": ""CURRENT"", + ""updated"": ""2011-01-21T11:33:21Z"", + ""id"": ""v2.0"", + ""links"": [ + { + ""href"": ""http://127.0.0.1:8774/v2/"", + ""rel"": ""self"" + } + ] + }, + { + ""status"": ""EXPERIMENTAL"", + ""updated"": ""2013-07-23T11:33:21Z"", + ""id"": ""v3.0"", + ""links"": [ + { + ""href"": ""http://127.0.0.1:8774/v3/"", + ""rel"": ""self"" + } + ] + } + ] + } + ] + } + } + ] + } + } + ] + }, + ""Encoding"": null + } + }, + ""Links"": null + } + ] + }, + ""300"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""300 response"", + ""Headers"": null, + ""Content"": { + ""application/json"": { + ""Schema"": null, + ""Example"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""foo"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Summary"": null, + ""Description"": null, + ""Value"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""versions"": [ + { + ""status"": ""CURRENT"", + ""updated"": ""2011-01-21T11:33:21Z"", + ""id"": ""v2.0"", + ""links"": [ + { + ""href"": ""http://127.0.0.1:8774/v2/"", + ""rel"": ""self"" + } + ] + }, + { + ""status"": ""EXPERIMENTAL"", + ""updated"": ""2013-07-23T11:33:21Z"", + ""id"": ""v3.0"", + ""links"": [ + { + ""href"": ""http://127.0.0.1:8774/v3/"", + ""rel"": ""self"" + } + ] + } + ] + } + ] + } + } + ] + } + } + ] + }, + ""Encoding"": null + } + }, + ""Links"": null + } + ] + } + } + }, + ""Callbacks"": null, + ""Deprecated"": null, + ""Security"": null, + ""Servers"": null + }, + ""Put"": null, + ""Post"": null, + ""Delete"": null, + ""Options"": null, + ""Head"": null, + ""Patch"": null, + ""Trace"": null, + ""Servers"": null, + ""Parameters"": null + }, + ""/v2"": { + ""Ref"": null, + ""Summary"": null, + ""Description"": null, + ""Get"": { + ""Tags"": null, + ""Summary"": ""Show API version details"", + ""Description"": null, + ""ExternalDocs"": null, + ""OperationId"": ""getVersionDetailsv2"", + ""Parameters"": null, + ""RequestBody"": null, + ""Responses"": { + ""Default"": null, + ""Patterns"": { + ""200"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""200 response"", + ""Headers"": null, + ""Content"": { + ""application/json"": { + ""Schema"": null, + ""Example"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""foo"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Summary"": null, + ""Description"": null, + ""Value"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""version"": { + ""status"": ""CURRENT"", + ""updated"": ""2011-01-21T11:33:21Z"", + ""media-types"": [ + { + ""base"": ""application/xml"", + ""type"": ""application/vnd.openstack.compute\u002Bxml;version=2"" + }, + { + ""base"": ""application/json"", + ""type"": ""application/vnd.openstack.compute\u002Bjson;version=2"" + } + ], + ""id"": ""v2.0"", + ""links"": [ + { + ""href"": ""http://127.0.0.1:8774/v2/"", + ""rel"": ""self"" + }, + { + ""href"": ""http://docs.openstack.org/api/openstack-compute/2/os-compute-devguide-2.pdf"", + ""type"": ""application/pdf"", + ""rel"": ""describedby"" + }, + { + ""href"": ""http://docs.openstack.org/api/openstack-compute/2/wadl/os-compute-2.wadl"", + ""type"": ""application/vnd.sun.wadl\u002Bxml"", + ""rel"": ""describedby"" + }, + { + ""href"": ""http://docs.openstack.org/api/openstack-compute/2/wadl/os-compute-2.wadl"", + ""type"": ""application/vnd.sun.wadl\u002Bxml"", + ""rel"": ""describedby"" + } + ] + } + } + ] + } + } + ] + } + } + ] + }, + ""Encoding"": null + } + }, + ""Links"": null + } + ] + }, + ""203"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""203 response"", + ""Headers"": null, + ""Content"": { + ""application/json"": { + ""Schema"": null, + ""Example"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""foo"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Summary"": null, + ""Description"": null, + ""Value"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""version"": { + ""status"": ""CURRENT"", + ""updated"": ""2011-01-21T11:33:21Z"", + ""media-types"": [ + { + ""base"": ""application/xml"", + ""type"": ""application/vnd.openstack.compute\u002Bxml;version=2"" + }, + { + ""base"": ""application/json"", + ""type"": ""application/vnd.openstack.compute\u002Bjson;version=2"" + } + ], + ""id"": ""v2.0"", + ""links"": [ + { + ""href"": ""http://23.253.228.211:8774/v2/"", + ""rel"": ""self"" + }, + { + ""href"": ""http://docs.openstack.org/api/openstack-compute/2/os-compute-devguide-2.pdf"", + ""type"": ""application/pdf"", + ""rel"": ""describedby"" + }, + { + ""href"": ""http://docs.openstack.org/api/openstack-compute/2/wadl/os-compute-2.wadl"", + ""type"": ""application/vnd.sun.wadl\u002Bxml"", + ""rel"": ""describedby"" + } + ] + } + } + ] + } + } + ] + } + } + ] + }, + ""Encoding"": null + } + }, + ""Links"": null + } + ] + } + } + }, + ""Callbacks"": null, + ""Deprecated"": null, + ""Security"": null, + ""Servers"": null + }, + ""Put"": null, + ""Post"": null, + ""Delete"": null, + ""Options"": null, + ""Head"": null, + ""Patch"": null, + ""Trace"": null, + ""Servers"": null, + ""Parameters"": null + } + } + }, + ""Components"": null, + ""Security"": null, + ""Tags"": null, + ""ExternalDocs"": null +}" + + return actual + } + + [] + let ``Callback example`` () = + let resource = + Assembly.getEmbeddedResource typeof.Assembly "callback-example.json" + |> JsonNode.Parse + |> _.AsObject() + + let actual = OpenApiSpec.Parse resource + + expect { + snapshotJson + @"{ + ""OpenApi"": ""3.0.0"", + ""Info"": { + ""Title"": ""Callback Example"", + ""Description"": null, + ""TermsOfService"": null, + ""Contact"": null, + ""License"": null, + ""Version"": ""1.0.0"" + }, + ""Servers"": null, + ""Paths"": { + ""Fields"": { + ""/streams"": { + ""Ref"": null, + ""Summary"": null, + ""Description"": null, + ""Get"": null, + ""Put"": null, + ""Post"": { + ""Tags"": null, + ""Summary"": null, + ""Description"": ""subscribes a client to receive out-of-band data"", + ""ExternalDocs"": null, + ""OperationId"": null, + ""Parameters"": [ + { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Name"": ""callbackUrl"", + ""In"": { + ""Case"": ""Query"" + }, + ""Description"": ""the location where data will be sent. Must be network accessible\nby the source server\n"", + ""Required"": true, + ""Deprecated"": null, + ""AllowEmptyValue"": null, + ""Style"": null, + ""Explode"": null, + ""AllowReserved"": null, + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Content"": null + } + ] + } + ], + ""RequestBody"": null, + ""Responses"": { + ""Default"": null, + ""Patterns"": { + ""201"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""subscription successfully created"", + ""Headers"": null, + ""Content"": { + ""application/json"": { + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Encoding"": null + } + }, + ""Links"": null + } + ] + } + } + }, + ""Callbacks"": { + ""onData"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Patterns"": { + ""{$request.query.callbackUrl}/data"": { + ""Ref"": null, + ""Summary"": null, + ""Description"": null, + ""Get"": null, + ""Put"": null, + ""Post"": { + ""Tags"": null, + ""Summary"": null, + ""Description"": null, + ""ExternalDocs"": null, + ""OperationId"": null, + ""Parameters"": null, + ""RequestBody"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""subscription payload"", + ""Content"": { + ""application/json"": { + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Encoding"": null + } + }, + ""Required"": null + } + ] + }, + ""Responses"": { + ""Default"": null, + ""Patterns"": { + ""202"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""Your server implementation should return this HTTP status code\nif the data was received successfully\n"", + ""Headers"": null, + ""Content"": null, + ""Links"": null + } + ] + }, + ""204"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""Your server should return this HTTP status code if no longer interested\nin further updates\n"", + ""Headers"": null, + ""Content"": null, + ""Links"": null + } + ] + } + } + }, + ""Callbacks"": null, + ""Deprecated"": null, + ""Security"": null, + ""Servers"": null + }, + ""Delete"": null, + ""Options"": null, + ""Head"": null, + ""Patch"": null, + ""Trace"": null, + ""Servers"": null, + ""Parameters"": null + } + } + } + ] + } + }, + ""Deprecated"": null, + ""Security"": null, + ""Servers"": null + }, + ""Delete"": null, + ""Options"": null, + ""Head"": null, + ""Patch"": null, + ""Trace"": null, + ""Servers"": null, + ""Parameters"": null + } + } + }, + ""Components"": null, + ""Security"": null, + ""Tags"": null, + ""ExternalDocs"": null +}" + + return actual + } + + [] + let ``Link example`` () = + let resource = + Assembly.getEmbeddedResource typeof.Assembly "link-example.json" + |> JsonNode.Parse + |> _.AsObject() + + let actual = OpenApiSpec.Parse resource + + expect { + snapshotJson + @"{ + ""OpenApi"": ""3.0.0"", + ""Info"": { + ""Title"": ""Link Example"", + ""Description"": null, + ""TermsOfService"": null, + ""Contact"": null, + ""License"": null, + ""Version"": ""1.0.0"" + }, + ""Servers"": null, + ""Paths"": { + ""Fields"": { + ""/2.0/repositories/{username}"": { + ""Ref"": null, + ""Summary"": null, + ""Description"": null, + ""Get"": { + ""Tags"": null, + ""Summary"": null, + ""Description"": null, + ""ExternalDocs"": null, + ""OperationId"": ""getRepositoriesByOwner"", + ""Parameters"": [ + { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Name"": ""username"", + ""In"": { + ""Case"": ""Path"" + }, + ""Description"": null, + ""Required"": true, + ""Deprecated"": null, + ""AllowEmptyValue"": null, + ""Style"": null, + ""Explode"": null, + ""AllowReserved"": null, + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Content"": null + } + ] + } + ], + ""RequestBody"": null, + ""Responses"": { + ""Default"": null, + ""Patterns"": { + ""200"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""repositories owned by the supplied user"", + ""Headers"": null, + ""Content"": { + ""application/json"": { + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Encoding"": null + } + }, + ""Links"": { + ""userRepository"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""Ref"": ""#/components/links/UserRepository"" + } + ] + } + } + } + ] + } + } + }, + ""Callbacks"": null, + ""Deprecated"": null, + ""Security"": null, + ""Servers"": null + }, + ""Put"": null, + ""Post"": null, + ""Delete"": null, + ""Options"": null, + ""Head"": null, + ""Patch"": null, + ""Trace"": null, + ""Servers"": null, + ""Parameters"": null + }, + ""/2.0/repositories/{username}/{slug}"": { + ""Ref"": null, + ""Summary"": null, + ""Description"": null, + ""Get"": { + ""Tags"": null, + ""Summary"": null, + ""Description"": null, + ""ExternalDocs"": null, + ""OperationId"": ""getRepository"", + ""Parameters"": [ + { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Name"": ""username"", + ""In"": { + ""Case"": ""Path"" + }, + ""Description"": null, + ""Required"": true, + ""Deprecated"": null, + ""AllowEmptyValue"": null, + ""Style"": null, + ""Explode"": null, + ""AllowReserved"": null, + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Content"": null + } + ] + }, + { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Name"": ""slug"", + ""In"": { + ""Case"": ""Path"" + }, + ""Description"": null, + ""Required"": true, + ""Deprecated"": null, + ""AllowEmptyValue"": null, + ""Style"": null, + ""Explode"": null, + ""AllowReserved"": null, + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Content"": null + } + ] + } + ], + ""RequestBody"": null, + ""Responses"": { + ""Default"": null, + ""Patterns"": { + ""200"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""The repository"", + ""Headers"": null, + ""Content"": { + ""application/json"": { + ""Schema"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""Ref"": ""#/components/schemas/repository"" + } + ] + }, + ""Example"": null, + ""Encoding"": null + } + }, + ""Links"": { + ""repositoryPullRequests"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""Ref"": ""#/components/links/RepositoryPullRequests"" + } + ] + } + } + } + ] + } + } + }, + ""Callbacks"": null, + ""Deprecated"": null, + ""Security"": null, + ""Servers"": null + }, + ""Put"": null, + ""Post"": null, + ""Delete"": null, + ""Options"": null, + ""Head"": null, + ""Patch"": null, + ""Trace"": null, + ""Servers"": null, + ""Parameters"": null + }, + ""/2.0/repositories/{username}/{slug}/pullrequests"": { + ""Ref"": null, + ""Summary"": null, + ""Description"": null, + ""Get"": { + ""Tags"": null, + ""Summary"": null, + ""Description"": null, + ""ExternalDocs"": null, + ""OperationId"": ""getPullRequestsByRepository"", + ""Parameters"": [ + { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Name"": ""username"", + ""In"": { + ""Case"": ""Path"" + }, + ""Description"": null, + ""Required"": true, + ""Deprecated"": null, + ""AllowEmptyValue"": null, + ""Style"": null, + ""Explode"": null, + ""AllowReserved"": null, + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Content"": null + } + ] + }, + { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Name"": ""slug"", + ""In"": { + ""Case"": ""Path"" + }, + ""Description"": null, + ""Required"": true, + ""Deprecated"": null, + ""AllowEmptyValue"": null, + ""Style"": null, + ""Explode"": null, + ""AllowReserved"": null, + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Content"": null + } + ] + }, + { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Name"": ""state"", + ""In"": { + ""Case"": ""Query"" + }, + ""Description"": null, + ""Required"": null, + ""Deprecated"": null, + ""AllowEmptyValue"": null, + ""Style"": null, + ""Explode"": null, + ""AllowReserved"": null, + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Content"": null + } + ] + } + ], + ""RequestBody"": null, + ""Responses"": { + ""Default"": null, + ""Patterns"": { + ""200"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""an array of pull request objects"", + ""Headers"": null, + ""Content"": { + ""application/json"": { + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Encoding"": null + } + }, + ""Links"": null + } + ] + } + } + }, + ""Callbacks"": null, + ""Deprecated"": null, + ""Security"": null, + ""Servers"": null + }, + ""Put"": null, + ""Post"": null, + ""Delete"": null, + ""Options"": null, + ""Head"": null, + ""Patch"": null, + ""Trace"": null, + ""Servers"": null, + ""Parameters"": null + }, + ""/2.0/repositories/{username}/{slug}/pullrequests/{pid}"": { + ""Ref"": null, + ""Summary"": null, + ""Description"": null, + ""Get"": { + ""Tags"": null, + ""Summary"": null, + ""Description"": null, + ""ExternalDocs"": null, + ""OperationId"": ""getPullRequestsById"", + ""Parameters"": [ + { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Name"": ""username"", + ""In"": { + ""Case"": ""Path"" + }, + ""Description"": null, + ""Required"": true, + ""Deprecated"": null, + ""AllowEmptyValue"": null, + ""Style"": null, + ""Explode"": null, + ""AllowReserved"": null, + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Content"": null + } + ] + }, + { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Name"": ""slug"", + ""In"": { + ""Case"": ""Path"" + }, + ""Description"": null, + ""Required"": true, + ""Deprecated"": null, + ""AllowEmptyValue"": null, + ""Style"": null, + ""Explode"": null, + ""AllowReserved"": null, + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Content"": null + } + ] + }, + { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Name"": ""pid"", + ""In"": { + ""Case"": ""Path"" + }, + ""Description"": null, + ""Required"": true, + ""Deprecated"": null, + ""AllowEmptyValue"": null, + ""Style"": null, + ""Explode"": null, + ""AllowReserved"": null, + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Content"": null + } + ] + } + ], + ""RequestBody"": null, + ""Responses"": { + ""Default"": null, + ""Patterns"": { + ""200"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""a pull request object"", + ""Headers"": null, + ""Content"": { + ""application/json"": { + ""Schema"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""Ref"": ""#/components/schemas/pullrequest"" + } + ] + }, + ""Example"": null, + ""Encoding"": null + } + }, + ""Links"": { + ""pullRequestMerge"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""Ref"": ""#/components/links/PullRequestMerge"" + } + ] + } + } + } + ] + } + } + }, + ""Callbacks"": null, + ""Deprecated"": null, + ""Security"": null, + ""Servers"": null + }, + ""Put"": null, + ""Post"": null, + ""Delete"": null, + ""Options"": null, + ""Head"": null, + ""Patch"": null, + ""Trace"": null, + ""Servers"": null, + ""Parameters"": null + }, + ""/2.0/repositories/{username}/{slug}/pullrequests/{pid}/merge"": { + ""Ref"": null, + ""Summary"": null, + ""Description"": null, + ""Get"": null, + ""Put"": null, + ""Post"": { + ""Tags"": null, + ""Summary"": null, + ""Description"": null, + ""ExternalDocs"": null, + ""OperationId"": ""mergePullRequest"", + ""Parameters"": [ + { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Name"": ""username"", + ""In"": { + ""Case"": ""Path"" + }, + ""Description"": null, + ""Required"": true, + ""Deprecated"": null, + ""AllowEmptyValue"": null, + ""Style"": null, + ""Explode"": null, + ""AllowReserved"": null, + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Content"": null + } + ] + }, + { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Name"": ""slug"", + ""In"": { + ""Case"": ""Path"" + }, + ""Description"": null, + ""Required"": true, + ""Deprecated"": null, + ""AllowEmptyValue"": null, + ""Style"": null, + ""Explode"": null, + ""AllowReserved"": null, + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Content"": null + } + ] + }, + { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Name"": ""pid"", + ""In"": { + ""Case"": ""Path"" + }, + ""Description"": null, + ""Required"": true, + ""Deprecated"": null, + ""AllowEmptyValue"": null, + ""Style"": null, + ""Explode"": null, + ""AllowReserved"": null, + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Content"": null + } + ] + } + ], + ""RequestBody"": null, + ""Responses"": { + ""Default"": null, + ""Patterns"": { + ""204"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""the PR was successfully merged"", + ""Headers"": null, + ""Content"": null, + ""Links"": null + } + ] + } + } + }, + ""Callbacks"": null, + ""Deprecated"": null, + ""Security"": null, + ""Servers"": null + }, + ""Delete"": null, + ""Options"": null, + ""Head"": null, + ""Patch"": null, + ""Trace"": null, + ""Servers"": null, + ""Parameters"": null + }, + ""/2.0/users/{username}"": { + ""Ref"": null, + ""Summary"": null, + ""Description"": null, + ""Get"": { + ""Tags"": null, + ""Summary"": null, + ""Description"": null, + ""ExternalDocs"": null, + ""OperationId"": ""getUserByName"", + ""Parameters"": [ + { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Name"": ""username"", + ""In"": { + ""Case"": ""Path"" + }, + ""Description"": null, + ""Required"": true, + ""Deprecated"": null, + ""AllowEmptyValue"": null, + ""Style"": null, + ""Explode"": null, + ""AllowReserved"": null, + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Content"": null + } + ] + } + ], + ""RequestBody"": null, + ""Responses"": { + ""Default"": null, + ""Patterns"": { + ""200"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""The User"", + ""Headers"": null, + ""Content"": { + ""application/json"": { + ""Schema"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""Ref"": ""#/components/schemas/user"" + } + ] + }, + ""Example"": null, + ""Encoding"": null + } + }, + ""Links"": { + ""userRepositories"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""Ref"": ""#/components/links/UserRepositories"" + } + ] + } + } + } + ] + } + } + }, + ""Callbacks"": null, + ""Deprecated"": null, + ""Security"": null, + ""Servers"": null + }, + ""Put"": null, + ""Post"": null, + ""Delete"": null, + ""Options"": null, + ""Head"": null, + ""Patch"": null, + ""Trace"": null, + ""Servers"": null, + ""Parameters"": null + } + } + }, + ""Components"": { + ""Schemas"": { + ""pullrequest"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""repository"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""user"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + } + }, + ""Responses"": null, + ""Parameters"": null, + ""Examples"": null, + ""RequestBodies"": null, + ""Headers"": null, + ""SecuritySchemes"": null, + ""Links"": { + ""PullRequestMerge"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Operation"": { + ""Case"": ""Id"", + ""Fields"": [ + ""mergePullRequest"" + ] + }, + ""Parameters"": { + ""pid"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + ""$response.body#/id"" + ] + }, + ""slug"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + ""$response.body#/repository/slug"" + ] + }, + ""username"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + ""$response.body#/author/username"" + ] + } + }, + ""RequestBody"": null, + ""Description"": null, + ""Server"": null + } + ] + }, + ""RepositoryPullRequests"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Operation"": { + ""Case"": ""Id"", + ""Fields"": [ + ""getPullRequestsByRepository"" + ] + }, + ""Parameters"": { + ""slug"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + ""$response.body#/slug"" + ] + }, + ""username"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + ""$response.body#/owner/username"" + ] + } + }, + ""RequestBody"": null, + ""Description"": null, + ""Server"": null + } + ] + }, + ""UserRepositories"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Operation"": { + ""Case"": ""Id"", + ""Fields"": [ + ""getRepositoriesByOwner"" + ] + }, + ""Parameters"": { + ""username"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + ""$response.body#/username"" + ] + } + }, + ""RequestBody"": null, + ""Description"": null, + ""Server"": null + } + ] + }, + ""UserRepository"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Operation"": { + ""Case"": ""Id"", + ""Fields"": [ + ""getRepository"" + ] + }, + ""Parameters"": { + ""slug"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + ""$response.body#/slug"" + ] + }, + ""username"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + ""$response.body#/owner/username"" + ] + } + }, + ""RequestBody"": null, + ""Description"": null, + ""Server"": null + } + ] + } + }, + ""Callbacks"": null + }, + ""Security"": null, + ""Tags"": null, + ""ExternalDocs"": null +}" + + return actual + } + + [] + let ``Non-oauth scopes example`` () = + let resource = + Assembly.getEmbeddedResource typeof.Assembly "non-oauth-scopes.json" + |> JsonNode.Parse + |> _.AsObject() + + let actual = OpenApiSpec.Parse resource + + expect { + snapshotJson + @"{ + ""OpenApi"": ""3.1.0"", + ""Info"": { + ""Title"": ""Non-oAuth Scopes example"", + ""Description"": null, + ""TermsOfService"": null, + ""Contact"": null, + ""License"": null, + ""Version"": ""1.0.0"" + }, + ""Servers"": null, + ""Paths"": { + ""Fields"": { + ""/users"": { + ""Ref"": null, + ""Summary"": null, + ""Description"": null, + ""Get"": { + ""Tags"": null, + ""Summary"": null, + ""Description"": null, + ""ExternalDocs"": null, + ""OperationId"": null, + ""Parameters"": null, + ""RequestBody"": null, + ""Responses"": null, + ""Callbacks"": null, + ""Deprecated"": null, + ""Security"": [ + { + ""Fields"": { + ""bearerAuth"": [ + ""read:users"", + ""public"" + ] + } + } + ], + ""Servers"": null + }, + ""Put"": null, + ""Post"": null, + ""Delete"": null, + ""Options"": null, + ""Head"": null, + ""Patch"": null, + ""Trace"": null, + ""Servers"": null, + ""Parameters"": null + } + } + }, + ""Components"": { + ""Schemas"": null, + ""Responses"": null, + ""Parameters"": null, + ""Examples"": null, + ""RequestBodies"": null, + ""Headers"": null, + ""SecuritySchemes"": { + ""bearerAuth"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Case"": ""Http"", + ""Fields"": [ + ""note: non-oauth scopes are not defined at the securityScheme level"", + ""bearer"", + ""jwt"" + ] + } + ] + } + }, + ""Links"": null, + ""Callbacks"": null + }, + ""Security"": null, + ""Tags"": null, + ""ExternalDocs"": null +}" + + return actual + } + + [] + let ``Petstore example`` () = + let resource = + Assembly.getEmbeddedResource typeof.Assembly "petstore.json" + |> JsonNode.Parse + |> _.AsObject() + + let actual = OpenApiSpec.Parse resource + + expect { + snapshotJson + @"{ + ""OpenApi"": ""3.0.0"", + ""Info"": { + ""Title"": ""Swagger Petstore"", + ""Description"": null, + ""TermsOfService"": null, + ""Contact"": null, + ""License"": { + ""Name"": ""MIT"", + ""Url"": null + }, + ""Version"": ""1.0.0"" + }, + ""Servers"": [ + { + ""Url"": ""http://petstore.swagger.io/v1"", + ""Description"": null, + ""Variables"": null + } + ], + ""Paths"": { + ""Fields"": { + ""/pets"": { + ""Ref"": null, + ""Summary"": null, + ""Description"": null, + ""Get"": { + ""Tags"": [ + ""pets"" + ], + ""Summary"": ""List all pets"", + ""Description"": null, + ""ExternalDocs"": null, + ""OperationId"": ""listPets"", + ""Parameters"": [ + { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Name"": ""limit"", + ""In"": { + ""Case"": ""Query"" + }, + ""Description"": ""How many items to return at one time (max 100)"", + ""Required"": false, + ""Deprecated"": null, + ""AllowEmptyValue"": null, + ""Style"": null, + ""Explode"": null, + ""AllowReserved"": null, + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Content"": null + } + ] + } + ], + ""RequestBody"": null, + ""Responses"": { + ""Default"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""unexpected error"", + ""Headers"": null, + ""Content"": { + ""application/json"": { + ""Schema"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""Ref"": ""#/components/schemas/Error"" + } + ] + }, + ""Example"": null, + ""Encoding"": null + } + }, + ""Links"": null + } + ] + }, + ""Patterns"": { + ""200"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""A paged array of pets"", + ""Headers"": { + ""x-next"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""A link to the next page of responses"", + ""Required"": null, + ""Deprecated"": null, + ""AllowEmptyValue"": null, + ""Style"": null, + ""Explode"": null, + ""AllowReserved"": null, + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Content"": null + } + ] + } + }, + ""Content"": { + ""application/json"": { + ""Schema"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""Ref"": ""#/components/schemas/Pets"" + } + ] + }, + ""Example"": null, + ""Encoding"": null + } + }, + ""Links"": null + } + ] + } + } + }, + ""Callbacks"": null, + ""Deprecated"": null, + ""Security"": null, + ""Servers"": null + }, + ""Put"": null, + ""Post"": { + ""Tags"": [ + ""pets"" + ], + ""Summary"": ""Create a pet"", + ""Description"": null, + ""ExternalDocs"": null, + ""OperationId"": ""createPets"", + ""Parameters"": null, + ""RequestBody"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": null, + ""Content"": { + ""application/json"": { + ""Schema"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""Ref"": ""#/components/schemas/Pet"" + } + ] + }, + ""Example"": null, + ""Encoding"": null + } + }, + ""Required"": true + } + ] + }, + ""Responses"": { + ""Default"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""unexpected error"", + ""Headers"": null, + ""Content"": { + ""application/json"": { + ""Schema"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""Ref"": ""#/components/schemas/Error"" + } + ] + }, + ""Example"": null, + ""Encoding"": null + } + }, + ""Links"": null + } + ] + }, + ""Patterns"": { + ""201"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""Null response"", + ""Headers"": null, + ""Content"": null, + ""Links"": null + } + ] + } + } + }, + ""Callbacks"": null, + ""Deprecated"": null, + ""Security"": null, + ""Servers"": null + }, + ""Delete"": null, + ""Options"": null, + ""Head"": null, + ""Patch"": null, + ""Trace"": null, + ""Servers"": null, + ""Parameters"": null + }, + ""/pets/{petId}"": { + ""Ref"": null, + ""Summary"": null, + ""Description"": null, + ""Get"": { + ""Tags"": [ + ""pets"" + ], + ""Summary"": ""Info for a specific pet"", + ""Description"": null, + ""ExternalDocs"": null, + ""OperationId"": ""showPetById"", + ""Parameters"": [ + { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Name"": ""petId"", + ""In"": { + ""Case"": ""Path"" + }, + ""Description"": ""The id of the pet to retrieve"", + ""Required"": true, + ""Deprecated"": null, + ""AllowEmptyValue"": null, + ""Style"": null, + ""Explode"": null, + ""AllowReserved"": null, + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Content"": null + } + ] + } + ], + ""RequestBody"": null, + ""Responses"": { + ""Default"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""unexpected error"", + ""Headers"": null, + ""Content"": { + ""application/json"": { + ""Schema"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""Ref"": ""#/components/schemas/Error"" + } + ] + }, + ""Example"": null, + ""Encoding"": null + } + }, + ""Links"": null + } + ] + }, + ""Patterns"": { + ""200"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""Expected response to a valid request"", + ""Headers"": null, + ""Content"": { + ""application/json"": { + ""Schema"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""Ref"": ""#/components/schemas/Pet"" + } + ] + }, + ""Example"": null, + ""Encoding"": null + } + }, + ""Links"": null + } + ] + } + } + }, + ""Callbacks"": null, + ""Deprecated"": null, + ""Security"": null, + ""Servers"": null + }, + ""Put"": null, + ""Post"": null, + ""Delete"": null, + ""Options"": null, + ""Head"": null, + ""Patch"": null, + ""Trace"": null, + ""Servers"": null, + ""Parameters"": null + } + } + }, + ""Components"": { + ""Schemas"": { + ""Error"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Pet"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Pets"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + } + }, + ""Responses"": null, + ""Parameters"": null, + ""Examples"": null, + ""RequestBodies"": null, + ""Headers"": null, + ""SecuritySchemes"": null, + ""Links"": null, + ""Callbacks"": null + }, + ""Security"": null, + ""Tags"": null, + ""ExternalDocs"": null +}" + + return actual + } + + [] + let ``Petstore expanded example`` () = + let resource = + Assembly.getEmbeddedResource typeof.Assembly "petstore-expanded.json" + |> JsonNode.Parse + |> _.AsObject() + + let actual = OpenApiSpec.Parse resource + + expect { + snapshotJson + @"{ + ""OpenApi"": ""3.0.0"", + ""Info"": { + ""Title"": ""Swagger Petstore"", + ""Description"": ""A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification"", + ""TermsOfService"": ""http://swagger.io/terms/"", + ""Contact"": { + ""Name"": ""Swagger API Team"", + ""Url"": ""http://swagger.io"", + ""Email"": ""apiteam@swagger.io"" + }, + ""License"": { + ""Name"": ""Apache 2.0"", + ""Url"": ""https://www.apache.org/licenses/LICENSE-2.0.html"" + }, + ""Version"": ""1.0.0"" + }, + ""Servers"": [ + { + ""Url"": ""https://petstore.swagger.io/v2"", + ""Description"": null, + ""Variables"": null + } + ], + ""Paths"": { + ""Fields"": { + ""/pets"": { + ""Ref"": null, + ""Summary"": null, + ""Description"": null, + ""Get"": { + ""Tags"": null, + ""Summary"": null, + ""Description"": ""Returns all pets from the system that the user has access to\nNam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia.\n\nSed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien.\n"", + ""ExternalDocs"": null, + ""OperationId"": ""findPets"", + ""Parameters"": [ + { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Name"": ""tags"", + ""In"": { + ""Case"": ""Query"" + }, + ""Description"": ""tags to filter by"", + ""Required"": false, + ""Deprecated"": null, + ""AllowEmptyValue"": null, + ""Style"": ""form"", + ""Explode"": null, + ""AllowReserved"": null, + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Content"": null + } + ] + }, + { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Name"": ""limit"", + ""In"": { + ""Case"": ""Query"" + }, + ""Description"": ""maximum number of results to return"", + ""Required"": false, + ""Deprecated"": null, + ""AllowEmptyValue"": null, + ""Style"": null, + ""Explode"": null, + ""AllowReserved"": null, + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Content"": null + } + ] + } + ], + ""RequestBody"": null, + ""Responses"": { + ""Default"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""unexpected error"", + ""Headers"": null, + ""Content"": { + ""application/json"": { + ""Schema"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""Ref"": ""#/components/schemas/Error"" + } + ] + }, + ""Example"": null, + ""Encoding"": null + } + }, + ""Links"": null + } + ] + }, + ""Patterns"": { + ""200"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""pet response"", + ""Headers"": null, + ""Content"": { + ""application/json"": { + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Encoding"": null + } + }, + ""Links"": null + } + ] + } + } + }, + ""Callbacks"": null, + ""Deprecated"": null, + ""Security"": null, + ""Servers"": null + }, + ""Put"": null, + ""Post"": { + ""Tags"": null, + ""Summary"": null, + ""Description"": ""Creates a new pet in the store. Duplicates are allowed"", + ""ExternalDocs"": null, + ""OperationId"": ""addPet"", + ""Parameters"": null, + ""RequestBody"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""Pet to add to the store"", + ""Content"": { + ""application/json"": { + ""Schema"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""Ref"": ""#/components/schemas/NewPet"" + } + ] + }, + ""Example"": null, + ""Encoding"": null + } + }, + ""Required"": true + } + ] + }, + ""Responses"": { + ""Default"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""unexpected error"", + ""Headers"": null, + ""Content"": { + ""application/json"": { + ""Schema"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""Ref"": ""#/components/schemas/Error"" + } + ] + }, + ""Example"": null, + ""Encoding"": null + } + }, + ""Links"": null + } + ] + }, + ""Patterns"": { + ""200"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""pet response"", + ""Headers"": null, + ""Content"": { + ""application/json"": { + ""Schema"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""Ref"": ""#/components/schemas/Pet"" + } + ] + }, + ""Example"": null, + ""Encoding"": null + } + }, + ""Links"": null + } + ] + } + } + }, + ""Callbacks"": null, + ""Deprecated"": null, + ""Security"": null, + ""Servers"": null + }, + ""Delete"": null, + ""Options"": null, + ""Head"": null, + ""Patch"": null, + ""Trace"": null, + ""Servers"": null, + ""Parameters"": null + }, + ""/pets/{id}"": { + ""Ref"": null, + ""Summary"": null, + ""Description"": null, + ""Get"": { + ""Tags"": null, + ""Summary"": null, + ""Description"": ""Returns a user based on a single ID, if the user does not have access to the pet"", + ""ExternalDocs"": null, + ""OperationId"": ""find pet by id"", + ""Parameters"": [ + { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Name"": ""id"", + ""In"": { + ""Case"": ""Path"" + }, + ""Description"": ""ID of pet to fetch"", + ""Required"": true, + ""Deprecated"": null, + ""AllowEmptyValue"": null, + ""Style"": null, + ""Explode"": null, + ""AllowReserved"": null, + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Content"": null + } + ] + } + ], + ""RequestBody"": null, + ""Responses"": { + ""Default"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""unexpected error"", + ""Headers"": null, + ""Content"": { + ""application/json"": { + ""Schema"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""Ref"": ""#/components/schemas/Error"" + } + ] + }, + ""Example"": null, + ""Encoding"": null + } + }, + ""Links"": null + } + ] + }, + ""Patterns"": { + ""200"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""pet response"", + ""Headers"": null, + ""Content"": { + ""application/json"": { + ""Schema"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""Ref"": ""#/components/schemas/Pet"" + } + ] + }, + ""Example"": null, + ""Encoding"": null + } + }, + ""Links"": null + } + ] + } + } + }, + ""Callbacks"": null, + ""Deprecated"": null, + ""Security"": null, + ""Servers"": null + }, + ""Put"": null, + ""Post"": null, + ""Delete"": { + ""Tags"": null, + ""Summary"": null, + ""Description"": ""deletes a single pet based on the ID supplied"", + ""ExternalDocs"": null, + ""OperationId"": ""deletePet"", + ""Parameters"": [ + { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Name"": ""id"", + ""In"": { + ""Case"": ""Path"" + }, + ""Description"": ""ID of pet to delete"", + ""Required"": true, + ""Deprecated"": null, + ""AllowEmptyValue"": null, + ""Style"": null, + ""Explode"": null, + ""AllowReserved"": null, + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Content"": null + } + ] + } + ], + ""RequestBody"": null, + ""Responses"": { + ""Default"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""unexpected error"", + ""Headers"": null, + ""Content"": { + ""application/json"": { + ""Schema"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""Ref"": ""#/components/schemas/Error"" + } + ] + }, + ""Example"": null, + ""Encoding"": null + } + }, + ""Links"": null + } + ] + }, + ""Patterns"": { + ""204"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""pet deleted"", + ""Headers"": null, + ""Content"": null, + ""Links"": null + } + ] + } + } + }, + ""Callbacks"": null, + ""Deprecated"": null, + ""Security"": null, + ""Servers"": null + }, + ""Options"": null, + ""Head"": null, + ""Patch"": null, + ""Trace"": null, + ""Servers"": null, + ""Parameters"": null + } + } + }, + ""Components"": { + ""Schemas"": { + ""Error"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""NewPet"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Pet"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + } + }, + ""Responses"": null, + ""Parameters"": null, + ""Examples"": null, + ""RequestBodies"": null, + ""Headers"": null, + ""SecuritySchemes"": null, + ""Links"": null, + ""Callbacks"": null + }, + ""Security"": null, + ""Tags"": null, + ""ExternalDocs"": null +}" + + return actual + } + + [] + let ``Tictactoe example`` () = + let resource = + Assembly.getEmbeddedResource typeof.Assembly "tictactoe.json" + |> JsonNode.Parse + |> _.AsObject() + + let actual = OpenApiSpec.Parse resource + + expect { + snapshotJson + @"{ + ""OpenApi"": ""3.1.0"", + ""Info"": { + ""Title"": ""Tic Tac Toe"", + ""Description"": ""This API allows writing down marks on a Tic Tac Toe board\nand requesting the state of the board or of individual squares.\n"", + ""TermsOfService"": null, + ""Contact"": null, + ""License"": null, + ""Version"": ""1.0.0"" + }, + ""Servers"": null, + ""Paths"": { + ""Fields"": { + ""/board"": { + ""Ref"": null, + ""Summary"": null, + ""Description"": null, + ""Get"": { + ""Tags"": [ + ""Gameplay"" + ], + ""Summary"": ""Get the whole board"", + ""Description"": ""Retrieves the current state of the board and the winner."", + ""ExternalDocs"": null, + ""OperationId"": ""get-board"", + ""Parameters"": null, + ""RequestBody"": null, + ""Responses"": { + ""Default"": null, + ""Patterns"": { + ""200"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""OK"", + ""Headers"": null, + ""Content"": { + ""application/json"": { + ""Schema"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""Ref"": ""#/components/schemas/status"" + } + ] + }, + ""Example"": null, + ""Encoding"": null + } + }, + ""Links"": null + } + ] + } + } + }, + ""Callbacks"": null, + ""Deprecated"": null, + ""Security"": [ + { + ""Fields"": { + ""defaultApiKey"": [] + } + }, + { + ""Fields"": { + ""app2AppOauth"": [ + ""board:read"" + ] + } + } + ], + ""Servers"": null + }, + ""Put"": null, + ""Post"": null, + ""Delete"": null, + ""Options"": null, + ""Head"": null, + ""Patch"": null, + ""Trace"": null, + ""Servers"": null, + ""Parameters"": null + }, + ""/board/{row}/{column}"": { + ""Ref"": null, + ""Summary"": null, + ""Description"": null, + ""Get"": { + ""Tags"": [ + ""Gameplay"" + ], + ""Summary"": ""Get a single board square"", + ""Description"": ""Retrieves the requested square."", + ""ExternalDocs"": null, + ""OperationId"": ""get-square"", + ""Parameters"": null, + ""RequestBody"": null, + ""Responses"": { + ""Default"": null, + ""Patterns"": { + ""200"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""OK"", + ""Headers"": null, + ""Content"": { + ""application/json"": { + ""Schema"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""Ref"": ""#/components/schemas/mark"" + } + ] + }, + ""Example"": null, + ""Encoding"": null + } + }, + ""Links"": null + } + ] + }, + ""400"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""The provided parameters are incorrect"", + ""Headers"": null, + ""Content"": { + ""text/html"": { + ""Schema"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""Ref"": ""#/components/schemas/errorMessage"" + } + ] + }, + ""Example"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + ""Illegal coordinates"" + ] + }, + ""Encoding"": null + } + }, + ""Links"": null + } + ] + } + } + }, + ""Callbacks"": null, + ""Deprecated"": null, + ""Security"": [ + { + ""Fields"": { + ""bearerHttpAuthentication"": [] + } + }, + { + ""Fields"": { + ""user2AppOauth"": [ + ""board:read"" + ] + } + } + ], + ""Servers"": null + }, + ""Put"": { + ""Tags"": [ + ""Gameplay"" + ], + ""Summary"": ""Set a single board square"", + ""Description"": ""Places a mark on the board and retrieves the whole board and the winner (if any)."", + ""ExternalDocs"": null, + ""OperationId"": ""put-square"", + ""Parameters"": null, + ""RequestBody"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": null, + ""Content"": { + ""application/json"": { + ""Schema"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""Ref"": ""#/components/schemas/mark"" + } + ] + }, + ""Example"": null, + ""Encoding"": null + } + }, + ""Required"": true + } + ] + }, + ""Responses"": { + ""Default"": null, + ""Patterns"": { + ""200"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""OK"", + ""Headers"": null, + ""Content"": { + ""application/json"": { + ""Schema"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""Ref"": ""#/components/schemas/status"" + } + ] + }, + ""Example"": null, + ""Encoding"": null + } + }, + ""Links"": null + } + ] + }, + ""400"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""The provided parameters are incorrect"", + ""Headers"": null, + ""Content"": { + ""text/html"": { + ""Schema"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""Ref"": ""#/components/schemas/errorMessage"" + } + ] + }, + ""Example"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""illegalCoordinates"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Summary"": null, + ""Description"": null, + ""Value"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + ""Illegal coordinates."" + ] + } + } + ] + }, + ""invalidMark"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Summary"": null, + ""Description"": null, + ""Value"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + ""Invalid Mark (X or O)."" + ] + } + } + ] + }, + ""notEmpty"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Summary"": null, + ""Description"": null, + ""Value"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + ""Square is not empty."" + ] + } + } + ] + } + } + ] + }, + ""Encoding"": null + } + }, + ""Links"": null + } + ] + } + } + }, + ""Callbacks"": null, + ""Deprecated"": null, + ""Security"": [ + { + ""Fields"": { + ""bearerHttpAuthentication"": [] + } + }, + { + ""Fields"": { + ""user2AppOauth"": [ + ""board:write"" + ] + } + } + ], + ""Servers"": null + }, + ""Post"": null, + ""Delete"": null, + ""Options"": null, + ""Head"": null, + ""Patch"": null, + ""Trace"": null, + ""Servers"": null, + ""Parameters"": [ + { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""Ref"": ""#/components/parameters/rowParam"" + } + ] + }, + { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""Ref"": ""#/components/parameters/columnParam"" + } + ] + } + ] + } + } + }, + ""Components"": { + ""Schemas"": { + ""board"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""coordinate"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""errorMessage"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""mark"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""status"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""winner"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + } + }, + ""Responses"": null, + ""Parameters"": { + ""columnParam"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Name"": ""column"", + ""In"": { + ""Case"": ""Path"" + }, + ""Description"": ""Board column (horizontal coordinate)"", + ""Required"": true, + ""Deprecated"": null, + ""AllowEmptyValue"": null, + ""Style"": null, + ""Explode"": null, + ""AllowReserved"": null, + ""Schema"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""Ref"": ""#/components/schemas/coordinate"" + } + ] + }, + ""Example"": null, + ""Content"": null + } + ] + }, + ""rowParam"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Name"": ""row"", + ""In"": { + ""Case"": ""Path"" + }, + ""Description"": ""Board row (vertical coordinate)"", + ""Required"": true, + ""Deprecated"": null, + ""AllowEmptyValue"": null, + ""Style"": null, + ""Explode"": null, + ""AllowReserved"": null, + ""Schema"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""Ref"": ""#/components/schemas/coordinate"" + } + ] + }, + ""Example"": null, + ""Content"": null + } + ] + } + }, + ""Examples"": null, + ""RequestBodies"": null, + ""Headers"": null, + ""SecuritySchemes"": { + ""app2AppOauth"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Case"": ""Oauth2"", + ""Fields"": [ + null, + { + ""Implicit"": null, + ""Password"": null, + ""ClientCredentials"": { + ""AuthorizationUrl"": null, + ""TokenUrl"": ""https://learn.openapis.org/oauth/2.0/token"", + ""RefreshUrl"": null, + ""Scopes"": { + ""board:read"": ""Read the board"" + } + }, + ""AuthorizationCode"": null + } + ] + } + ] + }, + ""basicHttpAuthentication"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Case"": ""Http"", + ""Fields"": [ + ""Basic HTTP Authentication"", + ""Basic"", + null + ] + } + ] + }, + ""bearerHttpAuthentication"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Case"": ""Http"", + ""Fields"": [ + ""Bearer token using a JWT"", + ""Bearer"", + ""JWT"" + ] + } + ] + }, + ""defaultApiKey"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Case"": ""ApiKey"", + ""Fields"": [ + ""API key provided in console"", + ""api-key"", + { + ""Case"": ""Header"" + } + ] + } + ] + }, + ""user2AppOauth"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Case"": ""Oauth2"", + ""Fields"": [ + null, + { + ""Implicit"": null, + ""Password"": null, + ""ClientCredentials"": null, + ""AuthorizationCode"": { + ""AuthorizationUrl"": ""https://learn.openapis.org/oauth/2.0/auth"", + ""TokenUrl"": ""https://learn.openapis.org/oauth/2.0/token"", + ""RefreshUrl"": null, + ""Scopes"": { + ""board:read"": ""Read the board"", + ""board:write"": ""Write to the board"" + } + } + } + ] + } + ] + } + }, + ""Links"": null, + ""Callbacks"": null + }, + ""Security"": null, + ""Tags"": [ + { + ""Name"": ""Gameplay"", + ""Description"": null, + ""ExternalDocs"": null + } + ], + ""ExternalDocs"": null +}" + + return actual + } + + [] + let ``uspto example`` () = + let resource = + Assembly.getEmbeddedResource typeof.Assembly "uspto.json" + |> JsonNode.Parse + |> _.AsObject() + + let actual = OpenApiSpec.Parse resource + + expect { + snapshotJson + @"{ + ""OpenApi"": ""3.0.1"", + ""Info"": { + ""Title"": ""USPTO Data Set API"", + ""Description"": ""The Data Set API (DSAPI) allows the public users to discover and search USPTO exported data sets. This is a generic API that allows USPTO users to make any CSV based data files searchable through API. With the help of GET call, it returns the list of data fields that are searchable. With the help of POST call, data can be fetched based on the filters on the field names. Please note that POST call is used to search the actual data. The reason for the POST call is that it allows users to specify any complex search criteria without worry about the GET size limitations as well as encoding of the input parameters."", + ""TermsOfService"": null, + ""Contact"": { + ""Name"": ""Open Data Portal"", + ""Url"": ""https://developer.uspto.gov"", + ""Email"": ""developer@uspto.gov"" + }, + ""License"": null, + ""Version"": ""1.0.0"" + }, + ""Servers"": [ + { + ""Url"": ""{scheme}://developer.uspto.gov/ds-api"", + ""Description"": null, + ""Variables"": { + ""scheme"": { + ""Enum"": [ + ""https"", + ""http"" + ], + ""Default"": ""https"", + ""Description"": ""The Data Set API is accessible via https and http"" + } + } + } + ], + ""Paths"": { + ""Fields"": { + ""/"": { + ""Ref"": null, + ""Summary"": null, + ""Description"": null, + ""Get"": { + ""Tags"": [ + ""metadata"" + ], + ""Summary"": ""List available data sets"", + ""Description"": null, + ""ExternalDocs"": null, + ""OperationId"": ""list-data-sets"", + ""Parameters"": null, + ""RequestBody"": null, + ""Responses"": { + ""Default"": null, + ""Patterns"": { + ""200"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""Returns a list of data sets"", + ""Headers"": null, + ""Content"": { + ""application/json"": { + ""Schema"": { + ""Case"": ""Choice2Of2"", + ""Fields"": [ + { + ""Ref"": ""#/components/schemas/dataSetList"" + } + ] + }, + ""Example"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""total"": 2, + ""apis"": [ + { + ""apiKey"": ""oa_citations"", + ""apiVersionNumber"": ""v1"", + ""apiUrl"": ""https://developer.uspto.gov/ds-api/oa_citations/v1/fields"", + ""apiDocumentationUrl"": ""https://developer.uspto.gov/ds-api-docs/index.html?url=https://developer.uspto.gov/ds-api/swagger/docs/oa_citations.json"" + }, + { + ""apiKey"": ""cancer_moonshot"", + ""apiVersionNumber"": ""v1"", + ""apiUrl"": ""https://developer.uspto.gov/ds-api/cancer_moonshot/v1/fields"", + ""apiDocumentationUrl"": ""https://developer.uspto.gov/ds-api-docs/index.html?url=https://developer.uspto.gov/ds-api/swagger/docs/cancer_moonshot.json"" + } + ] + } + ] + }, + ""Encoding"": null + } + }, + ""Links"": null + } + ] + } + } + }, + ""Callbacks"": null, + ""Deprecated"": null, + ""Security"": null, + ""Servers"": null + }, + ""Put"": null, + ""Post"": null, + ""Delete"": null, + ""Options"": null, + ""Head"": null, + ""Patch"": null, + ""Trace"": null, + ""Servers"": null, + ""Parameters"": null + }, + ""/{dataset}/{version}/fields"": { + ""Ref"": null, + ""Summary"": null, + ""Description"": null, + ""Get"": { + ""Tags"": [ + ""metadata"" + ], + ""Summary"": ""Provides the general information about the API and the list of fields that can be used to query the dataset."", + ""Description"": ""This GET API returns the list of all the searchable field names that are in the oa_citations. Please see the \u0027fields\u0027 attribute which returns an array of field names. Each field or a combination of fields can be searched using the syntax options shown below."", + ""ExternalDocs"": null, + ""OperationId"": ""list-searchable-fields"", + ""Parameters"": [ + { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Name"": ""dataset"", + ""In"": { + ""Case"": ""Path"" + }, + ""Description"": ""Name of the dataset."", + ""Required"": true, + ""Deprecated"": null, + ""AllowEmptyValue"": null, + ""Style"": null, + ""Explode"": null, + ""AllowReserved"": null, + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + ""oa_citations"" + ] + }, + ""Content"": null + } + ] + }, + { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Name"": ""version"", + ""In"": { + ""Case"": ""Path"" + }, + ""Description"": ""Version of the dataset."", + ""Required"": true, + ""Deprecated"": null, + ""AllowEmptyValue"": null, + ""Style"": null, + ""Explode"": null, + ""AllowReserved"": null, + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + ""v1"" + ] + }, + ""Content"": null + } + ] + } + ], + ""RequestBody"": null, + ""Responses"": { + ""Default"": null, + ""Patterns"": { + ""200"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""The dataset API for the given version is found and it is accessible to consume."", + ""Headers"": null, + ""Content"": { + ""application/json"": { + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Encoding"": null + } + }, + ""Links"": null + } + ] + }, + ""404"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""The combination of dataset name and version is not found in the system or it is not published yet to be consumed by public."", + ""Headers"": null, + ""Content"": { + ""application/json"": { + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Encoding"": null + } + }, + ""Links"": null + } + ] + } + } + }, + ""Callbacks"": null, + ""Deprecated"": null, + ""Security"": null, + ""Servers"": null + }, + ""Put"": null, + ""Post"": null, + ""Delete"": null, + ""Options"": null, + ""Head"": null, + ""Patch"": null, + ""Trace"": null, + ""Servers"": null, + ""Parameters"": null + }, + ""/{dataset}/{version}/records"": { + ""Ref"": null, + ""Summary"": null, + ""Description"": null, + ""Get"": null, + ""Put"": null, + ""Post"": { + ""Tags"": [ + ""search"" + ], + ""Summary"": ""Provides search capability for the data set with the given search criteria."", + ""Description"": ""This API is based on Solr/Lucene Search. The data is indexed using SOLR. This GET API returns the list of all the searchable field names that are in the Solr Index. Please see the \u0027fields\u0027 attribute which returns an array of field names. Each field or a combination of fields can be searched using the Solr/Lucene Syntax. Please refer https://lucene.apache.org/core/3_6_2/queryparsersyntax.html#Overview for the query syntax. List of field names that are searchable can be determined using above GET api."", + ""ExternalDocs"": null, + ""OperationId"": ""perform-search"", + ""Parameters"": [ + { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Name"": ""version"", + ""In"": { + ""Case"": ""Path"" + }, + ""Description"": ""Version of the dataset."", + ""Required"": true, + ""Deprecated"": null, + ""AllowEmptyValue"": null, + ""Style"": null, + ""Explode"": null, + ""AllowReserved"": null, + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Content"": null + } + ] + }, + { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Name"": ""dataset"", + ""In"": { + ""Case"": ""Path"" + }, + ""Description"": ""Name of the dataset. In this case, the default value is oa_citations"", + ""Required"": true, + ""Deprecated"": null, + ""AllowEmptyValue"": null, + ""Style"": null, + ""Explode"": null, + ""AllowReserved"": null, + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Content"": null + } + ] + } + ], + ""RequestBody"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": null, + ""Content"": { + ""application/x-www-form-urlencoded"": { + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Encoding"": null + } + }, + ""Required"": null + } + ] + }, + ""Responses"": { + ""Default"": null, + ""Patterns"": { + ""200"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""successful operation"", + ""Headers"": null, + ""Content"": { + ""application/json"": { + ""Schema"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + }, + ""Example"": null, + ""Encoding"": null + } + }, + ""Links"": null + } + ] + }, + ""404"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + { + ""Description"": ""No matching record found for the given criteria."", + ""Headers"": null, + ""Content"": null, + ""Links"": null + } + ] + } + } + }, + ""Callbacks"": null, + ""Deprecated"": null, + ""Security"": null, + ""Servers"": null + }, + ""Delete"": null, + ""Options"": null, + ""Head"": null, + ""Patch"": null, + ""Trace"": null, + ""Servers"": null, + ""Parameters"": null + } + } + }, + ""Components"": { + ""Schemas"": { + ""dataSetList"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + } + }, + ""Responses"": null, + ""Parameters"": null, + ""Examples"": null, + ""RequestBodies"": null, + ""Headers"": null, + ""SecuritySchemes"": null, + ""Links"": null, + ""Callbacks"": null + }, + ""Security"": null, + ""Tags"": [ + { + ""Name"": ""metadata"", + ""Description"": ""Find out about the data sets"", + ""ExternalDocs"": null + }, + { + ""Name"": ""search"", + ""Description"": ""Search a data set"", + ""ExternalDocs"": null + } + ], + ""ExternalDocs"": null +}" + + return actual + } + + [] + let ``webhook example`` () = + // Webhooks aren't mentioned in the 3.0.0 spec so we have no information here. + let resource = + Assembly.getEmbeddedResource typeof.Assembly "webhook-example.json" + |> JsonNode.Parse + |> _.AsObject() + + let actual = OpenApiSpec.Parse resource + + expect { + snapshotJson + @"{ + ""OpenApi"": ""3.1.0"", + ""Info"": { + ""Title"": ""Webhook Example"", + ""Description"": null, + ""TermsOfService"": null, + ""Contact"": null, + ""License"": null, + ""Version"": ""1.0.0"" + }, + ""Servers"": null, + ""Paths"": null, + ""Components"": { + ""Schemas"": { + ""Pet"": { + ""Case"": ""Choice1Of2"", + ""Fields"": [ + null + ] + } + }, + ""Responses"": null, + ""Parameters"": null, + ""Examples"": null, + ""RequestBodies"": null, + ""Headers"": null, + ""SecuritySchemes"": null, + ""Links"": null, + ""Callbacks"": null + }, + ""Security"": null, + ""Tags"": null, + ""ExternalDocs"": null +}" + + return actual + } diff --git a/WoofWare.Myriad.Plugins.Test/TestSwagger/api-with-examples.json b/WoofWare.Myriad.Plugins.Test/TestSwagger/api-with-examples.json new file mode 100644 index 0000000..31d2e9b --- /dev/null +++ b/WoofWare.Myriad.Plugins.Test/TestSwagger/api-with-examples.json @@ -0,0 +1,192 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Simple API overview", + "version": "2.0.0" + }, + "paths": { + "/": { + "get": { + "operationId": "listVersionsv2", + "summary": "List API versions", + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "foo": { + "value": { + "versions": [ + { + "status": "CURRENT", + "updated": "2011-01-21T11:33:21Z", + "id": "v2.0", + "links": [ + { + "href": "http://127.0.0.1:8774/v2/", + "rel": "self" + } + ] + }, + { + "status": "EXPERIMENTAL", + "updated": "2013-07-23T11:33:21Z", + "id": "v3.0", + "links": [ + { + "href": "http://127.0.0.1:8774/v3/", + "rel": "self" + } + ] + } + ] + } + } + } + } + } + }, + "300": { + "description": "300 response", + "content": { + "application/json": { + "examples": { + "foo": { + "value": { + "versions": [ + { + "status": "CURRENT", + "updated": "2011-01-21T11:33:21Z", + "id": "v2.0", + "links": [ + { + "href": "http://127.0.0.1:8774/v2/", + "rel": "self" + } + ] + }, + { + "status": "EXPERIMENTAL", + "updated": "2013-07-23T11:33:21Z", + "id": "v3.0", + "links": [ + { + "href": "http://127.0.0.1:8774/v3/", + "rel": "self" + } + ] + } + ] + } + } + } + } + } + } + } + } + }, + "/v2": { + "get": { + "operationId": "getVersionDetailsv2", + "summary": "Show API version details", + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "foo": { + "value": { + "version": { + "status": "CURRENT", + "updated": "2011-01-21T11:33:21Z", + "media-types": [ + { + "base": "application/xml", + "type": "application/vnd.openstack.compute+xml;version=2" + }, + { + "base": "application/json", + "type": "application/vnd.openstack.compute+json;version=2" + } + ], + "id": "v2.0", + "links": [ + { + "href": "http://127.0.0.1:8774/v2/", + "rel": "self" + }, + { + "href": "http://docs.openstack.org/api/openstack-compute/2/os-compute-devguide-2.pdf", + "type": "application/pdf", + "rel": "describedby" + }, + { + "href": "http://docs.openstack.org/api/openstack-compute/2/wadl/os-compute-2.wadl", + "type": "application/vnd.sun.wadl+xml", + "rel": "describedby" + }, + { + "href": "http://docs.openstack.org/api/openstack-compute/2/wadl/os-compute-2.wadl", + "type": "application/vnd.sun.wadl+xml", + "rel": "describedby" + } + ] + } + } + } + } + } + } + }, + "203": { + "description": "203 response", + "content": { + "application/json": { + "examples": { + "foo": { + "value": { + "version": { + "status": "CURRENT", + "updated": "2011-01-21T11:33:21Z", + "media-types": [ + { + "base": "application/xml", + "type": "application/vnd.openstack.compute+xml;version=2" + }, + { + "base": "application/json", + "type": "application/vnd.openstack.compute+json;version=2" + } + ], + "id": "v2.0", + "links": [ + { + "href": "http://23.253.228.211:8774/v2/", + "rel": "self" + }, + { + "href": "http://docs.openstack.org/api/openstack-compute/2/os-compute-devguide-2.pdf", + "type": "application/pdf", + "rel": "describedby" + }, + { + "href": "http://docs.openstack.org/api/openstack-compute/2/wadl/os-compute-2.wadl", + "type": "application/vnd.sun.wadl+xml", + "rel": "describedby" + } + ] + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/WoofWare.Myriad.Plugins.Test/TestSwagger/callback-example.json b/WoofWare.Myriad.Plugins.Test/TestSwagger/callback-example.json new file mode 100644 index 0000000..edae06a --- /dev/null +++ b/WoofWare.Myriad.Plugins.Test/TestSwagger/callback-example.json @@ -0,0 +1,82 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Callback Example", + "version": "1.0.0" + }, + "paths": { + "/streams": { + "post": { + "description": "subscribes a client to receive out-of-band data", + "parameters": [ + { + "name": "callbackUrl", + "in": "query", + "required": true, + "description": "the location where data will be sent. Must be network accessible\nby the source server\n", + "schema": { + "type": "string", + "format": "uri", + "example": "https://tonys-server.com" + } + } + ], + "responses": { + "201": { + "description": "subscription successfully created", + "content": { + "application/json": { + "schema": { + "description": "subscription information", + "required": ["subscriptionId"], + "properties": { + "subscriptionId": { + "description": "this unique identifier allows management of the subscription", + "type": "string", + "example": "2531329f-fb09-4ef7-887e-84e648214436" + } + } + } + } + } + } + }, + "callbacks": { + "onData": { + "{$request.query.callbackUrl}/data": { + "post": { + "requestBody": { + "description": "subscription payload", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "timestamp": { + "type": "string", + "format": "date-time" + }, + "userData": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "202": { + "description": "Your server implementation should return this HTTP status code\nif the data was received successfully\n" + }, + "204": { + "description": "Your server should return this HTTP status code if no longer interested\nin further updates\n" + } + } + } + } + } + } + } + } + } +} diff --git a/WoofWare.Myriad.Plugins.Test/TestSwagger/link-example.json b/WoofWare.Myriad.Plugins.Test/TestSwagger/link-example.json new file mode 100644 index 0000000..e230387 --- /dev/null +++ b/WoofWare.Myriad.Plugins.Test/TestSwagger/link-example.json @@ -0,0 +1,319 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Link Example", + "version": "1.0.0" + }, + "paths": { + "/2.0/users/{username}": { + "get": { + "operationId": "getUserByName", + "parameters": [ + { + "name": "username", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The User", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/user" + } + } + }, + "links": { + "userRepositories": { + "$ref": "#/components/links/UserRepositories" + } + } + } + } + } + }, + "/2.0/repositories/{username}": { + "get": { + "operationId": "getRepositoriesByOwner", + "parameters": [ + { + "name": "username", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "repositories owned by the supplied user", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/repository" + } + } + } + }, + "links": { + "userRepository": { + "$ref": "#/components/links/UserRepository" + } + } + } + } + } + }, + "/2.0/repositories/{username}/{slug}": { + "get": { + "operationId": "getRepository", + "parameters": [ + { + "name": "username", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The repository", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/repository" + } + } + }, + "links": { + "repositoryPullRequests": { + "$ref": "#/components/links/RepositoryPullRequests" + } + } + } + } + } + }, + "/2.0/repositories/{username}/{slug}/pullrequests": { + "get": { + "operationId": "getPullRequestsByRepository", + "parameters": [ + { + "name": "username", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "state", + "in": "query", + "schema": { + "type": "string", + "enum": ["open", "merged", "declined"] + } + } + ], + "responses": { + "200": { + "description": "an array of pull request objects", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/pullrequest" + } + } + } + } + } + } + } + }, + "/2.0/repositories/{username}/{slug}/pullrequests/{pid}": { + "get": { + "operationId": "getPullRequestsById", + "parameters": [ + { + "name": "username", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "pid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "a pull request object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/pullrequest" + } + } + }, + "links": { + "pullRequestMerge": { + "$ref": "#/components/links/PullRequestMerge" + } + } + } + } + } + }, + "/2.0/repositories/{username}/{slug}/pullrequests/{pid}/merge": { + "post": { + "operationId": "mergePullRequest", + "parameters": [ + { + "name": "username", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "pid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "the PR was successfully merged" + } + } + } + } + }, + "components": { + "links": { + "UserRepositories": { + "operationId": "getRepositoriesByOwner", + "parameters": { + "username": "$response.body#/username" + } + }, + "UserRepository": { + "operationId": "getRepository", + "parameters": { + "username": "$response.body#/owner/username", + "slug": "$response.body#/slug" + } + }, + "RepositoryPullRequests": { + "operationId": "getPullRequestsByRepository", + "parameters": { + "username": "$response.body#/owner/username", + "slug": "$response.body#/slug" + } + }, + "PullRequestMerge": { + "operationId": "mergePullRequest", + "parameters": { + "username": "$response.body#/author/username", + "slug": "$response.body#/repository/slug", + "pid": "$response.body#/id" + } + } + }, + "schemas": { + "user": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "repository": { + "type": "object", + "properties": { + "slug": { + "type": "string" + }, + "owner": { + "$ref": "#/components/schemas/user" + } + } + }, + "pullrequest": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "repository": { + "$ref": "#/components/schemas/repository" + }, + "author": { + "$ref": "#/components/schemas/user" + } + } + } + } + } +} diff --git a/WoofWare.Myriad.Plugins.Test/TestSwagger/non-oauth-scopes.json b/WoofWare.Myriad.Plugins.Test/TestSwagger/non-oauth-scopes.json new file mode 100644 index 0000000..6a39a72 --- /dev/null +++ b/WoofWare.Myriad.Plugins.Test/TestSwagger/non-oauth-scopes.json @@ -0,0 +1,28 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Non-oAuth Scopes example", + "version": "1.0.0" + }, + "paths": { + "/users": { + "get": { + "security": [ + { + "bearerAuth": ["read:users", "public"] + } + ] + } + } + }, + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "jwt", + "description": "note: non-oauth scopes are not defined at the securityScheme level" + } + } + } +} diff --git a/WoofWare.Myriad.Plugins.Test/TestSwagger/petstore-expanded.json b/WoofWare.Myriad.Plugins.Test/TestSwagger/petstore-expanded.json new file mode 100644 index 0000000..7315872 --- /dev/null +++ b/WoofWare.Myriad.Plugins.Test/TestSwagger/petstore-expanded.json @@ -0,0 +1,235 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "Swagger Petstore", + "description": "A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "Swagger API Team", + "email": "apiteam@swagger.io", + "url": "http://swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "servers": [ + { + "url": "https://petstore.swagger.io/v2" + } + ], + "paths": { + "/pets": { + "get": { + "description": "Returns all pets from the system that the user has access to\nNam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia.\n\nSed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien.\n", + "operationId": "findPets", + "parameters": [ + { + "name": "tags", + "in": "query", + "description": "tags to filter by", + "required": false, + "style": "form", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "limit", + "in": "query", + "description": "maximum number of results to return", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "pet response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "description": "Creates a new pet in the store. Duplicates are allowed", + "operationId": "addPet", + "requestBody": { + "description": "Pet to add to the store", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "200": { + "description": "pet response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/pets/{id}": { + "get": { + "description": "Returns a user based on a single ID, if the user does not have access to the pet", + "operationId": "find pet by id", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of pet to fetch", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "pet response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "delete": { + "description": "deletes a single pet based on the ID supplied", + "operationId": "deletePet", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of pet to delete", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "204": { + "description": "pet deleted" + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "allOf": [ + { + "$ref": "#/components/schemas/NewPet" + }, + { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "format": "int64" + } + } + } + ] + }, + "NewPet": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "Error": { + "type": "object", + "required": ["code", "message"], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + } + } + } + } + } +} diff --git a/WoofWare.Myriad.Plugins.Test/TestSwagger/petstore.json b/WoofWare.Myriad.Plugins.Test/TestSwagger/petstore.json new file mode 100644 index 0000000..9d717b6 --- /dev/null +++ b/WoofWare.Myriad.Plugins.Test/TestSwagger/petstore.json @@ -0,0 +1,177 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "Swagger Petstore", + "license": { + "name": "MIT" + } + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "summary": "List all pets", + "operationId": "listPets", + "tags": ["pets"], + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "How many items to return at one time (max 100)", + "required": false, + "schema": { + "type": "integer", + "maximum": 100, + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "A paged array of pets", + "headers": { + "x-next": { + "description": "A link to the next page of responses", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pets" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "summary": "Create a pet", + "operationId": "createPets", + "tags": ["pets"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Null response" + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "summary": "Info for a specific pet", + "operationId": "showPetById", + "tags": ["pets"], + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "description": "The id of the pet to retrieve", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Expected response to a valid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "required": ["id", "name"], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "Pets": { + "type": "array", + "maxItems": 100, + "items": { + "$ref": "#/components/schemas/Pet" + } + }, + "Error": { + "type": "object", + "required": ["code", "message"], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + } + } + } + } + } +} diff --git a/WoofWare.Myriad.Plugins.Test/TestSwagger/tictactoe.json b/WoofWare.Myriad.Plugins.Test/TestSwagger/tictactoe.json new file mode 100644 index 0000000..79f34cb --- /dev/null +++ b/WoofWare.Myriad.Plugins.Test/TestSwagger/tictactoe.json @@ -0,0 +1,261 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Tic Tac Toe", + "description": "This API allows writing down marks on a Tic Tac Toe board\nand requesting the state of the board or of individual squares.\n", + "version": "1.0.0" + }, + "tags": [ + { + "name": "Gameplay" + } + ], + "paths": { + "/board": { + "get": { + "summary": "Get the whole board", + "description": "Retrieves the current state of the board and the winner.", + "tags": ["Gameplay"], + "operationId": "get-board", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/status" + } + } + } + } + }, + "security": [ + { + "defaultApiKey": [] + }, + { + "app2AppOauth": ["board:read"] + } + ] + } + }, + "/board/{row}/{column}": { + "parameters": [ + { + "$ref": "#/components/parameters/rowParam" + }, + { + "$ref": "#/components/parameters/columnParam" + } + ], + "get": { + "summary": "Get a single board square", + "description": "Retrieves the requested square.", + "tags": ["Gameplay"], + "operationId": "get-square", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/mark" + } + } + } + }, + "400": { + "description": "The provided parameters are incorrect", + "content": { + "text/html": { + "schema": { + "$ref": "#/components/schemas/errorMessage" + }, + "example": "Illegal coordinates" + } + } + } + }, + "security": [ + { + "bearerHttpAuthentication": [] + }, + { + "user2AppOauth": ["board:read"] + } + ] + }, + "put": { + "summary": "Set a single board square", + "description": "Places a mark on the board and retrieves the whole board and the winner (if any).", + "tags": ["Gameplay"], + "operationId": "put-square", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/mark" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/status" + } + } + } + }, + "400": { + "description": "The provided parameters are incorrect", + "content": { + "text/html": { + "schema": { + "$ref": "#/components/schemas/errorMessage" + }, + "examples": { + "illegalCoordinates": { + "value": "Illegal coordinates." + }, + "notEmpty": { + "value": "Square is not empty." + }, + "invalidMark": { + "value": "Invalid Mark (X or O)." + } + } + } + } + } + }, + "security": [ + { + "bearerHttpAuthentication": [] + }, + { + "user2AppOauth": ["board:write"] + } + ] + } + } + }, + "components": { + "parameters": { + "rowParam": { + "description": "Board row (vertical coordinate)", + "name": "row", + "in": "path", + "required": true, + "schema": { + "$ref": "#/components/schemas/coordinate" + } + }, + "columnParam": { + "description": "Board column (horizontal coordinate)", + "name": "column", + "in": "path", + "required": true, + "schema": { + "$ref": "#/components/schemas/coordinate" + } + } + }, + "schemas": { + "errorMessage": { + "type": "string", + "maxLength": 256, + "description": "A text message describing an error" + }, + "coordinate": { + "type": "integer", + "minimum": 1, + "maximum": 3, + "example": 1 + }, + "mark": { + "type": "string", + "enum": [".", "X", "O"], + "description": "Possible values for a board square. `.` means empty square.", + "example": "." + }, + "board": { + "type": "array", + "maxItems": 3, + "minItems": 3, + "items": { + "type": "array", + "maxItems": 3, + "minItems": 3, + "items": { + "$ref": "#/components/schemas/mark" + } + } + }, + "winner": { + "type": "string", + "enum": [".", "X", "O"], + "description": "Winner of the game. `.` means nobody has won yet.", + "example": "." + }, + "status": { + "type": "object", + "properties": { + "winner": { + "$ref": "#/components/schemas/winner" + }, + "board": { + "$ref": "#/components/schemas/board" + } + } + } + }, + "securitySchemes": { + "defaultApiKey": { + "description": "API key provided in console", + "type": "apiKey", + "name": "api-key", + "in": "header" + }, + "basicHttpAuthentication": { + "description": "Basic HTTP Authentication", + "type": "http", + "scheme": "Basic" + }, + "bearerHttpAuthentication": { + "description": "Bearer token using a JWT", + "type": "http", + "scheme": "Bearer", + "bearerFormat": "JWT" + }, + "app2AppOauth": { + "type": "oauth2", + "flows": { + "clientCredentials": { + "tokenUrl": "https://learn.openapis.org/oauth/2.0/token", + "scopes": { + "board:read": "Read the board" + } + } + } + }, + "user2AppOauth": { + "type": "oauth2", + "flows": { + "authorizationCode": { + "authorizationUrl": "https://learn.openapis.org/oauth/2.0/auth", + "tokenUrl": "https://learn.openapis.org/oauth/2.0/token", + "scopes": { + "board:read": "Read the board", + "board:write": "Write to the board" + } + } + } + } + } + } +} diff --git a/WoofWare.Myriad.Plugins.Test/TestSwagger/uspto.json b/WoofWare.Myriad.Plugins.Test/TestSwagger/uspto.json new file mode 100644 index 0000000..5c687e2 --- /dev/null +++ b/WoofWare.Myriad.Plugins.Test/TestSwagger/uspto.json @@ -0,0 +1,241 @@ +{ + "openapi": "3.0.1", + "servers": [ + { + "url": "{scheme}://developer.uspto.gov/ds-api", + "variables": { + "scheme": { + "description": "The Data Set API is accessible via https and http", + "enum": ["https", "http"], + "default": "https" + } + } + } + ], + "info": { + "description": "The Data Set API (DSAPI) allows the public users to discover and search USPTO exported data sets. This is a generic API that allows USPTO users to make any CSV based data files searchable through API. With the help of GET call, it returns the list of data fields that are searchable. With the help of POST call, data can be fetched based on the filters on the field names. Please note that POST call is used to search the actual data. The reason for the POST call is that it allows users to specify any complex search criteria without worry about the GET size limitations as well as encoding of the input parameters.", + "version": "1.0.0", + "title": "USPTO Data Set API", + "contact": { + "name": "Open Data Portal", + "url": "https://developer.uspto.gov", + "email": "developer@uspto.gov" + } + }, + "tags": [ + { + "name": "metadata", + "description": "Find out about the data sets" + }, + { + "name": "search", + "description": "Search a data set" + } + ], + "paths": { + "/": { + "get": { + "tags": ["metadata"], + "operationId": "list-data-sets", + "summary": "List available data sets", + "responses": { + "200": { + "description": "Returns a list of data sets", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/dataSetList" + }, + "example": { + "total": 2, + "apis": [ + { + "apiKey": "oa_citations", + "apiVersionNumber": "v1", + "apiUrl": "https://developer.uspto.gov/ds-api/oa_citations/v1/fields", + "apiDocumentationUrl": "https://developer.uspto.gov/ds-api-docs/index.html?url=https://developer.uspto.gov/ds-api/swagger/docs/oa_citations.json" + }, + { + "apiKey": "cancer_moonshot", + "apiVersionNumber": "v1", + "apiUrl": "https://developer.uspto.gov/ds-api/cancer_moonshot/v1/fields", + "apiDocumentationUrl": "https://developer.uspto.gov/ds-api-docs/index.html?url=https://developer.uspto.gov/ds-api/swagger/docs/cancer_moonshot.json" + } + ] + } + } + } + } + } + } + }, + "/{dataset}/{version}/fields": { + "get": { + "tags": ["metadata"], + "summary": "Provides the general information about the API and the list of fields that can be used to query the dataset.", + "description": "This GET API returns the list of all the searchable field names that are in the oa_citations. Please see the 'fields' attribute which returns an array of field names. Each field or a combination of fields can be searched using the syntax options shown below.", + "operationId": "list-searchable-fields", + "parameters": [ + { + "name": "dataset", + "in": "path", + "description": "Name of the dataset.", + "required": true, + "example": "oa_citations", + "schema": { + "type": "string" + } + }, + { + "name": "version", + "in": "path", + "description": "Version of the dataset.", + "required": true, + "example": "v1", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The dataset API for the given version is found and it is accessible to consume.", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "404": { + "description": "The combination of dataset name and version is not found in the system or it is not published yet to be consumed by public.", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/{dataset}/{version}/records": { + "post": { + "tags": ["search"], + "summary": "Provides search capability for the data set with the given search criteria.", + "description": "This API is based on Solr/Lucene Search. The data is indexed using SOLR. This GET API returns the list of all the searchable field names that are in the Solr Index. Please see the 'fields' attribute which returns an array of field names. Each field or a combination of fields can be searched using the Solr/Lucene Syntax. Please refer https://lucene.apache.org/core/3_6_2/queryparsersyntax.html#Overview for the query syntax. List of field names that are searchable can be determined using above GET api.", + "operationId": "perform-search", + "parameters": [ + { + "name": "version", + "in": "path", + "description": "Version of the dataset.", + "required": true, + "schema": { + "type": "string", + "default": "v1" + } + }, + { + "name": "dataset", + "in": "path", + "description": "Name of the dataset. In this case, the default value is oa_citations", + "required": true, + "schema": { + "type": "string", + "default": "oa_citations" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "type": "object" + } + } + } + } + } + }, + "404": { + "description": "No matching record found for the given criteria." + } + }, + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "properties": { + "criteria": { + "description": "Uses Lucene Query Syntax in the format of propertyName:value, propertyName:[num1 TO num2] and date range format: propertyName:[yyyyMMdd TO yyyyMMdd]. In the response please see the 'docs' element which has the list of record objects. Each record structure would consist of all the fields and their corresponding values.", + "type": "string", + "default": "*:*" + }, + "start": { + "description": "Starting record number. Default value is 0.", + "type": "integer", + "default": 0 + }, + "rows": { + "description": "Specify number of rows to be returned. If you run the search with default values, in the response you will see 'numFound' attribute which will tell the number of records available in the dataset.", + "type": "integer", + "default": 100 + } + }, + "required": ["criteria"] + } + } + } + } + } + } + }, + "components": { + "schemas": { + "dataSetList": { + "type": "object", + "properties": { + "total": { + "type": "integer" + }, + "apis": { + "type": "array", + "items": { + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "description": "To be used as a dataset parameter value" + }, + "apiVersionNumber": { + "type": "string", + "description": "To be used as a version parameter value" + }, + "apiUrl": { + "type": "string", + "format": "uriref", + "description": "The URL describing the dataset's fields" + }, + "apiDocumentationUrl": { + "type": "string", + "format": "uriref", + "description": "A URL to the API console for each API" + } + } + } + } + } + } + } + } +} diff --git a/WoofWare.Myriad.Plugins.Test/TestSwagger/webhook-example.json b/WoofWare.Myriad.Plugins.Test/TestSwagger/webhook-example.json new file mode 100644 index 0000000..d39d129 --- /dev/null +++ b/WoofWare.Myriad.Plugins.Test/TestSwagger/webhook-example.json @@ -0,0 +1,47 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Webhook Example", + "version": "1.0.0" + }, + "webhooks": { + "newPet": { + "post": { + "requestBody": { + "description": "Information about a new pet in the system", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "responses": { + "200": { + "description": "Return a 200 status to indicate that the data was received successfully" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "required": ["id", "name"], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + } + } + } +} diff --git a/WoofWare.Myriad.Plugins.Test/WoofWare.Myriad.Plugins.Test.fsproj b/WoofWare.Myriad.Plugins.Test/WoofWare.Myriad.Plugins.Test.fsproj index 701cbed..aab401d 100644 --- a/WoofWare.Myriad.Plugins.Test/WoofWare.Myriad.Plugins.Test.fsproj +++ b/WoofWare.Myriad.Plugins.Test/WoofWare.Myriad.Plugins.Test.fsproj @@ -13,6 +13,7 @@ + @@ -36,6 +37,16 @@ + + + + + + + + + + diff --git a/WoofWare.Myriad.Plugins/OpenApi3.fs b/WoofWare.Myriad.Plugins/OpenApi3.fs index 3e49971..d21c963 100644 --- a/WoofWare.Myriad.Plugins/OpenApi3.fs +++ b/WoofWare.Myriad.Plugins/OpenApi3.fs @@ -11,7 +11,19 @@ type ExternalDocumentation = Url : Uri } -type Schema = | Schema of unit + static member Parse (node : JsonObject) : ExternalDocumentation = + let description = asOpt node "description" + let url = asString node "url" |> Uri + + { + Description = description + Url = url + } + +type Schema = + | Schema of unit + + static member Parse (_ : JsonObject) : Schema = Schema () type Example = { @@ -22,12 +34,38 @@ type Example = Value : Choice option } + static member Parse (node : JsonObject) : Example = + let description = asOpt node "description" + let summary = asOpt node "summary" + let externalValue = asOpt node "externalValue" |> Option.map Uri + + let value = + match externalValue with + | Some u -> Choice2Of2 u |> Some + | None -> + match node.TryGetPropertyValue "value" with + | true, v -> Choice1Of2 v |> Some + | false, _ -> None + + { + Summary = summary + Description = description + Value = value + } + type Reference = { /// The reference string. Ref : string } + static member Parse (node : JsonObject) : Reference = + let ref = asString node "$ref" + + { + Ref = ref + } + type Tag = { /// The name of the tag. @@ -38,6 +76,17 @@ type Tag = ExternalDocs : ExternalDocumentation option } + static member Parse (node : JsonObject) : Tag = + let name = asString node "name" + let description = asOpt node "description" + let docs = asObjOpt node "externalDocs" |> Option.map ExternalDocumentation.Parse + + { + Name = name + Description = description + ExternalDocs = docs + } + type ServerVariable = { /// An enumeration of string values to be used if the substitution options are from a limited set. @@ -49,18 +98,48 @@ type ServerVariable = Description : string option } + static member Parse (node : JsonObject) : ServerVariable = + let enum = asArrOpt' node "enum" + let default' = asString node "default" + let description = asOpt node "description" + + { + Enum = enum + Default = default' + Description = description + } + type Server = { /// A URL to the target host. /// This URL supports Server Variables and MAY be relative, to indicate that the host location is relative to the location where the OpenAPI document is being served. /// Variable substitutions will be made when a variable is named in {brackets}. - Url : Uri + Url : string /// Describes the host designated by the URL, possibly with CommonMark. Description : string option /// Used for substituting in the Url. Variables : Map option } + static member Parse (node : JsonObject) : Server = + let url = asString node "url" + let description = asOpt node "description" + + let variables = + match node.TryGetPropertyValue "variables" with + | false, _ -> None + | true, o -> + o.AsObject () + |> Seq.map (fun (KeyValue (k, v)) -> k, ServerVariable.Parse (v.AsObject ())) + |> Map.ofSeq + |> Some + + { + Url = url + Description = description + Variables = variables + } + type StringFormat = /// base64-encoded characters | Byte @@ -102,6 +181,17 @@ type Contact = Email : string option } + static member Parse (node : JsonObject) : Contact = + let name = asOpt node "name" + let url = asOpt node "url" |> Option.map Uri + let email = asOpt node "email" + + { + Email = email + Url = url + Name = name + } + type License = { /// The license name used for the API. @@ -110,6 +200,15 @@ type License = Url : Uri option } + static member Parse (node : JsonObject) : License = + let url = asOpt node "url" |> Option.map Uri + let name = asString node "name" + + { + Name = name + Url = url + } + type OpenApiInfo = { /// Title of the application @@ -126,6 +225,23 @@ type OpenApiInfo = Version : string } + static member Parse (node : JsonObject) : OpenApiInfo = + let title = asString node "title" + let version = asString node "version" + let desc = asOpt node "description" + let termsOfService = asOpt node "termsOfService" |> Option.map Uri + let contact = asObjOpt node "contact" |> Option.map Contact.Parse + let license = asObjOpt node "license" |> Option.map License.Parse + + { + Title = title + Description = desc + TermsOfService = termsOfService + Contact = contact + License = license + Version = version + } + type Encoding = { /// The Content-Type for encoding a specific property. @@ -158,6 +274,39 @@ type Encoding = AllowReserved : bool option } + static member Parse (node : JsonObject) : Encoding = + let contentType = asOpt node "contentType" + let style = asOpt node "style" + let explode = asOpt node "explode" + let allowReserved = asOpt node "allowReserved" + + let headers = + match node.TryGetPropertyValue "headers" with + | false, _ -> None + | true, o -> + o.AsObject () + |> Seq.map (fun (KeyValue (k, v)) -> + let obj = v.AsObject () + + let parsed = + if obj.ContainsKey "$ref" then + Choice2Of2 (Reference.Parse obj) + else + Choice1Of2 (Header.Parse obj) + + k, parsed + ) + |> Map.ofSeq + |> Some + + { + ContentType = contentType + Headers = headers + Style = style + Explode = explode + AllowReserved = allowReserved + } + and MediaType = { /// The schema defining the type used for the request body. @@ -169,6 +318,56 @@ and MediaType = Encoding : Map option } + static member Parse (node : JsonObject) : MediaType = + let schema = + match node.TryGetPropertyValue "schema" with + | false, _ -> None + | true, s -> + let obj = s.AsObject () + + if obj.ContainsKey "$ref" then + Choice2Of2 (Reference.Parse obj) |> Some + else + Choice1Of2 (Schema.Parse obj) |> Some + + let example = + match node.TryGetPropertyValue "example" with + | true, e -> Choice1Of2 e |> Some + | false, _ -> + match node.TryGetPropertyValue "examples" with + | false, _ -> None + | true, e -> + e.AsObject () + |> Seq.map (fun (KeyValue (k, v)) -> + let obj = v.AsObject () + + let parsed = + if obj.ContainsKey "$ref" then + Choice2Of2 (Reference.Parse obj) + else + Choice1Of2 (Example.Parse obj) + + k, parsed + ) + |> Map.ofSeq + |> Choice2Of2 + |> Some + + let encoding = + match node.TryGetPropertyValue "encoding" with + | false, _ -> None + | true, e -> + e.AsObject () + |> Seq.map (fun (KeyValue (k, v)) -> k, Encoding.Parse (v.AsObject ())) + |> Map.ofSeq + |> Some + + { + Schema = schema + Example = example + Encoding = encoding + } + /// The Header Object basically follows the structure of the Parameter Object. /// All traits that are affected by the location MUST be applicable to a location of header (for example, style). and Header = @@ -208,6 +407,71 @@ and Header = Content : Map option } + static member Parse (node : JsonObject) : Header = + let description = asOpt node "description" + let required = asOpt node "required" + let deprecated = asOpt node "deprecated" + let allowEmptyValue = asOpt node "allowEmptyValue" + let style = asOpt node "style" + let explode = asOpt node "explode" + let allowReserved = asOpt node "allowReserved" + + let schema = + match node.TryGetPropertyValue "schema" with + | false, _ -> None + | true, s -> + let obj = s.AsObject () + + if obj.ContainsKey "$ref" then + Choice2Of2 (Reference.Parse obj) |> Some + else + Choice1Of2 (Schema.Parse obj) |> Some + + let example = + match node.TryGetPropertyValue "example" with + | true, e -> Choice1Of2 e |> Some + | false, _ -> + match node.TryGetPropertyValue "examples" with + | false, _ -> None + | true, e -> + e.AsObject () + |> Seq.map (fun (KeyValue (k, v)) -> + let obj = v.AsObject () + + let parsed = + if obj.ContainsKey "$ref" then + Choice2Of2 (Reference.Parse obj) + else + Choice1Of2 (Example.Parse obj) + + k, parsed + ) + |> Map.ofSeq + |> Choice2Of2 + |> Some + + let content = + match node.TryGetPropertyValue "content" with + | false, _ -> None + | true, c -> + c.AsObject () + |> Seq.map (fun (KeyValue (k, v)) -> k, MediaType.Parse (v.AsObject ())) + |> Map.ofSeq + |> Some + + { + Description = description + Required = required + Deprecated = deprecated + AllowEmptyValue = allowEmptyValue + Style = style + Explode = explode + AllowReserved = allowReserved + Schema = schema + Example = example + Content = content + } + type LinkOperation = /// A relative or absolute reference to an OAS operation. /// This field is mutually exclusive of the operationId field, and MUST point to an Operation Object. @@ -236,6 +500,44 @@ type Link = Server : Server option } + static member Parse (node : JsonObject) : Link = + let operation = + match node.TryGetPropertyValue "operationRef" with + | true, ref -> LinkOperation.Ref (ref.GetValue ()) |> Some + | false, _ -> + match node.TryGetPropertyValue "operationId" with + | true, id -> LinkOperation.Id (id.GetValue ()) |> Some + | false, _ -> None + + let parameters = + match node.TryGetPropertyValue "parameters" with + | false, _ -> None + | true, p -> + p.AsObject () + |> Seq.map (fun (KeyValue (k, v)) -> + // In OpenAPI spec, this can be any value or {expression} + // For simplicity, treating non-string values as JsonNode + k, Choice1Of2 v + ) + |> Map.ofSeq + |> Some + + let requestBody = + match node.TryGetPropertyValue "requestBody" with + | false, _ -> None + | true, rb -> Choice1Of2 rb |> Some + + let description = asOpt node "description" + let server = asObjOpt node "server" |> Option.map Server.Parse + + { + Operation = operation + Parameters = parameters + RequestBody = requestBody + Description = description + Server = server + } + type Response = { /// A short description of the response, possibly CommonMark. @@ -253,6 +555,63 @@ type Response = Links : Map> option } + static member Parse (node : JsonObject) : Response = + let description = asString node "description" + + let headers = + match node.TryGetPropertyValue "headers" with + | false, _ -> None + | true, h -> + h.AsObject () + |> Seq.map (fun (KeyValue (k, v)) -> + let obj = v.AsObject () + + let parsed = + if obj.ContainsKey "$ref" then + Choice2Of2 (Reference.Parse obj) + else + Choice1Of2 (Header.Parse obj) + + k, parsed + ) + |> Map.ofSeq + |> Some + + let content = + match node.TryGetPropertyValue "content" with + | false, _ -> None + | true, c -> + c.AsObject () + |> Seq.map (fun (KeyValue (k, v)) -> k, MediaType.Parse (v.AsObject ())) + |> Map.ofSeq + |> Some + + let links = + match node.TryGetPropertyValue "links" with + | false, _ -> None + | true, l -> + l.AsObject () + |> Seq.map (fun (KeyValue (k, v)) -> + let obj = v.AsObject () + + let parsed = + if obj.ContainsKey "$ref" then + Choice2Of2 (Reference.Parse obj) + else + Choice1Of2 (Link.Parse obj) + + k, parsed + ) + |> Map.ofSeq + |> Some + + { + Description = description + Headers = headers + Content = content + Links = links + } + type Responses = { /// The documentation of responses other than the ones declared for specific HTTP response codes. @@ -263,6 +622,45 @@ type Responses = Patterns : Map> option } + static member Parse (node : JsonObject) : Responses = + let default' = + match node.TryGetPropertyValue "default" with + | false, _ -> None + | true, d -> + let obj = d.AsObject () + + if obj.ContainsKey "$ref" then + Choice2Of2 (Reference.Parse obj) |> Some + else + Choice1Of2 (Response.Parse obj) |> Some + + // All other properties are HTTP status codes + let patterns = + node + |> Seq.choose (fun (KeyValue (k, v)) -> + if k = "default" then + None + else + let obj = v.AsObject () + + let parsed = + if obj.ContainsKey "$ref" then + Choice2Of2 (Reference.Parse obj) + else + Choice1Of2 (Response.Parse obj) + + Some (k, parsed) + ) + |> Map.ofSeq + |> function + | m when m.IsEmpty -> None + | m -> Some m + + { + Default = default' + Patterns = patterns + } + type SecuritySchemeIn = | Query | Header @@ -271,15 +669,34 @@ type SecuritySchemeIn = type OauthFlow = { /// The authorization URL to be used for this flow. - AuthorizationUrl : Uri + /// Required for "implicit" and "authorizationCode". + AuthorizationUrl : Uri option /// The token URL to be used for this flow. - TokenUrl : Uri + /// Required for "password", "clientCredentials", "authorizationCode". + TokenUrl : Uri option /// The URL to be used for obtaining refresh tokens. RefreshUrl : Uri option /// The available scopes for the OAuth2 security scheme. A map between the scope name and a short description for it. - Scopes : Map + /// Required for "oauth2". + Scopes : Map option } + static member Parse (node : JsonObject) : OauthFlow = + let authorizationUrl = asOpt node "authorizationUrl" |> Option.map Uri + let tokenUrl = asOpt node "tokenUrl" |> Option.map Uri + let refreshUrl = asOpt node "refreshUrl" |> Option.map Uri + + let scopes = + asObjOpt node "scopes" + |> Option.map (fun s -> s |> Seq.map (fun (KeyValue (k, v)) -> k, v.GetValue ()) |> Map.ofSeq) + + { + AuthorizationUrl = authorizationUrl + TokenUrl = tokenUrl + RefreshUrl = refreshUrl + Scopes = scopes + } + type SecurityRequirement = { /// Each name MUST correspond to a security scheme which is declared in the Security Schemes under the Components Object. @@ -288,24 +705,86 @@ type SecurityRequirement = Fields : Map option } + static member Parse (node : JsonObject) : SecurityRequirement = + // The entire object is the security requirement + let fields = + node + |> Seq.map (fun (KeyValue (k, v)) -> + let scopes = v.AsArray () |> Seq.map (fun s -> s.GetValue ()) |> Seq.toList + k, scopes + ) + |> Map.ofSeq + |> function + | m when m.IsEmpty -> None + | m -> Some m + + { + Fields = fields + } + type OauthFlows = { /// Configuration for the OAuth Implicit flow - Implicit : OauthFlow + Implicit : OauthFlow option /// Configuration for the OAuth Resource Owner Password flow - Password : OauthFlow + Password : OauthFlow option /// Configuration for the OAuth Client Credentials flow. - ClientCredentials : OauthFlow + ClientCredentials : OauthFlow option /// Configuration for the OAuth Authorization Code flow. - AuthorizationCode : OauthFlow + AuthorizationCode : OauthFlow option } + static member Parse (node : JsonObject) : OauthFlows = + let implicit = asObjOpt node "implicit" |> Option.map OauthFlow.Parse + let password = asObjOpt node "password" |> Option.map OauthFlow.Parse + + let clientCredentials = + asObjOpt node "clientCredentials" |> Option.map OauthFlow.Parse + + let authorizationCode = + asObjOpt node "authorizationCode" |> Option.map OauthFlow.Parse + + { + Implicit = implicit + Password = password + ClientCredentials = clientCredentials + AuthorizationCode = authorizationCode + } + type SecurityScheme = | ApiKey of description : string option * name : string * inValue : SecuritySchemeIn | Http of description : string option * scheme : string * bearerFormat : string option | Oauth2 of description : string option * OauthFlows | OpenIdConnect of description : string option * url : Uri + static member Parse (node : JsonObject) : SecurityScheme = + let type' = asString node "type" + let description = asOpt node "description" + + match type' with + | "apiKey" -> + let name = asString node "name" + + let inValue = + match asString node "in" with + | "query" -> SecuritySchemeIn.Query + | "header" -> SecuritySchemeIn.Header + | "cookie" -> SecuritySchemeIn.Cookie + | other -> failwithf "Unknown 'in' value for apiKey: %s" other + + SecurityScheme.ApiKey (description, name, inValue) + | "http" -> + let scheme = asString node "scheme" + let bearerFormat = asOpt node "bearerFormat" + SecurityScheme.Http (description, scheme, bearerFormat) + | "oauth2" -> + let flows = asObj node "flows" |> OauthFlows.Parse + SecurityScheme.Oauth2 (description, flows) + | "openIdConnect" -> + let url = asString node "openIdConnectUrl" |> Uri + SecurityScheme.OpenIdConnect (description, url) + | other -> failwithf "Unknown security scheme type: %s" other + type ParameterIn = /// Used together with Path Templating, where the parameter value is actually part of the operation’s URL. /// This does not include the host or base path of the API. @@ -365,6 +844,83 @@ type Parameter = Content : Map option } + static member Parse (node : JsonObject) : Parameter = + let name = asString node "name" + + let in' = + match asString node "in" with + | "path" -> ParameterIn.Path + | "header" -> ParameterIn.Header + | "query" -> ParameterIn.Query + | "cookie" -> ParameterIn.Cookie + | other -> failwithf "Unknown 'in' value for parameter: %s" other + + let description = asOpt node "description" + let required = asOpt node "required" + let deprecated = asOpt node "deprecated" + let allowEmptyValue = asOpt node "allowEmptyValue" + let style = asOpt node "style" + let explode = asOpt node "explode" + let allowReserved = asOpt node "allowReserved" + + let schema = + match node.TryGetPropertyValue "schema" with + | false, _ -> None + | true, s -> + let obj = s.AsObject () + + if obj.ContainsKey "$ref" then + Choice2Of2 (Reference.Parse obj) |> Some + else + Choice1Of2 (Schema.Parse obj) |> Some + + let example = + match node.TryGetPropertyValue "example" with + | true, e -> Choice1Of2 e |> Some + | false, _ -> + match node.TryGetPropertyValue "examples" with + | false, _ -> None + | true, e -> + e.AsObject () + |> Seq.map (fun (KeyValue (k, v)) -> + let obj = v.AsObject () + + let parsed = + if obj.ContainsKey "$ref" then + Choice2Of2 (Reference.Parse obj) + else + Choice1Of2 (Example.Parse obj) + + k, parsed + ) + |> Map.ofSeq + |> Choice2Of2 + |> Some + + let content = + match node.TryGetPropertyValue "content" with + | false, _ -> None + | true, c -> + c.AsObject () + |> Seq.map (fun (KeyValue (k, v)) -> k, MediaType.Parse (v.AsObject ())) + |> Map.ofSeq + |> Some + + { + Name = name + In = in' + Description = description + Required = required + Deprecated = deprecated + AllowEmptyValue = allowEmptyValue + Style = style + Explode = explode + AllowReserved = allowReserved + Schema = schema + Example = example + Content = content + } + type RequestBody = { /// A brief description of the request body. This could contain examples of use. @@ -378,12 +934,41 @@ type RequestBody = Required : bool option } + static member Parse (node : JsonObject) : RequestBody = + let description = asOpt node "description" + + let content = + asObj node "content" + |> Seq.map (fun (KeyValue (k, v)) -> k, MediaType.Parse (v.AsObject ())) + |> Map.ofSeq + + let required = asOpt node "required" + + { + Description = description + Content = content + Required = required + } + type Callback = { /// For the semantics of the keys, see https://spec.openapis.org/oas/v3.0.0#key-expression Patterns : Map option } + static member Parse (node : JsonObject) : Callback = + let patterns = + node + |> Seq.map (fun (KeyValue (k, v)) -> k, PathItem.Parse (v.AsObject ())) + |> Map.ofSeq + |> function + | m when m.IsEmpty -> None + | m -> Some m + + { + Patterns = patterns + } + and Operation = { /// A list of tags for API documentation control. @@ -394,7 +979,7 @@ and Operation = /// A verbose explanation of the operation behavior, possibly in CommonMark. Description : string option /// Additional external documentation for this operation. - ExternalDocs : ExternalDocumentation + ExternalDocs : ExternalDocumentation option /// Unique string used to identify the operation. /// The id MUST be unique among all operations described in the API. /// Tools and libraries MAY use the operationId to uniquely identify an operation, therefore, @@ -411,7 +996,8 @@ and Operation = /// In other cases where the HTTP spec is vague, requestBody SHALL be ignored by consumers. RequestBody : Choice option /// The list of possible responses as they are returned from executing this operation. - Responses : Responses + /// Per the spec these are required, but one of the official examples lacks them. + Responses : Responses option /// A map of possible out-of band callbacks related to the parent operation. /// The key is a unique identifier for the Callback Object. /// Each value in the map is a Callback Object that describes a request that may be initiated by the API provider and the expected responses. @@ -430,6 +1016,99 @@ and Operation = Servers : Server list option } + static member Parse (node : JsonObject) : Operation = + let tags = asArrOpt' node "tags" + let summary = asOpt node "summary" + let description = asOpt node "description" + + let externalDocs = + asObjOpt node "externalDocs" |> Option.map ExternalDocumentation.Parse + + let operationId = asOpt node "operationId" + + let parameters = + match node.TryGetPropertyValue "parameters" with + | false, _ -> None + | true, p -> + p.AsArray () + |> Seq.map (fun v -> + let obj = v.AsObject () + + if obj.ContainsKey "$ref" then + Choice2Of2 (Reference.Parse obj) + else + Choice1Of2 (Parameter.Parse obj) + ) + |> Seq.toList + |> Some + + let requestBody = + match node.TryGetPropertyValue "requestBody" with + | false, _ -> None + | true, rb -> + let obj = rb.AsObject () + + if obj.ContainsKey "$ref" then + Choice2Of2 (Reference.Parse obj) |> Some + else + Choice1Of2 (RequestBody.Parse obj) |> Some + + let responses = asObjOpt node "responses" |> Option.map Responses.Parse + + let callbacks = + match node.TryGetPropertyValue "callbacks" with + | false, _ -> None + | true, c -> + c.AsObject () + |> Seq.map (fun (KeyValue (k, v)) -> + let obj = v.AsObject () + + let parsed = + if obj.ContainsKey "$ref" then + Choice2Of2 (Reference.Parse obj) + else + Choice1Of2 (Callback.Parse obj) + + k, parsed + ) + |> Map.ofSeq + |> Some + + let deprecated = asOpt node "deprecated" + + let security = + match node.TryGetPropertyValue "security" with + | false, _ -> None + | true, s -> + s.AsArray () + |> Seq.map (fun v -> SecurityRequirement.Parse (v.AsObject ())) + |> Seq.toList + |> Some + + let servers = + match node.TryGetPropertyValue "servers" with + | false, _ -> None + | true, s -> + s.AsArray () + |> Seq.map (fun v -> Server.Parse (v.AsObject ())) + |> Seq.toList + |> Some + + { + Tags = tags + Summary = summary + Description = description + ExternalDocs = externalDocs + OperationId = operationId + Parameters = parameters + RequestBody = requestBody + Responses = responses + Callbacks = callbacks + Deprecated = deprecated + Security = security + Servers = servers + } + and PathItem = { /// Allows for an external definition of this path item. @@ -466,6 +1145,60 @@ and PathItem = Parameters : Choice list option } + static member Parse (node : JsonObject) : PathItem = + let ref = asOpt node "$ref" + let summary = asOpt node "summary" + let description = asOpt node "description" + let get = asObjOpt node "get" |> Option.map Operation.Parse + let put = asObjOpt node "put" |> Option.map Operation.Parse + let post = asObjOpt node "post" |> Option.map Operation.Parse + let delete = asObjOpt node "delete" |> Option.map Operation.Parse + let options = asObjOpt node "options" |> Option.map Operation.Parse + let head = asObjOpt node "head" |> Option.map Operation.Parse + let patch = asObjOpt node "patch" |> Option.map Operation.Parse + let trace = asObjOpt node "trace" |> Option.map Operation.Parse + + let servers = + match node.TryGetPropertyValue "servers" with + | false, _ -> None + | true, s -> + s.AsArray () + |> Seq.map (fun v -> Server.Parse (v.AsObject ())) + |> Seq.toList + |> Some + + let parameters = + match node.TryGetPropertyValue "parameters" with + | false, _ -> None + | true, p -> + p.AsArray () + |> Seq.map (fun v -> + let obj = v.AsObject () + + if obj.ContainsKey "$ref" then + Choice2Of2 (Reference.Parse obj) + else + Choice1Of2 (Parameter.Parse obj) + ) + |> Seq.toList + |> Some + + { + Ref = ref + Summary = summary + Description = description + Get = get + Put = put + Post = post + Delete = delete + Options = options + Head = head + Patch = patch + Trace = trace + Servers = servers + Parameters = parameters + } + type Paths = { /// A relative path to an individual endpoint. @@ -478,27 +1211,122 @@ type Paths = Fields : Map option } + static member Parse (node : JsonObject) : Paths = + let fields = + node + |> Seq.map (fun (KeyValue (k, v)) -> k, PathItem.Parse (v.AsObject ())) + |> Map.ofSeq + |> function + | m when m.IsEmpty -> None + | m -> Some m + + { + Fields = fields + } + type Components = { - Schemas : Map> - Responses : Map> - Parameters : Map> - Examples : Map> - RequestBodies : Map> - Headers : Map> - SecuritySchemes : Map> - Links : Map> - Callbacks : Map> + Schemas : Map> option + Responses : Map> option + Parameters : Map> option + Examples : Map> option + RequestBodies : Map> option + Headers : Map> option + SecuritySchemes : Map> option + Links : Map> option + Callbacks : Map> option } + static member Parse (node : JsonObject) : Components = + let parseMap (key : string) (parser : JsonObject -> 'T) = + match node.TryGetPropertyValue key with + | false, _ -> None + | true, o -> + o.AsObject () + |> Seq.map (fun (KeyValue (k, v)) -> + let obj = v.AsObject () + + let parsed = + if obj.ContainsKey "$ref" then + Choice2Of2 (Reference.Parse obj) + else + Choice1Of2 (parser obj) + + k, parsed + ) + |> Map.ofSeq + |> Some + + { + Schemas = parseMap "schemas" Schema.Parse + Responses = parseMap "responses" Response.Parse + Parameters = parseMap "parameters" Parameter.Parse + Examples = parseMap "examples" Example.Parse + RequestBodies = parseMap "requestBodies" RequestBody.Parse + Headers = parseMap "headers" Header.Parse + SecuritySchemes = parseMap "securitySchemes" SecurityScheme.Parse + Links = parseMap "links" Link.Parse + Callbacks = parseMap "callbacks" Callback.Parse + } + type OpenApiSpec = { OpenApi : Version Info : OpenApiInfo Servers : Server list option - Paths : Paths + /// According to the spec, this is required, but then one of their own examples + /// does not contain a Paths. + Paths : Paths option Components : Components option Security : SecurityRequirement list option Tags : Tag list option ExternalDocs : ExternalDocumentation option } + + static member Parse (node : JsonObject) : OpenApiSpec = + let openapi = asString node "openapi" |> Version + let info = asObj node "info" |> OpenApiInfo.Parse + + let servers = + match node.TryGetPropertyValue "servers" with + | false, _ -> None + | true, s -> + s.AsArray () + |> Seq.map (fun v -> Server.Parse (v.AsObject ())) + |> Seq.toList + |> Some + + let paths = asObjOpt node "paths" |> Option.map Paths.Parse + let components = asObjOpt node "components" |> Option.map Components.Parse + + let security = + match node.TryGetPropertyValue "security" with + | false, _ -> None + | true, s -> + s.AsArray () + |> Seq.map (fun v -> SecurityRequirement.Parse (v.AsObject ())) + |> Seq.toList + |> Some + + let tags = + match node.TryGetPropertyValue "tags" with + | false, _ -> None + | true, t -> + t.AsArray () + |> Seq.map (fun v -> Tag.Parse (v.AsObject ())) + |> Seq.toList + |> Some + + let externalDocs = + asObjOpt node "externalDocs" |> Option.map ExternalDocumentation.Parse + + { + OpenApi = openapi + Info = info + Servers = servers + Paths = paths + Components = components + Security = security + Tags = tags + ExternalDocs = externalDocs + }