From d5ede86fc4c55978bf86fe30f943f3577c7910bc Mon Sep 17 00:00:00 2001 From: Tuomas Hietanen Date: Fri, 29 May 2020 15:31:24 +0100 Subject: [PATCH 1/3] Initial test and code clean-up --- .../Provider.OpenApiClient.fs | 14 ++------- .../Provider.SwaggerClient.fs | 27 ++--------------- src/SwaggerProvider.DesignTime/Utils.fs | 30 ++++++++++++++++++- .../v2/Parser/Parsers.fs | 15 +++++++++- .../v2/Schema.Spec.Yaml.Tests.fs | 12 ++++++++ tests/SwaggerProvider.Tests/v2/token.yaml | 19 ++++++++++++ 6 files changed, 79 insertions(+), 38 deletions(-) create mode 100644 tests/SwaggerProvider.Tests/v2/token.yaml diff --git a/src/SwaggerProvider.DesignTime/Provider.OpenApiClient.fs b/src/SwaggerProvider.DesignTime/Provider.OpenApiClient.fs index 3fdb1fa2..36292771 100644 --- a/src/SwaggerProvider.DesignTime/Provider.OpenApiClient.fs +++ b/src/SwaggerProvider.DesignTime/Provider.OpenApiClient.fs @@ -56,18 +56,8 @@ type public OpenApiClientTypeProvider(cfg : TypeProviderConfig) as this = | Some(ty) -> ty | None -> let schemaData = - match schemaPathRaw.StartsWith("http", true, null) with - | true -> - let request = new HttpRequestMessage(HttpMethod.Get, schemaPathRaw) - // using a custom handler means that we can set the default credentials. - use handler = new HttpClientHandler(UseDefaultCredentials = true) - use client = new HttpClient(handler) - async { - let! response = client.SendAsync(request) |> Async.AwaitTask - return! response.Content.ReadAsStringAsync() |> Async.AwaitTask - } |> Async.RunSynchronously - | false -> - schemaPathRaw |> IO.File.ReadAllText + SwaggerProvider.Internal.SchemaReader.readSchemaPath "" schemaPathRaw + |> Async.RunSynchronously let openApiReader = Microsoft.OpenApi.Readers.OpenApiStringReader() let (schema, diagnostic) = openApiReader.Read(schemaData) diff --git a/src/SwaggerProvider.DesignTime/Provider.SwaggerClient.fs b/src/SwaggerProvider.DesignTime/Provider.SwaggerClient.fs index 7fb71b79..5503e479 100644 --- a/src/SwaggerProvider.DesignTime/Provider.SwaggerClient.fs +++ b/src/SwaggerProvider.DesignTime/Provider.SwaggerClient.fs @@ -1,8 +1,7 @@ -namespace SwaggerProvider +namespace SwaggerProvider open System open System.Reflection -open System.Net.Http open ProviderImplementation.ProvidedTypes open Microsoft.FSharp.Core.CompilerServices open Swagger @@ -71,28 +70,8 @@ type public SwaggerTypeProvider(cfg : TypeProviderConfig) as this = | Some(ty) -> ty | None -> let schemaData = - match schemaPathRaw.StartsWith("http", true, null) with - | true -> - let headers = - headersStr.Split('|') - |> Seq.choose (fun x -> - let pair = x.Split('=') - if (pair.Length = 2) - then Some (pair.[0],pair.[1]) - else None - ) - let request = new HttpRequestMessage(HttpMethod.Get, schemaPathRaw) - for (name, value) in headers do - request.Headers.TryAddWithoutValidation(name, value) |> ignore - // using a custom handler means that we can set the default credentials. - use handler = new HttpClientHandler(UseDefaultCredentials = true) - use client = new HttpClient(handler) - async { - let! response = client.SendAsync(request) |> Async.AwaitTask - return! response.Content.ReadAsStringAsync() |> Async.AwaitTask - } |> Async.RunSynchronously - | false -> - schemaPathRaw |> IO.File.ReadAllText + SwaggerProvider.Internal.SchemaReader.readSchemaPath headersStr schemaPathRaw + |> Async.RunSynchronously let schema = SwaggerParser.parseSchema schemaData let defCompiler = DefinitionCompiler(schema, preferNullable) diff --git a/src/SwaggerProvider.DesignTime/Utils.fs b/src/SwaggerProvider.DesignTime/Utils.fs index 0a4b79b3..919602f6 100644 --- a/src/SwaggerProvider.DesignTime/Utils.fs +++ b/src/SwaggerProvider.DesignTime/Utils.fs @@ -1,4 +1,32 @@ -namespace SwaggerProvider.Internal +namespace SwaggerProvider.Internal + +module SchemaReader = + open System + open System.Net.Http + + let readSchemaPath (headersStr:string) (schemaPathRaw:string) = + async { + match schemaPathRaw.StartsWith("http", true, null) with + | true -> + let headers = + headersStr.Split('|') + |> Seq.choose (fun x -> + let pair = x.Split('=') + if (pair.Length = 2) + then Some (pair.[0],pair.[1]) + else None + ) + let request = new HttpRequestMessage(HttpMethod.Get, schemaPathRaw) + for (name, value) in headers do + request.Headers.TryAddWithoutValidation(name, value) |> ignore + // using a custom handler means that we can set the default credentials. + use handler = new HttpClientHandler(UseDefaultCredentials = true) + use client = new HttpClient(handler) + let! response = client.SendAsync(request) |> Async.AwaitTask + return! response.Content.ReadAsStringAsync() |> Async.AwaitTask + | false -> + return schemaPathRaw |> IO.File.ReadAllText + } type UniqueNameGenerator() = let hash = System.Collections.Generic.HashSet<_>() diff --git a/src/SwaggerProvider.DesignTime/v2/Parser/Parsers.fs b/src/SwaggerProvider.DesignTime/v2/Parser/Parsers.fs index cd4db071..a2a947a8 100644 --- a/src/SwaggerProvider.DesignTime/v2/Parser/Parsers.fs +++ b/src/SwaggerProvider.DesignTime/v2/Parser/Parsers.fs @@ -412,7 +412,20 @@ module Parsers = | "options" -> Some <| parseOperationObject context path Options obj | "head" -> Some <| parseOperationObject context path Head obj | "patch" -> Some <| parseOperationObject context path Patch obj - | "$ref" -> failwith "External definition of this path item is not supported yet" + | "$ref" -> + let fileName = obj.AsString() + let path = + // If path is empty: + // We could match something like interactive __SOURCE_DIRECTORY__ + // or else (System.Reflection.Assembly.GetExecutingAssembly().Location |> System.IO.Path.GetDirectoryName) + obj.GetStringSafe("basePath") + let filePath = System.IO.Path.Combine [| path; (if fileName.Contains("#") then fileName.Split('#').[0] else fileName) |] + + let schemaData = + SwaggerProvider.Internal.SchemaReader.readSchemaPath "" filePath + |> Async.RunSynchronously + + failwith "External definition of this path item is not supported yet" | _ -> None let updateContext (pathItemObj:SchemaNode) = match pathItemObj.TryGetProperty("parameters") with diff --git a/tests/SwaggerProvider.Tests/v2/Schema.Spec.Yaml.Tests.fs b/tests/SwaggerProvider.Tests/v2/Schema.Spec.Yaml.Tests.fs index d4dac7be..d029afb9 100644 --- a/tests/SwaggerProvider.Tests/v2/Schema.Spec.Yaml.Tests.fs +++ b/tests/SwaggerProvider.Tests/v2/Schema.Spec.Yaml.Tests.fs @@ -821,4 +821,16 @@ GeneralError: } |] |> Map.ofArray) Expect.equal actual expected "Responses Definitions Object" + + + ptestCase "External reference test" <| fun _ -> // Ignore("Not supported") + """ +# What should be the relative path? e.g. $ref: ../v2/token.yaml#/token +/tokens: + $ref: ./tests/SwaggerProvider.Tests/v2/token.yaml#/token +""" + |> SwaggerParser.parseYaml + |> Parsers.parsePathsObject Parsers.ParserContext.Empty + |> fun actual -> + Expect.equal (actual.ToString()) "token" "External reference test" ] diff --git a/tests/SwaggerProvider.Tests/v2/token.yaml b/tests/SwaggerProvider.Tests/v2/token.yaml new file mode 100644 index 00000000..41e6147e --- /dev/null +++ b/tests/SwaggerProvider.Tests/v2/token.yaml @@ -0,0 +1,19 @@ + token: + post: + summary: Just a test + operationId: test_token + requestBody: + required: true + content: + application/json: + schema: + $ref: ../v2/token.yaml + responses: + "200": + description: Generated + content: + application/json: + schema: + $ref: ../v2/token.yaml + default: + $ref: ../error.yaml From 50416f03b1e9476d59af9ff06434ee447a1dfd48 Mon Sep 17 00:00:00 2001 From: Tuomas Hietanen Date: Fri, 29 May 2020 18:18:45 +0100 Subject: [PATCH 2/3] fixed caching --- src/SwaggerProvider.DesignTime/Caching.fs | 7 ++++++- src/SwaggerProvider.DesignTime/Provider.SwaggerClient.fs | 6 ++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/SwaggerProvider.DesignTime/Caching.fs b/src/SwaggerProvider.DesignTime/Caching.fs index 64473640..8f4ee42a 100644 --- a/src/SwaggerProvider.DesignTime/Caching.fs +++ b/src/SwaggerProvider.DesignTime/Caching.fs @@ -74,6 +74,7 @@ type ICache<'TKey, 'TValue> = abstract Set : key:'TKey * value:'TValue -> unit abstract TryRetrieve : key:'TKey * ?extendCacheExpiration:bool -> 'TValue option abstract Remove : key:'TKey -> unit + abstract GetOrAdd : key:'TKey * valueFactory:(unit -> 'TValue) -> 'TValue /// Creates a cache that uses in-memory collection let createInMemoryCache (expiration:TimeSpan) = @@ -106,4 +107,8 @@ let createInMemoryCache (expiration:TimeSpan) = match dict.TryRemove(key) with | true, _ -> log (sprintf "Explicitly removed from cache: %O" key) | _ -> () - } \ No newline at end of file + member __.GetOrAdd(key, valueFactory) = + let res, _ = dict.GetOrAdd(key, fun k -> valueFactory(), DateTime.UtcNow) + invalidationFunction key |> Async.Start + res + } diff --git a/src/SwaggerProvider.DesignTime/Provider.SwaggerClient.fs b/src/SwaggerProvider.DesignTime/Provider.SwaggerClient.fs index 5503e479..735d9c3b 100644 --- a/src/SwaggerProvider.DesignTime/Provider.SwaggerClient.fs +++ b/src/SwaggerProvider.DesignTime/Provider.SwaggerClient.fs @@ -66,9 +66,7 @@ type public SwaggerTypeProvider(cfg : TypeProviderConfig) as this = (schemaPathRaw, headersStr, ignoreOperationId, ignoreControllerPrefix, preferNullable, preferAsync) |> sprintf "%A" - match Cache.providedTypes.TryRetrieve(cacheKey) with - | Some(ty) -> ty - | None -> + let addCache() = let schemaData = SwaggerProvider.Internal.SchemaReader.readSchemaPath headersStr schemaPathRaw |> Async.RunSynchronously @@ -85,8 +83,8 @@ type public SwaggerTypeProvider(cfg : TypeProviderConfig) as this = ty.AddMembers tys tempAsm.AddTypes [ty] - Cache.providedTypes.Set(cacheKey, ty) ty + Cache.providedTypes.GetOrAdd(cacheKey, addCache) ) t do From 3e52bdf4103ca72e487d678565b901cc4a4e6d5b Mon Sep 17 00:00:00 2001 From: Tuomas Hietanen Date: Sat, 30 May 2020 10:36:48 +0100 Subject: [PATCH 3/3] If response statuscode fails, there might be still an usefull message at response body. --- src/SwaggerProvider.DesignTime/Utils.fs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/SwaggerProvider.DesignTime/Utils.fs b/src/SwaggerProvider.DesignTime/Utils.fs index 919602f6..696a4482 100644 --- a/src/SwaggerProvider.DesignTime/Utils.fs +++ b/src/SwaggerProvider.DesignTime/Utils.fs @@ -22,8 +22,21 @@ module SchemaReader = // using a custom handler means that we can set the default credentials. use handler = new HttpClientHandler(UseDefaultCredentials = true) use client = new HttpClient(handler) - let! response = client.SendAsync(request) |> Async.AwaitTask - return! response.Content.ReadAsStringAsync() |> Async.AwaitTask + let! res = + async { + let! response = client.SendAsync(request) |> Async.AwaitTask + return! response.Content.ReadAsStringAsync() |> Async.AwaitTask + } |> Async.Catch + match res with + | Choice1Of2 x -> return x + | Choice2Of2 (:? System.Net.WebException as wex) -> + use stream = wex.Response.GetResponseStream() + use reader = new System.IO.StreamReader(stream) + let err = reader.ReadToEnd() + return + if String.IsNullOrEmpty err then raise wex + else err.ToString() + | Choice2Of2 e -> return failwith(e.ToString()) | false -> return schemaPathRaw |> IO.File.ReadAllText }