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).