From 515ea306a297434e4e1a04d2e30929f762b43f7e Mon Sep 17 00:00:00 2001 From: Patrick Stevens <3138005+Smaug123@users.noreply.github.com> Date: Thu, 25 Jan 2024 19:56:44 +0000 Subject: [PATCH] Map/dictionary support, and check for null when passing to ofJson (#68) --- ConsumePlugin/ConsumePlugin.fsproj | 7 +- ConsumePlugin/GeneratedJson.fs | 12 +- ConsumePlugin/GeneratedPureGymDto.fs | 99 +++- ConsumePlugin/GeneratedVault.fs | 549 ++++++++++++++++++ ConsumePlugin/Vault.fs | 78 +++ .../TestHttpClient/TestVaultClient.fs | 170 ++++++ .../WoofWare.Myriad.Plugins.Test.fsproj | 1 + WoofWare.Myriad.Plugins/AstHelper.fs | 59 +- WoofWare.Myriad.Plugins/JsonParseGenerator.fs | 111 +++- 9 files changed, 1069 insertions(+), 17 deletions(-) create mode 100644 ConsumePlugin/GeneratedVault.fs create mode 100644 ConsumePlugin/Vault.fs create mode 100644 WoofWare.Myriad.Plugins.Test/TestHttpClient/TestVaultClient.fs diff --git a/ConsumePlugin/ConsumePlugin.fsproj b/ConsumePlugin/ConsumePlugin.fsproj index 518896a..7278c06 100644 --- a/ConsumePlugin/ConsumePlugin.fsproj +++ b/ConsumePlugin/ConsumePlugin.fsproj @@ -31,9 +31,10 @@ MockExample.fs - - runmyriad.sh - + + + Vault.fs + diff --git a/ConsumePlugin/GeneratedJson.fs b/ConsumePlugin/GeneratedJson.fs index 8c1486a..ca0132d 100644 --- a/ConsumePlugin/GeneratedJson.fs +++ b/ConsumePlugin/GeneratedJson.fs @@ -61,7 +61,17 @@ module JsonRecordType = |> Seq.map (fun elt -> elt.AsValue().GetValue ()) |> Array.ofSeq - let D = InnerType.jsonParse node.["d"] + let D = + InnerType.jsonParse ( + match node.["d"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("d") + ) + ) + | v -> v + ) let C = (match node.["hi"] with diff --git a/ConsumePlugin/GeneratedPureGymDto.fs b/ConsumePlugin/GeneratedPureGymDto.fs index d0ce354..8436efe 100644 --- a/ConsumePlugin/GeneratedPureGymDto.fs +++ b/ConsumePlugin/GeneratedPureGymDto.fs @@ -254,9 +254,41 @@ module Gym = .AsValue() .GetValue () - let Location = GymLocation.jsonParse node.["location"] - let AccessOptions = GymAccessOptions.jsonParse node.["accessOptions"] - let GymOpeningHours = GymOpeningHours.jsonParse node.["gymOpeningHours"] + let Location = + GymLocation.jsonParse ( + match node.["location"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("location") + ) + ) + | v -> v + ) + + let AccessOptions = + GymAccessOptions.jsonParse ( + match node.["accessOptions"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("accessOptions") + ) + ) + | v -> v + ) + + let GymOpeningHours = + GymOpeningHours.jsonParse ( + match node.["gymOpeningHours"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("gymOpeningHours") + ) + ) + | v -> v + ) let EmailAddress = (match node.["emailAddress"] with @@ -282,7 +314,17 @@ module Gym = .AsValue() .GetValue () - let Address = GymAddress.jsonParse node.["address"] + let Address = + GymAddress.jsonParse ( + match node.["address"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("address") + ) + ) + | v -> v + ) let Status = (match node.["status"] with @@ -857,7 +899,17 @@ namespace PureGym module Visit = /// Parse from a JSON node. let jsonParse (node : System.Text.Json.Nodes.JsonNode) : Visit = - let Gym = VisitGym.jsonParse node.["Gym"] + let Gym = + VisitGym.jsonParse ( + match node.["Gym"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("Gym") + ) + ) + | v -> v + ) let Duration = (match node.["Duration"] with @@ -910,8 +962,29 @@ namespace PureGym module SessionsSummary = /// Parse from a JSON node. let jsonParse (node : System.Text.Json.Nodes.JsonNode) : SessionsSummary = - let ThisWeek = SessionsAggregate.jsonParse node.["ThisWeek"] - let Total = SessionsAggregate.jsonParse node.["Total"] + let ThisWeek = + SessionsAggregate.jsonParse ( + match node.["ThisWeek"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("ThisWeek") + ) + ) + | v -> v + ) + + let Total = + SessionsAggregate.jsonParse ( + match node.["Total"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("Total") + ) + ) + | v -> v + ) { Total = Total @@ -938,7 +1011,17 @@ module Sessions = |> Seq.map (fun elt -> Visit.jsonParse elt) |> List.ofSeq - let Summary = SessionsSummary.jsonParse node.["Summary"] + let Summary = + SessionsSummary.jsonParse ( + match node.["Summary"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("Summary") + ) + ) + | v -> v + ) { Summary = Summary diff --git a/ConsumePlugin/GeneratedVault.fs b/ConsumePlugin/GeneratedVault.fs new file mode 100644 index 0000000..cace90a --- /dev/null +++ b/ConsumePlugin/GeneratedVault.fs @@ -0,0 +1,549 @@ +//------------------------------------------------------------------------------ +// This code was generated by myriad. +// Changes to this file will be lost when the code is regenerated. +//------------------------------------------------------------------------------ + + +namespace ConsumePlugin + +/// Module containing JSON parsing methods for the JwtVaultAuthResponse type +[] +[] +module JwtVaultAuthResponse = + /// Parse from a JSON node. + let jsonParse (node : System.Text.Json.Nodes.JsonNode) : JwtVaultAuthResponse = + let NumUses = + (match node.["num_uses"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("num_uses") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let Orphan = + (match node.["orphan"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("orphan") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let EntityId = + (match node.["entity_id"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("entity_id") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let TokenType = + (match node.["token_type"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("token_type") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let Renewable = + (match node.["renewable"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("renewable") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let LeaseDuration = + (match node.["lease_duration"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("lease_duration") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let IdentityPolicies = + (match node.["identity_policies"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("identity_policies") + ) + ) + | v -> v) + .AsArray () + |> Seq.map (fun elt -> elt.AsValue().GetValue ()) + |> List.ofSeq + + let TokenPolicies = + (match node.["token_policies"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("token_policies") + ) + ) + | v -> v) + .AsArray () + |> Seq.map (fun elt -> elt.AsValue().GetValue ()) + |> List.ofSeq + + let Policies = + (match node.["policies"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("policies") + ) + ) + | v -> v) + .AsArray () + |> Seq.map (fun elt -> elt.AsValue().GetValue ()) + |> List.ofSeq + + let Accessor = + (match node.["accessor"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("accessor") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let ClientToken = + (match node.["client_token"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("client_token") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + { + ClientToken = ClientToken + Accessor = Accessor + Policies = Policies + TokenPolicies = TokenPolicies + IdentityPolicies = IdentityPolicies + LeaseDuration = LeaseDuration + Renewable = Renewable + TokenType = TokenType + EntityId = EntityId + Orphan = Orphan + NumUses = NumUses + } +namespace ConsumePlugin + +/// Module containing JSON parsing methods for the JwtVaultResponse type +[] +[] +module JwtVaultResponse = + /// Parse from a JSON node. + let jsonParse (node : System.Text.Json.Nodes.JsonNode) : JwtVaultResponse = + let Auth = + JwtVaultAuthResponse.jsonParse ( + match node.["auth"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("auth") + ) + ) + | v -> v + ) + + let LeaseDuration = + (match node.["lease_duration"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("lease_duration") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let Renewable = + (match node.["renewable"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("renewable") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let LeaseId = + (match node.["lease_id"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("lease_id") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let RequestId = + (match node.["request_id"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("request_id") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + { + RequestId = RequestId + LeaseId = LeaseId + Renewable = Renewable + LeaseDuration = LeaseDuration + Auth = Auth + } +namespace ConsumePlugin + +/// Module containing JSON parsing methods for the JwtSecretResponse type +[] +[] +module JwtSecretResponse = + /// Parse from a JSON node. + let jsonParse (node : System.Text.Json.Nodes.JsonNode) : JwtSecretResponse = + let Data8 = + (match node.["data8"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("data8") + ) + ) + | v -> v) + .AsObject () + |> Seq.map (fun kvp -> + let key = (kvp.Key) + let value = (kvp.Value).AsValue().GetValue () |> System.Uri + key, value + ) + |> Seq.map System.Collections.Generic.KeyValuePair + |> System.Collections.Generic.Dictionary + + let Data7 = + (match node.["data7"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("data7") + ) + ) + | v -> v) + .AsObject () + |> Seq.map (fun kvp -> + let key = (kvp.Key) + let value = (kvp.Value).AsValue().GetValue () + key, value + ) + |> Map.ofSeq + + let Data6 = + (match node.["data6"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("data6") + ) + ) + | v -> v) + .AsObject () + |> Seq.map (fun kvp -> + let key = (kvp.Key) |> System.Uri + let value = (kvp.Value).AsValue().GetValue () + key, value + ) + |> dict + + let Data5 = + (match node.["data5"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("data5") + ) + ) + | v -> v) + .AsObject () + |> Seq.map (fun kvp -> + let key = (kvp.Key) |> System.Uri + let value = (kvp.Value).AsValue().GetValue () + key, value + ) + |> readOnlyDict + + let Data4 = + (match node.["data4"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("data4") + ) + ) + | v -> v) + .AsObject () + |> Seq.map (fun kvp -> + let key = (kvp.Key) + let value = (kvp.Value).AsValue().GetValue () + key, value + ) + |> Map.ofSeq + + let Data3 = + (match node.["data3"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("data3") + ) + ) + | v -> v) + .AsObject () + |> Seq.map (fun kvp -> + let key = (kvp.Key) + let value = (kvp.Value).AsValue().GetValue () + key, value + ) + |> Seq.map System.Collections.Generic.KeyValuePair + |> System.Collections.Generic.Dictionary + + let Data2 = + (match node.["data2"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("data2") + ) + ) + | v -> v) + .AsObject () + |> Seq.map (fun kvp -> + let key = (kvp.Key) + let value = (kvp.Value).AsValue().GetValue () + key, value + ) + |> dict + + let Data = + (match node.["data"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("data") + ) + ) + | v -> v) + .AsObject () + |> Seq.map (fun kvp -> + let key = (kvp.Key) + let value = (kvp.Value).AsValue().GetValue () + key, value + ) + |> readOnlyDict + + let LeaseDuration = + (match node.["lease_duration"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("lease_duration") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let Renewable = + (match node.["renewable"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("renewable") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let LeaseId = + (match node.["lease_id"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("lease_id") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let RequestId = + (match node.["request_id"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("request_id") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + { + RequestId = RequestId + LeaseId = LeaseId + Renewable = Renewable + LeaseDuration = LeaseDuration + Data = Data + Data2 = Data2 + Data3 = Data3 + Data4 = Data4 + Data5 = Data5 + Data6 = Data6 + Data7 = Data7 + Data8 = Data8 + } + +namespace ConsumePlugin + +open System +open System.Collections.Generic +open System.Text.Json.Serialization +open System.Threading +open System.Threading.Tasks +open RestEase + +/// Module for constructing a REST client. +[] +[] +module VaultClient = + /// Create a REST client. + let make (client : System.Net.Http.HttpClient) : IVaultClient = + { new IVaultClient with + member _.GetSecret + ( + jwt : JwtVaultResponse, + path : string, + mountPoint : string, + ct : CancellationToken option + ) + = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> + raise ( + System.ArgumentNullException ( + nameof (client.BaseAddress), + "No base address was supplied on the type, and no BaseAddress was on the HttpClient." + ) + ) + | v -> v), + System.Uri ( + "v1/{mountPoint}/{path}" + .Replace("{path}", path.ToString () |> System.Web.HttpUtility.UrlEncode) + .Replace ( + "{mountPoint}", + mountPoint.ToString () |> System.Web.HttpUtility.UrlEncode + ), + System.UriKind.Relative + ) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! stream = response.Content.ReadAsStreamAsync ct |> Async.AwaitTask + + let! node = + System.Text.Json.Nodes.JsonNode.ParseAsync (stream, cancellationToken = ct) + |> Async.AwaitTask + + return JwtSecretResponse.jsonParse node + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.GetJwt (role : string, jwt : string, ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> + raise ( + System.ArgumentNullException ( + nameof (client.BaseAddress), + "No base address was supplied on the type, and no BaseAddress was on the HttpClient." + ) + ) + | v -> v), + System.Uri ("v1/auth/jwt/login", System.UriKind.Relative) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! stream = response.Content.ReadAsStreamAsync ct |> Async.AwaitTask + + let! node = + System.Text.Json.Nodes.JsonNode.ParseAsync (stream, cancellationToken = ct) + |> Async.AwaitTask + + return JwtVaultResponse.jsonParse node + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + } diff --git a/ConsumePlugin/Vault.fs b/ConsumePlugin/Vault.fs new file mode 100644 index 0000000..73dcaf9 --- /dev/null +++ b/ConsumePlugin/Vault.fs @@ -0,0 +1,78 @@ +namespace ConsumePlugin + +open System +open System.Collections.Generic +open System.Text.Json.Serialization +open System.Threading +open System.Threading.Tasks +open RestEase + +[] +type JwtVaultAuthResponse = + { + [] + ClientToken : string + Accessor : string + Policies : string list + [] + TokenPolicies : string list + [] + IdentityPolicies : string list + [] + LeaseDuration : int + Renewable : bool + [] + TokenType : string + [] + EntityId : string + Orphan : bool + [] + NumUses : int + } + +[] +type JwtVaultResponse = + { + [] + RequestId : string + [] + LeaseId : string + Renewable : bool + [] + LeaseDuration : int + Auth : JwtVaultAuthResponse + } + +[] +type JwtSecretResponse = + { + [] + RequestId : string + [] + LeaseId : string + Renewable : bool + [] + LeaseDuration : int + Data : IReadOnlyDictionary + // These ones aren't actually part of the Vault response, but are here for tests + Data2 : IDictionary + Data3 : Dictionary + Data4 : Map + Data5 : IReadOnlyDictionary + Data6 : IDictionary + Data7 : Map + Data8 : Dictionary + } + +[] +type IVaultClient = + [] + abstract GetSecret : + jwt : JwtVaultResponse * + [] path : string * + [] mountPoint : string * + ?ct : CancellationToken -> + Task + + [] + abstract GetJwt : role : string * jwt : string * ?ct : CancellationToken -> Task diff --git a/WoofWare.Myriad.Plugins.Test/TestHttpClient/TestVaultClient.fs b/WoofWare.Myriad.Plugins.Test/TestHttpClient/TestVaultClient.fs new file mode 100644 index 0000000..bfacfab --- /dev/null +++ b/WoofWare.Myriad.Plugins.Test/TestHttpClient/TestVaultClient.fs @@ -0,0 +1,170 @@ +namespace WoofWare.Myriad.Plugins.Test + +open System +open System.Net +open System.Net.Http +open NUnit.Framework +open FsUnitTyped +open ConsumePlugin + +[] +module TestVaultClient = + + let exampleVaultKeyResponseString = + """{ + "request_id": "e2470000-0000-0000-0000-000000001f47", + "lease_id": "", + "renewable": false, + "lease_duration": 0, + "data": { + "key1_1": "value1_1", + "key1_2": "value1_2" + }, + "data2": { + "key2_1": "value2_1", + "key2_2": "value2_2" + }, + "data3": { + "key3_1": "value3_1", + "key3_2": "value3_2" + }, + "data4": { + "key4_1": "value4_1", + "key4_2": "value4_2" + }, + "data5": { + "https://example.com/data5/1": "value5_1", + "https://example.com/data5/2": "value5_2" + }, + "data6": { + "https://example.com/data6/1": "value6_1", + "https://example.com/data6/2": "value6_2" + }, + "data7": { + "key7_1": 71, + "key7_2": 72 + }, + "data8": { + "key8_1": "https://example.com/data8/1", + "key8_2": "https://example.com/data8/2" + } +}""" + + let exampleVaultJwtResponseString = + """{ + "request_id": "80000000-0000-0000-0000-00000000000d", + "lease_id": "", + "renewable": false, + "lease_duration": 0, + "data": null, + "wrap_info": null, + "warnings": null, + "auth": { + "client_token": "redacted_client_token", + "accessor": "redacted_accessor", + "policies": [ + "policy1", + "default" + ], + "identity_policies": [ + "identity-policy", + "default-2" + ], + "token_policies": [ + "token-policy", + "default-3" + ], + "metadata": { + "role": "some-role" + }, + "lease_duration": 43200, + "renewable": true, + "entity_id": "20000000-0000-0000-0000-000000000007", + "token_type": "service", + "orphan": true, + "mfa_requirement": null, + "num_uses": 0 + } +}""" + + [] + let ``URI example`` () = + let proc (message : HttpRequestMessage) : HttpResponseMessage Async = + async { + message.Method |> shouldEqual HttpMethod.Get + + let requestUri = message.RequestUri.ToString () + + match requestUri with + | "https://my-vault.com/v1/auth/jwt/login" -> + let content = new StringContent (exampleVaultJwtResponseString) + + let resp = new HttpResponseMessage (HttpStatusCode.OK) + resp.Content <- content + return resp + | "https://my-vault.com/v1/mount/path" -> + let content = new StringContent (exampleVaultKeyResponseString) + + let resp = new HttpResponseMessage (HttpStatusCode.OK) + resp.Content <- content + return resp + | _ -> return failwith $"bad URI: %s{requestUri}" + } + + use client = HttpClientMock.make (Uri "https://my-vault.com") proc + let api = VaultClient.make client + + let vaultResponse = api.GetJwt("role", "jwt").Result + let value = api.GetSecret(vaultResponse, "path", "mount").Result + + value.Data + |> Seq.toList + |> List.map (fun (KeyValue (k, v)) -> k, v) + |> shouldEqual [ "key1_1", "value1_1" ; "key1_2", "value1_2" ] + + value.Data2 + |> Seq.toList + |> List.map (fun (KeyValue (k, v)) -> k, v) + |> shouldEqual [ "key2_1", "value2_1" ; "key2_2", "value2_2" ] + + value.Data3 + |> Seq.toList + |> List.map (fun (KeyValue (k, v)) -> k, v) + |> shouldEqual [ "key3_1", "value3_1" ; "key3_2", "value3_2" ] + + value.Data4 + |> Seq.toList + |> List.map (fun (KeyValue (k, v)) -> k, v) + |> shouldEqual [ "key4_1", "value4_1" ; "key4_2", "value4_2" ] + + value.Data5 + |> Seq.toList + |> List.map (fun (KeyValue (k, v)) -> (k : Uri).ToString (), v) + |> shouldEqual + [ + "https://example.com/data5/1", "value5_1" + "https://example.com/data5/2", "value5_2" + ] + + value.Data6 + |> Seq.toList + |> List.map (fun (KeyValue (k, v)) -> (k : Uri).ToString (), v) + |> shouldEqual + [ + "https://example.com/data6/1", "value6_1" + "https://example.com/data6/2", "value6_2" + ] + + value.Data7 + |> Seq.toList + |> List.map (fun (KeyValue (k, v)) -> k, v) + |> shouldEqual [ "key7_1", 71 ; "key7_2", 72 ] + + value.Data8 + |> Seq.toList + |> List.map (fun (KeyValue (k, v)) -> k, (v : Uri).ToString ()) + |> shouldEqual + [ + "key8_1", "https://example.com/data8/1" + "key8_2", "https://example.com/data8/2" + ] diff --git a/WoofWare.Myriad.Plugins.Test/WoofWare.Myriad.Plugins.Test.fsproj b/WoofWare.Myriad.Plugins.Test/WoofWare.Myriad.Plugins.Test.fsproj index 5500c78..0990990 100644 --- a/WoofWare.Myriad.Plugins.Test/WoofWare.Myriad.Plugins.Test.fsproj +++ b/WoofWare.Myriad.Plugins.Test/WoofWare.Myriad.Plugins.Test.fsproj @@ -18,6 +18,7 @@ + diff --git a/WoofWare.Myriad.Plugins/AstHelper.fs b/WoofWare.Myriad.Plugins/AstHelper.fs index c005985..dedeb4b 100644 --- a/WoofWare.Myriad.Plugins/AstHelper.fs +++ b/WoofWare.Myriad.Plugins/AstHelper.fs @@ -102,10 +102,35 @@ module internal AstHelper = || System.String.Equals (i.idText, "[]", System.StringComparison.Ordinal) -> true - // TODO: consider FSharpList or whatever it is - | [ i ] -> - printfn $"Not array: %s{i.idText}" - false + | _ -> false + + let isMapIdent (ident : SynLongIdent) : bool = + match ident.LongIdent |> List.map _.idText with + | [ "Map" ] -> true + | _ -> false + + let isReadOnlyDictionaryIdent (ident : SynLongIdent) : bool = + match ident.LongIdent |> List.map _.idText with + | [ "IReadOnlyDictionary" ] + | [ "Generic" ; "IReadOnlyDictionary" ] + | [ "Collections" ; "Generic" ; "IReadOnlyDictionary" ] + | [ "System" ; "Collections" ; "Generic" ; "IReadOnlyDictionary" ] -> true + | _ -> false + + let isDictionaryIdent (ident : SynLongIdent) : bool = + match ident.LongIdent |> List.map _.idText with + | [ "Dictionary" ] + | [ "Generic" ; "Dictionary" ] + | [ "Collections" ; "Generic" ; "Dictionary" ] + | [ "System" ; "Collections" ; "Generic" ; "Dictionary" ] -> true + | _ -> false + + let isIDictionaryIdent (ident : SynLongIdent) : bool = + match ident.LongIdent |> List.map _.idText with + | [ "IDictionary" ] + | [ "Generic" ; "IDictionary" ] + | [ "Collections" ; "Generic" ; "IDictionary" ] + | [ "System" ; "Collections" ; "Generic" ; "IDictionary" ] -> true | _ -> false let rec private extractOpensFromDecl (moduleDecls : SynModuleDecl list) : SynOpenDeclTarget list = @@ -337,6 +362,32 @@ module internal SynTypePatterns = | SynType.Array (1, innerType, _) -> Some innerType | _ -> None + let (|DictionaryType|_|) (fieldType : SynType) = + match fieldType with + | SynType.App (SynType.LongIdent ident, _, [ key ; value ], _, _, _, _) when AstHelper.isDictionaryIdent ident -> + Some (key, value) + | _ -> None + + let (|IDictionaryType|_|) (fieldType : SynType) = + match fieldType with + | SynType.App (SynType.LongIdent ident, _, [ key ; value ], _, _, _, _) when AstHelper.isIDictionaryIdent ident -> + Some (key, value) + | _ -> None + + let (|IReadOnlyDictionaryType|_|) (fieldType : SynType) = + match fieldType with + | SynType.App (SynType.LongIdent ident, _, [ key ; value ], _, _, _, _) when + AstHelper.isReadOnlyDictionaryIdent ident + -> + Some (key, value) + | _ -> None + + let (|MapType|_|) (fieldType : SynType) = + match fieldType with + | SynType.App (SynType.LongIdent ident, _, [ key ; value ], _, _, _, _) when AstHelper.isMapIdent ident -> + Some (key, value) + | _ -> None + /// Returns the string name of the type. let (|PrimitiveType|_|) (fieldType : SynType) = match fieldType with diff --git a/WoofWare.Myriad.Plugins/JsonParseGenerator.fs b/WoofWare.Myriad.Plugins/JsonParseGenerator.fs index 8756c0a..186cbe5 100644 --- a/WoofWare.Myriad.Plugins/JsonParseGenerator.fs +++ b/WoofWare.Myriad.Plugins/JsonParseGenerator.fs @@ -85,6 +85,14 @@ module internal JsonParseGenerator = |> SynExpr.callMethod "AsValue" |> SynExpr.callGenericMethod "GetValue" typeName + /// {node}.AsObject() + /// If `propertyName` is Some, uses `assertNotNull {node}` instead of `{node}`. + let asObject (propertyName : SynExpr option) (node : SynExpr) : SynExpr = + match propertyName with + | None -> node + | Some propertyName -> assertNotNull propertyName node + |> SynExpr.callMethod "AsObject" + /// {type}.jsonParse {node} let typeJsonParse (typeName : LongIdent) (node : SynExpr) : SynExpr = SynExpr.CreateApp ( @@ -133,6 +141,54 @@ module internal JsonParseGenerator = let parseFunction (typeName : string) : LongIdent = List.append (SynExpr.qualifyPrimitiveType typeName) [ Ident.Create "Parse" ] + /// fun kvp -> let key = {key(kvp)} in let value = {value(kvp)} in (key, value)) + /// The inputs will be fed with appropriate SynExprs to apply them to the `kvp.Key` and `kvp.Value` args. + let dictionaryMapper (key : SynExpr -> SynExpr) (value : SynExpr -> SynExpr) : SynExpr = + let keyArg = + SynExpr.CreateLongIdent (SynLongIdent.Create [ "kvp" ; "Key" ]) + |> SynExpr.CreateParen + + let valueArg = + SynExpr.CreateLongIdent (SynLongIdent.Create [ "kvp" ; "Value" ]) + |> SynExpr.CreateParen + + SynExpr.LetOrUse ( + false, + false, + [ + SynBinding.Let (pattern = SynPat.CreateNamed (Ident.Create "key"), expr = key keyArg) + ], + SynExpr.LetOrUse ( + false, + false, + [ + SynBinding.Let (pattern = SynPat.CreateNamed (Ident.Create "value"), expr = value valueArg) + ], + SynExpr.CreateTuple [ SynExpr.CreateIdentString "key" ; SynExpr.CreateIdentString "value" ], + range0, + { + InKeyword = None + } + ), + range0, + { + InKeyword = None + } + ) + |> SynExpr.createLambda "kvp" + + /// A conforming JSON object has only strings as keys. But it would be reasonable to allow the user + /// to parse these as URIs, for example. + let parseKeyString (desiredType : SynType) (key : SynExpr) : SynExpr = + match desiredType with + | String -> key + | Uri -> + key + |> SynExpr.pipeThroughFunction (SynExpr.CreateLongIdent (SynLongIdent.Create [ "System" ; "Uri" ])) + | _ -> + failwithf + $"Unable to parse the key type %+A{desiredType} of a JSON object. Keys are strings, and this plugin does not know how to convert to that from a string." + /// Given `node.["town"]`, for example, choose how to obtain a JSON value from it. /// The property name is used in error messages at runtime to show where a JSON /// parse error occurred; supply `None` to indicate "don't validate". @@ -217,6 +273,56 @@ module internal JsonParseGenerator = | ArrayType ty -> parseNode None options ty (SynExpr.CreateLongIdent (SynLongIdent.CreateString "elt")) |> asArrayMapped propertyName "Array" node + | IDictionaryType (keyType, valueType) -> + node + |> asObject propertyName + |> SynExpr.pipeThroughFunction ( + SynExpr.CreateApp ( + SynExpr.CreateLongIdent (SynLongIdent.Create [ "Seq" ; "map" ]), + dictionaryMapper (parseKeyString keyType) (parseNode None options valueType) + ) + ) + |> SynExpr.pipeThroughFunction (SynExpr.CreateLongIdent (SynLongIdent.Create [ "dict" ])) + | DictionaryType (keyType, valueType) -> + node + |> asObject propertyName + |> SynExpr.pipeThroughFunction ( + SynExpr.CreateApp ( + SynExpr.CreateLongIdent (SynLongIdent.Create [ "Seq" ; "map" ]), + dictionaryMapper (parseKeyString keyType) (parseNode None options valueType) + ) + ) + |> SynExpr.pipeThroughFunction ( + SynExpr.CreateApp ( + SynExpr.CreateLongIdent (SynLongIdent.Create [ "Seq" ; "map" ]), + SynExpr.CreateLongIdent ( + SynLongIdent.Create [ "System" ; "Collections" ; "Generic" ; "KeyValuePair" ] + ) + ) + ) + |> SynExpr.pipeThroughFunction ( + SynExpr.CreateLongIdent (SynLongIdent.Create [ "System" ; "Collections" ; "Generic" ; "Dictionary" ]) + ) + | IReadOnlyDictionaryType (keyType, valueType) -> + node + |> asObject propertyName + |> SynExpr.pipeThroughFunction ( + SynExpr.CreateApp ( + SynExpr.CreateLongIdent (SynLongIdent.Create [ "Seq" ; "map" ]), + dictionaryMapper (parseKeyString keyType) (parseNode None options valueType) + ) + ) + |> SynExpr.pipeThroughFunction (SynExpr.CreateLongIdent (SynLongIdent.Create [ "readOnlyDict" ])) + | MapType (keyType, valueType) -> + node + |> asObject propertyName + |> SynExpr.pipeThroughFunction ( + SynExpr.CreateApp ( + SynExpr.CreateLongIdent (SynLongIdent.Create [ "Seq" ; "map" ]), + dictionaryMapper (parseKeyString keyType) (parseNode None options valueType) + ) + ) + |> SynExpr.pipeThroughFunction (SynExpr.CreateLongIdent (SynLongIdent.Create [ "Map" ; "ofSeq" ])) | _ -> // Let's just hope that we've also got our own type annotation! let typeName = @@ -224,7 +330,10 @@ module internal JsonParseGenerator = | SynType.LongIdent ident -> ident.LongIdent | _ -> failwith $"Unrecognised type: %+A{fieldType}" - typeJsonParse typeName node + match propertyName with + | None -> node + | Some propertyName -> assertNotNull propertyName node + |> typeJsonParse typeName /// propertyName is probably a string literal, but it could be a [] variable /// The result of this function is the body of a let-binding (not including the LHS of that let-binding).