Compare commits

...

15 Commits

Author SHA1 Message Date
Patrick Stevens
0652744c57 Allow using fsproj annotations instead of attributes (#275) 2024-10-02 19:30:21 +00:00
dependabot[bot]
9252979673 Bump cachix/install-nix-action from V28 to 29 (#273) 2024-09-30 13:53:25 +01:00
patrick-conscriptus[bot]
1120a3752d Automated commit (#272)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2024-09-29 01:26:14 +00:00
patrick-conscriptus[bot]
7ca6b0c0eb Automated commit (#271)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2024-09-22 01:25:31 +00:00
Patrick Stevens
50efb8d9bc Bump flake (#270) 2024-09-20 19:44:24 +01:00
Patrick Stevens
93a1b630c8 Add HTTP Swagger client generator (#250) 2024-09-19 17:21:09 +01:00
Patrick Stevens
4482038e8a Support individual per-method headers (#268) 2024-09-19 17:08:38 +01:00
dependabot[bot]
a41fa9dd37 Bump FsUnit from 6.0.0 to 6.0.1 (#265)
* Bump fantomas from 6.3.12 to 6.3.15

Bumps [fantomas](https://github.com/fsprojects/fantomas) from 6.3.12 to 6.3.15.
- [Release notes](https://github.com/fsprojects/fantomas/releases)
- [Changelog](https://github.com/fsprojects/fantomas/blob/main/CHANGELOG.md)
- [Commits](https://github.com/fsprojects/fantomas/compare/v6.3.12...v6.3.15)

---
updated-dependencies:
- dependency-name: fantomas
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump FsUnit from 6.0.0 to 6.0.1

Bumps [FsUnit](https://github.com/fsprojects/FsUnit) from 6.0.0 to 6.0.1.
- [Release notes](https://github.com/fsprojects/FsUnit/releases)
- [Changelog](https://github.com/fsprojects/FsUnit/blob/master/RELEASE_NOTES.md)
- [Commits](https://github.com/fsprojects/FsUnit/compare/6.0.0...6.0.1)

---
updated-dependencies:
- dependency-name: FsUnit
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Deps

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Smaug123 <3138005+Smaug123@users.noreply.github.com>
2024-09-16 13:56:13 +00:00
dependabot[bot]
fc5acc2f58 Bump cachix/install-nix-action from V27 to 28 (#266) 2024-09-16 12:12:30 +01:00
Patrick Stevens
0a1783d6ed Support [<BasePath>] (#263) 2024-09-15 17:38:03 +00:00
Patrick Stevens
9a3ebbf28f Cope with unit type in JSON (#262) 2024-09-15 14:37:50 +00:00
Patrick Stevens
e22525c200 Interpret JsonExtensionData (#261) 2024-09-15 11:13:22 +01:00
Patrick Stevens
09b7109c84 Extract some utilities from http-client branch (#260) 2024-09-14 22:02:32 +00:00
Patrick Stevens
693b95106a Also pipe through parser in PositionalArgs true (#259) 2024-09-13 16:11:53 +00:00
Patrick Stevens
49ecfbf5e5 Fix includeFlagLike when arg doesn't have an equals (#257) 2024-09-12 22:10:08 +00:00
55 changed files with 74216 additions and 209 deletions

View File

@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"fantomas": {
"version": "6.3.12",
"version": "6.3.15",
"commands": [
"fantomas"
]

View File

@@ -29,7 +29,7 @@ jobs:
with:
fetch-depth: 0 # so that NerdBank.GitVersioning has access to history
- name: Install Nix
uses: cachix/install-nix-action@V27
uses: cachix/install-nix-action@v29
with:
extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
@@ -50,7 +50,7 @@ jobs:
with:
fetch-depth: 0 # so that NerdBank.GitVersioning has access to history
- name: Install Nix
uses: cachix/install-nix-action@V27
uses: cachix/install-nix-action@v29
with:
extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
@@ -67,7 +67,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Install Nix
uses: cachix/install-nix-action@V27
uses: cachix/install-nix-action@v29
with:
extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
@@ -82,7 +82,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Install Nix
uses: cachix/install-nix-action@V27
uses: cachix/install-nix-action@v29
with:
extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
@@ -97,7 +97,7 @@ jobs:
with:
fetch-depth: 0 # so that NerdBank.GitVersioning has access to history
- name: Install Nix
uses: cachix/install-nix-action@V27
uses: cachix/install-nix-action@v29
with:
extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
@@ -116,7 +116,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Install Nix
uses: cachix/install-nix-action@V27
uses: cachix/install-nix-action@v29
with:
extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
@@ -129,7 +129,7 @@ jobs:
steps:
- uses: actions/checkout@master
- name: Install Nix
uses: cachix/install-nix-action@V27
uses: cachix/install-nix-action@v29
with:
extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
@@ -142,7 +142,7 @@ jobs:
steps:
- uses: actions/checkout@master
- name: Install Nix
uses: cachix/install-nix-action@V27
uses: cachix/install-nix-action@v29
with:
extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
@@ -156,7 +156,7 @@ jobs:
with:
fetch-depth: 0 # so that NerdBank.GitVersioning has access to history
- name: Install Nix
uses: cachix/install-nix-action@V27
uses: cachix/install-nix-action@v29
with:
extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
@@ -276,7 +276,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Install Nix
uses: cachix/install-nix-action@V27
uses: cachix/install-nix-action@v29
with:
extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
@@ -309,7 +309,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Install Nix
uses: cachix/install-nix-action@V27
uses: cachix/install-nix-action@v29
with:
extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}

View File

@@ -27,8 +27,7 @@ jobs:
- name: Run passthru
run: |
set -o pipefail
./result | tee /tmp/passthru.txt
cp /"$(cat /tmp/passthru.txt | grep " wrote lockfile to " | cut -d / -f 2-)" nix/deps.nix
./result nix/deps.nix
- name: Format
run: 'nix develop --command alejandra .'

View File

@@ -1,5 +1,14 @@
Notable changes are recorded here.
# WoofWare.Myriad.Plugins 3.0.1
Semantics of `HttpClient`'s URI component composition changed:
we now implicitly insert `/` characters after `[<BaseAddress>]` and `[<BasePath>]`, so that URI composition doesn't silently drop the last component if you didn't put a slash there.
# WoofWare.Myriad.Plugins 2.3.9
`JsonParse` and `JsonSerialize` now interpret `[<JsonExtensionData>]`, which must be on a `Dictionary<string, _>`; this collects any extra components that were present on the JSON object.
# WoofWare.Myriad.Plugins 2.2.1, WoofWare.Myriad.Plugins.Attributes 3.2.1
New generator: `ArgParser`, a basic reflection-free argument parser.

View File

@@ -204,6 +204,30 @@ type FlagsIntoPositionalArgs =
GrabEverything : string list
}
[<ArgParser true>]
type FlagsIntoPositionalArgsChoice =
{
A : string
[<PositionalArgs true>]
GrabEverything : Choice<string, string> list
}
[<ArgParser true>]
type FlagsIntoPositionalArgsInt =
{
A : string
[<PositionalArgs true>]
GrabEverything : int list
}
[<ArgParser true>]
type FlagsIntoPositionalArgsIntChoice =
{
A : string
[<PositionalArgs true>]
GrabEverything : Choice<int, int> list
}
[<ArgParser true>]
type FlagsIntoPositionalArgs' =
{

View File

@@ -32,6 +32,20 @@
<Compile Include="GeneratedMock.fs">
<MyriadFile>MockExample.fs</MyriadFile>
</Compile>
<Compile Include="MockExampleNoAttributes.fs" />
<Compile Include="GeneratedMockNoAttributes.fs">
<MyriadFile>MockExampleNoAttributes.fs</MyriadFile>
<MyriadParams>
<IPublicTypeNoAttr>GenerateMock</IPublicTypeNoAttr>
<IPublicTypeInternalFalseNoAttr>GenerateMock(false)</IPublicTypeInternalFalseNoAttr>
<InternalTypeNoAttr>GenerateMock</InternalTypeNoAttr>
<PrivateTypeNoAttr>GenerateMock</PrivateTypeNoAttr>
<PrivateTypeInternalFalseNoAttr>GenerateMock(false)</PrivateTypeInternalFalseNoAttr>
<VeryPublicTypeNoAttr>GenerateMock</VeryPublicTypeNoAttr>
<CurriedNoAttr>GenerateMock</CurriedNoAttr>
<TypeWithInterfaceNoAttr>GenerateMock</TypeWithInterfaceNoAttr>
</MyriadParams>
</Compile>
<Compile Include="Vault.fs" />
<Compile Include="GeneratedVault.fs">
<MyriadFile>Vault.fs</MyriadFile>
@@ -56,6 +70,17 @@
<Compile Include="GeneratedArgs.fs">
<MyriadFile>Args.fs</MyriadFile>
</Compile>
<None Include="swagger-gitea.json" />
<Compile Include="GeneratedSwaggerGitea.fs">
<MyriadFile>swagger-gitea.json</MyriadFile>
<MyriadParams>
<GenerateMockInternal>true</GenerateMockInternal>
<ClassName>Gitea</ClassName>
</MyriadParams>
</Compile>
<Compile Include="Generated2SwaggerGitea.fs">
<MyriadFile>GeneratedSwaggerGitea.fs</MyriadFile>
</Compile>
</ItemGroup>
<ItemGroup>

File diff suppressed because it is too large Load Diff

View File

@@ -3635,6 +3635,9 @@ module FlagsIntoPositionalArgsArgParse =
| Error exc ->
if setFlagValue key then
go ParseState_FlagsIntoPositionalArgs.AwaitingKey (arg :: args)
else if true then
key |> (fun x -> x) |> arg_1.Add
go ParseState_FlagsIntoPositionalArgs.AwaitingKey (arg :: args)
else
match exc with
| None ->
@@ -3672,6 +3675,498 @@ open System
open System.IO
open WoofWare.Myriad.Plugins
/// Methods to parse arguments for the type FlagsIntoPositionalArgsChoice
[<AutoOpen>]
module FlagsIntoPositionalArgsChoiceArgParse =
type private ParseState_FlagsIntoPositionalArgsChoice =
/// Ready to consume a key or positional arg
| AwaitingKey
/// Waiting to receive a value for the key we've already consumed
| AwaitingValue of key : string
/// Extension methods for argument parsing
type FlagsIntoPositionalArgsChoice with
static member parse'
(getEnvironmentVariable : string -> string)
(args : string list)
: FlagsIntoPositionalArgsChoice
=
let ArgParser_errors = ResizeArray ()
let helpText () =
[
(sprintf "%s string%s%s" (sprintf "--%s" "a") "" "")
(sprintf
"%s string%s%s"
(sprintf "--%s" "grab-everything")
" (positional args) (can be repeated)"
"")
]
|> String.concat "\n"
let arg_1 : Choice<string, string> ResizeArray = ResizeArray ()
let mutable arg_0 : string option = None
/// Processes the key-value pair, returning Error if no key was matched.
/// If the key is an arg which can have arity 1, but throws when consuming that arg, we return Error(<the message>).
/// This can nevertheless be a successful parse, e.g. when the key may have arity 0.
let processKeyValue (key : string) (value : string) : Result<unit, string option> =
if System.String.Equals (key, sprintf "--%s" "a", System.StringComparison.OrdinalIgnoreCase) then
match arg_0 with
| Some x ->
sprintf
"Argument '%s' was supplied multiple times: %s and %s"
(sprintf "--%s" "a")
(x.ToString ())
(value.ToString ())
|> ArgParser_errors.Add
Ok ()
| None ->
try
arg_0 <- value |> (fun x -> x) |> Some
Ok ()
with _ as exc ->
exc.Message |> Some |> Error
else if
System.String.Equals (
key,
sprintf "--%s" "grab-everything",
System.StringComparison.OrdinalIgnoreCase
)
then
value |> (fun x -> x) |> Choice1Of2 |> arg_1.Add
() |> Ok
else
Error None
/// Returns false if we didn't set a value.
let setFlagValue (key : string) : bool = false
let rec go (state : ParseState_FlagsIntoPositionalArgsChoice) (args : string list) =
match args with
| [] ->
match state with
| ParseState_FlagsIntoPositionalArgsChoice.AwaitingKey -> ()
| ParseState_FlagsIntoPositionalArgsChoice.AwaitingValue key ->
if setFlagValue key then
()
else
sprintf
"Trailing argument %s had no value. Use a double-dash to separate positional args from key-value args."
key
|> ArgParser_errors.Add
| "--" :: rest -> arg_1.AddRange (rest |> Seq.map (fun x -> x) |> Seq.map Choice2Of2)
| arg :: args ->
match state with
| ParseState_FlagsIntoPositionalArgsChoice.AwaitingKey ->
if arg.StartsWith ("--", System.StringComparison.Ordinal) then
if arg = "--help" then
helpText () |> failwithf "Help text requested.\n%s"
else
let equals = arg.IndexOf (char 61)
if equals < 0 then
args |> go (ParseState_FlagsIntoPositionalArgsChoice.AwaitingValue arg)
else
let key = arg.[0 .. equals - 1]
let value = arg.[equals + 1 ..]
match processKeyValue key value with
| Ok () -> go ParseState_FlagsIntoPositionalArgsChoice.AwaitingKey args
| Error x ->
if true then
arg |> (fun x -> x) |> Choice1Of2 |> arg_1.Add
go ParseState_FlagsIntoPositionalArgsChoice.AwaitingKey args
else
match x with
| None ->
failwithf
"Unable to process argument %s as key %s and value %s"
arg
key
value
| Some msg ->
sprintf "%s (at arg %s)" msg arg |> ArgParser_errors.Add
go ParseState_FlagsIntoPositionalArgsChoice.AwaitingKey args
else
arg |> (fun x -> x) |> Choice1Of2 |> arg_1.Add
go ParseState_FlagsIntoPositionalArgsChoice.AwaitingKey args
| ParseState_FlagsIntoPositionalArgsChoice.AwaitingValue key ->
match processKeyValue key arg with
| Ok () -> go ParseState_FlagsIntoPositionalArgsChoice.AwaitingKey args
| Error exc ->
if setFlagValue key then
go ParseState_FlagsIntoPositionalArgsChoice.AwaitingKey (arg :: args)
else if true then
key |> (fun x -> x) |> Choice1Of2 |> arg_1.Add
go ParseState_FlagsIntoPositionalArgsChoice.AwaitingKey (arg :: args)
else
match exc with
| None ->
failwithf
"Unable to process supplied arg %s. Help text follows.\n%s"
key
(helpText ())
| Some msg -> msg |> ArgParser_errors.Add
go ParseState_FlagsIntoPositionalArgsChoice.AwaitingKey args
let arg_1 = arg_1 |> Seq.toList
let arg_0 =
match arg_0 with
| None ->
sprintf "Required argument '%s' received no value" (sprintf "--%s" "a")
|> ArgParser_errors.Add
Unchecked.defaultof<_>
| Some x -> x
if 0 = ArgParser_errors.Count then
{
A = arg_0
GrabEverything = arg_1
}
else
ArgParser_errors |> String.concat "\n" |> failwithf "Errors during parse!\n%s"
static member parse (args : string list) : FlagsIntoPositionalArgsChoice =
FlagsIntoPositionalArgsChoice.parse' System.Environment.GetEnvironmentVariable args
namespace ConsumePlugin
open System
open System.IO
open WoofWare.Myriad.Plugins
/// Methods to parse arguments for the type FlagsIntoPositionalArgsInt
[<AutoOpen>]
module FlagsIntoPositionalArgsIntArgParse =
type private ParseState_FlagsIntoPositionalArgsInt =
/// Ready to consume a key or positional arg
| AwaitingKey
/// Waiting to receive a value for the key we've already consumed
| AwaitingValue of key : string
/// Extension methods for argument parsing
type FlagsIntoPositionalArgsInt with
static member parse'
(getEnvironmentVariable : string -> string)
(args : string list)
: FlagsIntoPositionalArgsInt
=
let ArgParser_errors = ResizeArray ()
let helpText () =
[
(sprintf "%s string%s%s" (sprintf "--%s" "a") "" "")
(sprintf
"%s int32%s%s"
(sprintf "--%s" "grab-everything")
" (positional args) (can be repeated)"
"")
]
|> String.concat "\n"
let arg_1 : int ResizeArray = ResizeArray ()
let mutable arg_0 : string option = None
/// Processes the key-value pair, returning Error if no key was matched.
/// If the key is an arg which can have arity 1, but throws when consuming that arg, we return Error(<the message>).
/// This can nevertheless be a successful parse, e.g. when the key may have arity 0.
let processKeyValue (key : string) (value : string) : Result<unit, string option> =
if System.String.Equals (key, sprintf "--%s" "a", System.StringComparison.OrdinalIgnoreCase) then
match arg_0 with
| Some x ->
sprintf
"Argument '%s' was supplied multiple times: %s and %s"
(sprintf "--%s" "a")
(x.ToString ())
(value.ToString ())
|> ArgParser_errors.Add
Ok ()
| None ->
try
arg_0 <- value |> (fun x -> x) |> Some
Ok ()
with _ as exc ->
exc.Message |> Some |> Error
else if
System.String.Equals (
key,
sprintf "--%s" "grab-everything",
System.StringComparison.OrdinalIgnoreCase
)
then
value |> (fun x -> System.Int32.Parse x) |> arg_1.Add
() |> Ok
else
Error None
/// Returns false if we didn't set a value.
let setFlagValue (key : string) : bool = false
let rec go (state : ParseState_FlagsIntoPositionalArgsInt) (args : string list) =
match args with
| [] ->
match state with
| ParseState_FlagsIntoPositionalArgsInt.AwaitingKey -> ()
| ParseState_FlagsIntoPositionalArgsInt.AwaitingValue key ->
if setFlagValue key then
()
else
sprintf
"Trailing argument %s had no value. Use a double-dash to separate positional args from key-value args."
key
|> ArgParser_errors.Add
| "--" :: rest -> arg_1.AddRange (rest |> Seq.map (fun x -> System.Int32.Parse x))
| arg :: args ->
match state with
| ParseState_FlagsIntoPositionalArgsInt.AwaitingKey ->
if arg.StartsWith ("--", System.StringComparison.Ordinal) then
if arg = "--help" then
helpText () |> failwithf "Help text requested.\n%s"
else
let equals = arg.IndexOf (char 61)
if equals < 0 then
args |> go (ParseState_FlagsIntoPositionalArgsInt.AwaitingValue arg)
else
let key = arg.[0 .. equals - 1]
let value = arg.[equals + 1 ..]
match processKeyValue key value with
| Ok () -> go ParseState_FlagsIntoPositionalArgsInt.AwaitingKey args
| Error x ->
if true then
arg |> (fun x -> System.Int32.Parse x) |> arg_1.Add
go ParseState_FlagsIntoPositionalArgsInt.AwaitingKey args
else
match x with
| None ->
failwithf
"Unable to process argument %s as key %s and value %s"
arg
key
value
| Some msg ->
sprintf "%s (at arg %s)" msg arg |> ArgParser_errors.Add
go ParseState_FlagsIntoPositionalArgsInt.AwaitingKey args
else
arg |> (fun x -> System.Int32.Parse x) |> arg_1.Add
go ParseState_FlagsIntoPositionalArgsInt.AwaitingKey args
| ParseState_FlagsIntoPositionalArgsInt.AwaitingValue key ->
match processKeyValue key arg with
| Ok () -> go ParseState_FlagsIntoPositionalArgsInt.AwaitingKey args
| Error exc ->
if setFlagValue key then
go ParseState_FlagsIntoPositionalArgsInt.AwaitingKey (arg :: args)
else if true then
key |> (fun x -> System.Int32.Parse x) |> arg_1.Add
go ParseState_FlagsIntoPositionalArgsInt.AwaitingKey (arg :: args)
else
match exc with
| None ->
failwithf
"Unable to process supplied arg %s. Help text follows.\n%s"
key
(helpText ())
| Some msg -> msg |> ArgParser_errors.Add
go ParseState_FlagsIntoPositionalArgsInt.AwaitingKey args
let arg_1 = arg_1 |> Seq.toList
let arg_0 =
match arg_0 with
| None ->
sprintf "Required argument '%s' received no value" (sprintf "--%s" "a")
|> ArgParser_errors.Add
Unchecked.defaultof<_>
| Some x -> x
if 0 = ArgParser_errors.Count then
{
A = arg_0
GrabEverything = arg_1
}
else
ArgParser_errors |> String.concat "\n" |> failwithf "Errors during parse!\n%s"
static member parse (args : string list) : FlagsIntoPositionalArgsInt =
FlagsIntoPositionalArgsInt.parse' System.Environment.GetEnvironmentVariable args
namespace ConsumePlugin
open System
open System.IO
open WoofWare.Myriad.Plugins
/// Methods to parse arguments for the type FlagsIntoPositionalArgsIntChoice
[<AutoOpen>]
module FlagsIntoPositionalArgsIntChoiceArgParse =
type private ParseState_FlagsIntoPositionalArgsIntChoice =
/// Ready to consume a key or positional arg
| AwaitingKey
/// Waiting to receive a value for the key we've already consumed
| AwaitingValue of key : string
/// Extension methods for argument parsing
type FlagsIntoPositionalArgsIntChoice with
static member parse'
(getEnvironmentVariable : string -> string)
(args : string list)
: FlagsIntoPositionalArgsIntChoice
=
let ArgParser_errors = ResizeArray ()
let helpText () =
[
(sprintf "%s string%s%s" (sprintf "--%s" "a") "" "")
(sprintf
"%s int32%s%s"
(sprintf "--%s" "grab-everything")
" (positional args) (can be repeated)"
"")
]
|> String.concat "\n"
let arg_1 : Choice<int, int> ResizeArray = ResizeArray ()
let mutable arg_0 : string option = None
/// Processes the key-value pair, returning Error if no key was matched.
/// If the key is an arg which can have arity 1, but throws when consuming that arg, we return Error(<the message>).
/// This can nevertheless be a successful parse, e.g. when the key may have arity 0.
let processKeyValue (key : string) (value : string) : Result<unit, string option> =
if System.String.Equals (key, sprintf "--%s" "a", System.StringComparison.OrdinalIgnoreCase) then
match arg_0 with
| Some x ->
sprintf
"Argument '%s' was supplied multiple times: %s and %s"
(sprintf "--%s" "a")
(x.ToString ())
(value.ToString ())
|> ArgParser_errors.Add
Ok ()
| None ->
try
arg_0 <- value |> (fun x -> x) |> Some
Ok ()
with _ as exc ->
exc.Message |> Some |> Error
else if
System.String.Equals (
key,
sprintf "--%s" "grab-everything",
System.StringComparison.OrdinalIgnoreCase
)
then
value |> (fun x -> System.Int32.Parse x) |> Choice1Of2 |> arg_1.Add
() |> Ok
else
Error None
/// Returns false if we didn't set a value.
let setFlagValue (key : string) : bool = false
let rec go (state : ParseState_FlagsIntoPositionalArgsIntChoice) (args : string list) =
match args with
| [] ->
match state with
| ParseState_FlagsIntoPositionalArgsIntChoice.AwaitingKey -> ()
| ParseState_FlagsIntoPositionalArgsIntChoice.AwaitingValue key ->
if setFlagValue key then
()
else
sprintf
"Trailing argument %s had no value. Use a double-dash to separate positional args from key-value args."
key
|> ArgParser_errors.Add
| "--" :: rest -> arg_1.AddRange (rest |> Seq.map (fun x -> System.Int32.Parse x) |> Seq.map Choice2Of2)
| arg :: args ->
match state with
| ParseState_FlagsIntoPositionalArgsIntChoice.AwaitingKey ->
if arg.StartsWith ("--", System.StringComparison.Ordinal) then
if arg = "--help" then
helpText () |> failwithf "Help text requested.\n%s"
else
let equals = arg.IndexOf (char 61)
if equals < 0 then
args |> go (ParseState_FlagsIntoPositionalArgsIntChoice.AwaitingValue arg)
else
let key = arg.[0 .. equals - 1]
let value = arg.[equals + 1 ..]
match processKeyValue key value with
| Ok () -> go ParseState_FlagsIntoPositionalArgsIntChoice.AwaitingKey args
| Error x ->
if true then
arg |> (fun x -> System.Int32.Parse x) |> Choice1Of2 |> arg_1.Add
go ParseState_FlagsIntoPositionalArgsIntChoice.AwaitingKey args
else
match x with
| None ->
failwithf
"Unable to process argument %s as key %s and value %s"
arg
key
value
| Some msg ->
sprintf "%s (at arg %s)" msg arg |> ArgParser_errors.Add
go ParseState_FlagsIntoPositionalArgsIntChoice.AwaitingKey args
else
arg |> (fun x -> System.Int32.Parse x) |> Choice1Of2 |> arg_1.Add
go ParseState_FlagsIntoPositionalArgsIntChoice.AwaitingKey args
| ParseState_FlagsIntoPositionalArgsIntChoice.AwaitingValue key ->
match processKeyValue key arg with
| Ok () -> go ParseState_FlagsIntoPositionalArgsIntChoice.AwaitingKey args
| Error exc ->
if setFlagValue key then
go ParseState_FlagsIntoPositionalArgsIntChoice.AwaitingKey (arg :: args)
else if true then
key |> (fun x -> System.Int32.Parse x) |> Choice1Of2 |> arg_1.Add
go ParseState_FlagsIntoPositionalArgsIntChoice.AwaitingKey (arg :: args)
else
match exc with
| None ->
failwithf
"Unable to process supplied arg %s. Help text follows.\n%s"
key
(helpText ())
| Some msg -> msg |> ArgParser_errors.Add
go ParseState_FlagsIntoPositionalArgsIntChoice.AwaitingKey args
let arg_1 = arg_1 |> Seq.toList
let arg_0 =
match arg_0 with
| None ->
sprintf "Required argument '%s' received no value" (sprintf "--%s" "a")
|> ArgParser_errors.Add
Unchecked.defaultof<_>
| Some x -> x
if 0 = ArgParser_errors.Count then
{
A = arg_0
GrabEverything = arg_1
}
else
ArgParser_errors |> String.concat "\n" |> failwithf "Errors during parse!\n%s"
static member parse (args : string list) : FlagsIntoPositionalArgsIntChoice =
FlagsIntoPositionalArgsIntChoice.parse' System.Environment.GetEnvironmentVariable args
namespace ConsumePlugin
open System
open System.IO
open WoofWare.Myriad.Plugins
/// Methods to parse arguments for the type FlagsIntoPositionalArgs'
[<AutoOpen>]
module FlagsIntoPositionalArgs'ArgParse =
@@ -3796,6 +4291,9 @@ module FlagsIntoPositionalArgs'ArgParse =
| Error exc ->
if setFlagValue key then
go ParseState_FlagsIntoPositionalArgs'.AwaitingKey (arg :: args)
else if false then
key |> (fun x -> x) |> arg_1.Add
go ParseState_FlagsIntoPositionalArgs'.AwaitingKey (arg :: args)
else
match exc with
| None ->

View File

@@ -0,0 +1,200 @@
//------------------------------------------------------------------------------
// This code was generated by myriad.
// Changes to this file will be lost when the code is regenerated.
//------------------------------------------------------------------------------
namespace SomeNamespace
open System
/// Mock record type for an interface
type internal PublicTypeNoAttrMock =
{
Mem1 : string * int -> string list
Mem2 : string -> int
Mem3 : int * option<System.Threading.CancellationToken> -> string
}
/// An implementation where every method throws.
static member Empty : PublicTypeNoAttrMock =
{
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
Mem2 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem2"))
Mem3 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem3"))
}
interface IPublicTypeNoAttr with
member this.Mem1 (arg_0_0, arg_0_1) = this.Mem1 (arg_0_0, arg_0_1)
member this.Mem2 arg_0_0 = this.Mem2 (arg_0_0)
member this.Mem3 (arg_0_0, arg_0_1) = this.Mem3 (arg_0_0, arg_0_1)
namespace SomeNamespace
open System
/// Mock record type for an interface
type public PublicTypeInternalFalseNoAttrMock =
{
Mem1 : string * int -> string list
Mem2 : string -> int
Mem3 : int * option<System.Threading.CancellationToken> -> string
}
/// An implementation where every method throws.
static member Empty : PublicTypeInternalFalseNoAttrMock =
{
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
Mem2 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem2"))
Mem3 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem3"))
}
interface IPublicTypeInternalFalseNoAttr with
member this.Mem1 (arg_0_0, arg_0_1) = this.Mem1 (arg_0_0, arg_0_1)
member this.Mem2 arg_0_0 = this.Mem2 (arg_0_0)
member this.Mem3 (arg_0_0, arg_0_1) = this.Mem3 (arg_0_0, arg_0_1)
namespace SomeNamespace
open System
/// Mock record type for an interface
type internal InternalTypeNoAttrMock =
{
Mem1 : string * int -> unit
Mem2 : string -> int
}
/// An implementation where every method throws.
static member Empty : InternalTypeNoAttrMock =
{
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
Mem2 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem2"))
}
interface InternalTypeNoAttr with
member this.Mem1 (arg_0_0, arg_0_1) = this.Mem1 (arg_0_0, arg_0_1)
member this.Mem2 arg_0_0 = this.Mem2 (arg_0_0)
namespace SomeNamespace
open System
/// Mock record type for an interface
type private PrivateTypeNoAttrMock =
{
Mem1 : string * int -> unit
Mem2 : string -> int
}
/// An implementation where every method throws.
static member Empty : PrivateTypeNoAttrMock =
{
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
Mem2 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem2"))
}
interface PrivateTypeNoAttr with
member this.Mem1 (arg_0_0, arg_0_1) = this.Mem1 (arg_0_0, arg_0_1)
member this.Mem2 arg_0_0 = this.Mem2 (arg_0_0)
namespace SomeNamespace
open System
/// Mock record type for an interface
type private PrivateTypeInternalFalseNoAttrMock =
{
Mem1 : string * int -> unit
Mem2 : string -> int
}
/// An implementation where every method throws.
static member Empty : PrivateTypeInternalFalseNoAttrMock =
{
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
Mem2 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem2"))
}
interface PrivateTypeInternalFalseNoAttr with
member this.Mem1 (arg_0_0, arg_0_1) = this.Mem1 (arg_0_0, arg_0_1)
member this.Mem2 arg_0_0 = this.Mem2 (arg_0_0)
namespace SomeNamespace
open System
/// Mock record type for an interface
type internal VeryPublicTypeNoAttrMock<'a, 'b> =
{
Mem1 : 'a -> 'b
}
/// An implementation where every method throws.
static member Empty () : VeryPublicTypeNoAttrMock<'a, 'b> =
{
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
}
interface VeryPublicTypeNoAttr<'a, 'b> with
member this.Mem1 arg_0_0 = this.Mem1 (arg_0_0)
namespace SomeNamespace
open System
/// Mock record type for an interface
type internal CurriedNoAttrMock<'a> =
{
Mem1 : int -> 'a -> string
Mem2 : int * string -> 'a -> string
Mem3 : (int * string) -> 'a -> string
Mem4 : (int * string) -> ('a * int) -> string
Mem5 : int * string -> ('a * int) -> string
Mem6 : int * string -> 'a * int -> string
}
/// An implementation where every method throws.
static member Empty () : CurriedNoAttrMock<'a> =
{
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
Mem2 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem2"))
Mem3 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem3"))
Mem4 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem4"))
Mem5 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem5"))
Mem6 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem6"))
}
interface CurriedNoAttr<'a> with
member this.Mem1 arg_0_0 arg_1_0 = this.Mem1 (arg_0_0) (arg_1_0)
member this.Mem2 (arg_0_0, arg_0_1) arg_1_0 = this.Mem2 (arg_0_0, arg_0_1) (arg_1_0)
member this.Mem3 ((arg_0_0, arg_0_1)) arg_1_0 = this.Mem3 (arg_0_0, arg_0_1) (arg_1_0)
member this.Mem4 ((arg_0_0, arg_0_1)) ((arg_1_0, arg_1_1)) =
this.Mem4 (arg_0_0, arg_0_1) (arg_1_0, arg_1_1)
member this.Mem5 (arg_0_0, arg_0_1) ((arg_1_0, arg_1_1)) =
this.Mem5 (arg_0_0, arg_0_1) (arg_1_0, arg_1_1)
member this.Mem6 (arg_0_0, arg_0_1) (arg_1_0, arg_1_1) =
this.Mem6 (arg_0_0, arg_0_1) (arg_1_0, arg_1_1)
namespace SomeNamespace
open System
/// Mock record type for an interface
type internal TypeWithInterfaceNoAttrMock =
{
/// Implementation of IDisposable.Dispose
Dispose : unit -> unit
Mem1 : string option -> string[] Async
Mem2 : unit -> string[] Async
}
/// An implementation where every method throws.
static member Empty : TypeWithInterfaceNoAttrMock =
{
Dispose = (fun () -> ())
Mem1 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem1"))
Mem2 = (fun _ -> raise (System.NotImplementedException "Unimplemented mock function: Mem2"))
}
interface TypeWithInterfaceNoAttr with
member this.Mem1 arg_0_0 = this.Mem1 (arg_0_0)
member this.Mem2 () = this.Mem2 (())
interface System.IDisposable with
member this.Dispose () : unit = this.Dispose ()

View File

@@ -29,7 +29,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri (("v1/gyms/"), System.UriKind.Relative)
)
@@ -59,7 +59,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri (
"v1/gyms/{gym_id}/attendance"
@@ -93,7 +93,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri (
"v1/gyms/{gym_id}/attendance"
@@ -127,7 +127,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri ("v1/member", System.UriKind.Relative)
)
@@ -157,7 +157,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri (
"v1/gyms/{gym}"
@@ -191,7 +191,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri ("v1/member/activity", System.UriKind.Relative)
)
@@ -221,7 +221,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri ("some/url", System.UriKind.Relative)
)
@@ -251,7 +251,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri ("some/url", System.UriKind.Relative)
)
@@ -317,7 +317,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri (
("/v2/gymSessions/member"
@@ -358,7 +358,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri (
("/v2/gymSessions/member?foo=1"
@@ -399,7 +399,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri ("users/new", System.UriKind.Relative)
)
@@ -426,7 +426,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri ("users/new", System.UriKind.Relative)
)
@@ -453,7 +453,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri ("users/new", System.UriKind.Relative)
)
@@ -480,7 +480,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri ("users/new", System.UriKind.Relative)
)
@@ -507,7 +507,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri ("users/new", System.UriKind.Relative)
)
@@ -534,7 +534,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri ("users/new", System.UriKind.Relative)
)
@@ -567,7 +567,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri ("users/new", System.UriKind.Relative)
)
@@ -600,7 +600,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri ("users/new", System.UriKind.Relative)
)
@@ -633,7 +633,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri ("users/new", System.UriKind.Relative)
)
@@ -659,7 +659,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri (
"endpoint/{param}"
@@ -688,7 +688,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri ("endpoint", System.UriKind.Relative)
)
@@ -713,7 +713,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri ("endpoint", System.UriKind.Relative)
)
@@ -738,7 +738,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri ("endpoint", System.UriKind.Relative)
)
@@ -763,7 +763,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri ("endpoint", System.UriKind.Relative)
)
@@ -787,7 +787,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri ("endpoint", System.UriKind.Relative)
)
@@ -811,7 +811,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri ("endpoint", System.UriKind.Relative)
)
@@ -835,7 +835,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri ("endpoint", System.UriKind.Relative)
)
@@ -859,7 +859,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri ("endpoint", System.UriKind.Relative)
)
@@ -895,7 +895,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri ("endpoint", System.UriKind.Relative)
)
@@ -931,7 +931,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri ("endpoint", System.UriKind.Relative)
)
@@ -967,7 +967,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri ("endpoint", System.UriKind.Relative)
)
@@ -1003,7 +1003,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri ("endpoint", System.UriKind.Relative)
)
@@ -1026,7 +1026,7 @@ module PureGymApi =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| null -> System.Uri "https://whatnot.com/"
| v -> v),
System.Uri ("endpoint", System.UriKind.Relative)
)
@@ -1116,15 +1116,18 @@ module ApiWithBasePath =
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."
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),
| v -> v),
System.Uri ("foo/", System.UriKind.Relative)
),
System.Uri (
"endpoint/{param}"
.Replace ("{param}", parameter.ToString () |> System.Web.HttpUtility.UrlEncode),
@@ -1167,9 +1170,12 @@ module ApiWithBasePathAndAddress =
let uri =
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com"
| v -> v),
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com/thing/"
| v -> v),
System.Uri ("foo/", System.UriKind.Relative)
),
System.Uri (
"endpoint/{param}"
.Replace ("{param}", parameter.ToString () |> System.Web.HttpUtility.UrlEncode),
@@ -1200,6 +1206,312 @@ open System.Net
open System.Net.Http
open RestEase
/// Module for constructing a REST client.
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix) ; RequireQualifiedAccess>]
module ApiWithAbsoluteBasePath =
/// Create a REST client.
let make (client : System.Net.Http.HttpClient) : IApiWithAbsoluteBasePath =
{ new IApiWithAbsoluteBasePath with
member _.GetPathParam (parameter : string, cancellationToken : CancellationToken option) =
async {
let! ct = Async.CancellationToken
let uri =
System.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 ("/foo/", System.UriKind.Relative)
),
System.Uri (
"endpoint/{param}"
.Replace ("{param}", parameter.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! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask
return responseString
}
|> (fun a -> Async.StartAsTask (a, ?cancellationToken = cancellationToken))
}
namespace PureGym
open System
open System.Threading
open System.Threading.Tasks
open System.IO
open System.Net
open System.Net.Http
open RestEase
/// Module for constructing a REST client.
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix) ; RequireQualifiedAccess>]
module ApiWithAbsoluteBasePathAndAddress =
/// Create a REST client.
let make (client : System.Net.Http.HttpClient) : IApiWithAbsoluteBasePathAndAddress =
{ new IApiWithAbsoluteBasePathAndAddress with
member _.GetPathParam (parameter : string, ct : CancellationToken option) =
async {
let! ct = Async.CancellationToken
let uri =
System.Uri (
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com/thing/"
| v -> v),
System.Uri ("/foo/", System.UriKind.Relative)
),
System.Uri (
"endpoint/{param}"
.Replace ("{param}", parameter.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! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask
return responseString
}
|> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct))
}
namespace PureGym
open System
open System.Threading
open System.Threading.Tasks
open System.IO
open System.Net
open System.Net.Http
open RestEase
/// Module for constructing a REST client.
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix) ; RequireQualifiedAccess>]
module ApiWithBasePathAndAbsoluteEndpoint =
/// Create a REST client.
let make (client : System.Net.Http.HttpClient) : IApiWithBasePathAndAbsoluteEndpoint =
{ new IApiWithBasePathAndAbsoluteEndpoint with
member _.GetPathParam (parameter : string, cancellationToken : CancellationToken option) =
async {
let! ct = Async.CancellationToken
let uri =
System.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 ("foo/", System.UriKind.Relative)
),
System.Uri (
"/endpoint/{param}"
.Replace ("{param}", parameter.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! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask
return responseString
}
|> (fun a -> Async.StartAsTask (a, ?cancellationToken = cancellationToken))
}
namespace PureGym
open System
open System.Threading
open System.Threading.Tasks
open System.IO
open System.Net
open System.Net.Http
open RestEase
/// Module for constructing a REST client.
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix) ; RequireQualifiedAccess>]
module ApiWithBasePathAndAddressAndAbsoluteEndpoint =
/// Create a REST client.
let make (client : System.Net.Http.HttpClient) : IApiWithBasePathAndAddressAndAbsoluteEndpoint =
{ new IApiWithBasePathAndAddressAndAbsoluteEndpoint with
member _.GetPathParam (parameter : string, ct : CancellationToken option) =
async {
let! ct = Async.CancellationToken
let uri =
System.Uri (
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com/thing/"
| v -> v),
System.Uri ("foo/", System.UriKind.Relative)
),
System.Uri (
"/endpoint/{param}"
.Replace ("{param}", parameter.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! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask
return responseString
}
|> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct))
}
namespace PureGym
open System
open System.Threading
open System.Threading.Tasks
open System.IO
open System.Net
open System.Net.Http
open RestEase
/// Module for constructing a REST client.
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix) ; RequireQualifiedAccess>]
module ApiWithAbsoluteBasePathAndAbsoluteEndpoint =
/// Create a REST client.
let make (client : System.Net.Http.HttpClient) : IApiWithAbsoluteBasePathAndAbsoluteEndpoint =
{ new IApiWithAbsoluteBasePathAndAbsoluteEndpoint with
member _.GetPathParam (parameter : string, cancellationToken : CancellationToken option) =
async {
let! ct = Async.CancellationToken
let uri =
System.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 ("/foo/", System.UriKind.Relative)
),
System.Uri (
"/endpoint/{param}"
.Replace ("{param}", parameter.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! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask
return responseString
}
|> (fun a -> Async.StartAsTask (a, ?cancellationToken = cancellationToken))
}
namespace PureGym
open System
open System.Threading
open System.Threading.Tasks
open System.IO
open System.Net
open System.Net.Http
open RestEase
/// Module for constructing a REST client.
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix) ; RequireQualifiedAccess>]
module ApiWithAbsoluteBasePathAndAddressAndAbsoluteEndpoint =
/// Create a REST client.
let make (client : System.Net.Http.HttpClient) : IApiWithAbsoluteBasePathAndAddressAndAbsoluteEndpoint =
{ new IApiWithAbsoluteBasePathAndAddressAndAbsoluteEndpoint with
member _.GetPathParam (parameter : string, ct : CancellationToken option) =
async {
let! ct = Async.CancellationToken
let uri =
System.Uri (
System.Uri (
(match client.BaseAddress with
| null -> System.Uri "https://whatnot.com/thing/"
| v -> v),
System.Uri ("/foo/", System.UriKind.Relative)
),
System.Uri (
"/endpoint/{param}"
.Replace ("{param}", parameter.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! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask
return responseString
}
|> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct))
}
namespace PureGym
open System
open System.Threading
open System.Threading.Tasks
open System.IO
open System.Net
open System.Net.Http
open RestEase
/// Module for constructing a REST client.
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix) ; RequireQualifiedAccess>]
module ApiWithHeaders =
@@ -1245,6 +1557,7 @@ module ApiWithHeaders =
do httpMessage.Headers.Add ("X-Foo", this.SomeHeader.ToString ())
do httpMessage.Headers.Add ("Authorization", this.SomeOtherHeader.ToString ())
do httpMessage.Headers.Add ("Header-Name", "Header-Value")
do httpMessage.Headers.Add ("Something-Else", "val")
let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask
let response = response.EnsureSuccessStatusCode ()
let! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask

View File

@@ -210,6 +210,8 @@ module JsonRecordTypeWithBothJsonSerializeExtension =
|> (fun field -> field.ToString "o" |> System.Text.Json.Nodes.JsonValue.Create<string>))
)
node.Add ("unit", (input.Unit |> (fun value -> System.Text.Json.Nodes.JsonObject ())))
node :> _
namespace ConsumePlugin
@@ -291,6 +293,60 @@ module FooJsonSerializeExtension =
)
node :> _
namespace ConsumePlugin
open System
open System.Collections.Generic
open System.Text.Json.Serialization
/// Module containing JSON serializing extension members for the CollectRemaining type
[<AutoOpen>]
module CollectRemainingJsonSerializeExtension =
/// Extension methods for JSON parsing
type CollectRemaining with
/// Serialize to a JSON node
static member toJsonNode (input : CollectRemaining) : System.Text.Json.Nodes.JsonNode =
let node = System.Text.Json.Nodes.JsonObject ()
do
node.Add (
"message",
(input.Message
|> (fun field ->
match field with
| None -> null :> System.Text.Json.Nodes.JsonNode
| Some field -> HeaderAndValue.toJsonNode field
))
)
for KeyValue (key, value) in input.Rest do
node.Add (key, id value)
node :> _
namespace ConsumePlugin
open System
open System.Collections.Generic
open System.Text.Json.Serialization
/// Module containing JSON serializing extension members for the OuterCollectRemaining type
[<AutoOpen>]
module OuterCollectRemainingJsonSerializeExtension =
/// Extension methods for JSON parsing
type OuterCollectRemaining with
/// Serialize to a JSON node
static member toJsonNode (input : OuterCollectRemaining) : System.Text.Json.Nodes.JsonNode =
let node = System.Text.Json.Nodes.JsonObject ()
do
for KeyValue (key, value) in input.Others do
node.Add (key, System.Text.Json.Nodes.JsonValue.Create<int> value)
node.Add ("remaining", (input.Remaining |> CollectRemaining.toJsonNode))
node :> _
namespace ConsumePlugin
@@ -424,6 +480,8 @@ module JsonRecordTypeWithBothJsonParseExtension =
/// Parse from a JSON node.
static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : JsonRecordTypeWithBoth =
let arg_21 = ()
let arg_20 =
(match node.["timestamp"] with
| null ->
@@ -705,6 +763,7 @@ module JsonRecordTypeWithBothJsonParseExtension =
IntMeasureNullable = arg_18
Enum = arg_19
Timestamp = arg_20
Unit = arg_21
}
namespace ConsumePlugin
@@ -842,3 +901,83 @@ module FooJsonParseExtension =
{
Message = arg_0
}
namespace ConsumePlugin
/// Module containing JSON parsing extension members for the CollectRemaining type
[<AutoOpen>]
module CollectRemainingJsonParseExtension =
/// Extension methods for JSON parsing
type CollectRemaining with
/// Parse from a JSON node.
static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : CollectRemaining =
let arg_1 =
let result =
System.Collections.Generic.Dictionary<string, System.Text.Json.Nodes.JsonNode> ()
let node = node.AsObject ()
for KeyValue (key, value) in node do
if key = "message" then () else result.Add (key, node.[key])
result
let arg_0 =
match node.["message"] with
| null -> None
| v -> HeaderAndValue.jsonParse v |> Some
{
Message = arg_0
Rest = arg_1
}
namespace ConsumePlugin
/// Module containing JSON parsing extension members for the OuterCollectRemaining type
[<AutoOpen>]
module OuterCollectRemainingJsonParseExtension =
/// Extension methods for JSON parsing
type OuterCollectRemaining with
/// Parse from a JSON node.
static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : OuterCollectRemaining =
let arg_1 =
CollectRemaining.jsonParse (
match node.["remaining"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("remaining")
)
)
| v -> v
)
let arg_0 =
let result = System.Collections.Generic.Dictionary<string, int> ()
let node = node.AsObject ()
for KeyValue (key, value) in node do
if key = "remaining" then
()
else
result.Add (
key,
(match node.[key] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" (key)
)
)
| v -> v)
.AsValue()
.GetValue<System.Int32> ()
)
result
{
Others = arg_0
Remaining = arg_1
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,41 @@
namespace SomeNamespace
open System
type IPublicTypeNoAttr =
abstract Mem1 : string * int -> string list
abstract Mem2 : string -> int
abstract Mem3 : x : int * ?ct : System.Threading.CancellationToken -> string
type IPublicTypeInternalFalseNoAttr =
abstract Mem1 : string * int -> string list
abstract Mem2 : string -> int
abstract Mem3 : x : int * ?ct : System.Threading.CancellationToken -> string
type internal InternalTypeNoAttr =
abstract Mem1 : string * int -> unit
abstract Mem2 : string -> int
type private PrivateTypeNoAttr =
abstract Mem1 : string * int -> unit
abstract Mem2 : string -> int
type private PrivateTypeInternalFalseNoAttr =
abstract Mem1 : string * int -> unit
abstract Mem2 : string -> int
type VeryPublicTypeNoAttr<'a, 'b> =
abstract Mem1 : 'a -> 'b
type CurriedNoAttr<'a> =
abstract Mem1 : int -> 'a -> string
abstract Mem2 : int * string -> 'a -> string
abstract Mem3 : (int * string) -> 'a -> string
abstract Mem4 : (int * string) -> ('a * int) -> string
abstract Mem5 : x : int * string -> ('a * int) -> string
abstract Mem6 : int * string -> y : 'a * int -> string
type TypeWithInterfaceNoAttr =
inherit IDisposable
abstract Mem1 : string option -> string[] Async
abstract Mem2 : unit -> string[] Async

View File

@@ -122,8 +122,6 @@ type internal IApiWithoutBaseAddress =
[<Get "endpoint/{param}">]
abstract GetPathParam : [<Path "param">] parameter : string * ?ct : CancellationToken -> Task<string>
// TODO: implement BasePath support
[<WoofWare.Myriad.Plugins.HttpClient>]
[<BasePath "foo">]
type IApiWithBasePath =
@@ -132,12 +130,54 @@ type IApiWithBasePath =
abstract GetPathParam : [<Path "param">] parameter : string * ?cancellationToken : CancellationToken -> Task<string>
[<WoofWare.Myriad.Plugins.HttpClient>]
[<BaseAddress "https://whatnot.com">]
[<BaseAddress "https://whatnot.com/thing">]
[<BasePath "foo">]
type IApiWithBasePathAndAddress =
[<Get "endpoint/{param}">]
abstract GetPathParam : [<Path "param">] parameter : string * ?ct : CancellationToken -> Task<string>
[<WoofWare.Myriad.Plugins.HttpClient>]
[<BasePath "/foo">]
type IApiWithAbsoluteBasePath =
// Example where we use the bundled attributes rather than RestEase's
[<WoofWare.Myriad.Plugins.RestEase.Get "endpoint/{param}">]
abstract GetPathParam : [<Path "param">] parameter : string * ?cancellationToken : CancellationToken -> Task<string>
[<WoofWare.Myriad.Plugins.HttpClient>]
[<BaseAddress "https://whatnot.com/thing">]
[<BasePath "/foo">]
type IApiWithAbsoluteBasePathAndAddress =
[<Get "endpoint/{param}">]
abstract GetPathParam : [<Path "param">] parameter : string * ?ct : CancellationToken -> Task<string>
[<WoofWare.Myriad.Plugins.HttpClient>]
[<BasePath "foo">]
type IApiWithBasePathAndAbsoluteEndpoint =
// Example where we use the bundled attributes rather than RestEase's
[<WoofWare.Myriad.Plugins.RestEase.Get "/endpoint/{param}">]
abstract GetPathParam : [<Path "param">] parameter : string * ?cancellationToken : CancellationToken -> Task<string>
[<WoofWare.Myriad.Plugins.HttpClient>]
[<BaseAddress "https://whatnot.com/thing">]
[<BasePath "foo">]
type IApiWithBasePathAndAddressAndAbsoluteEndpoint =
[<Get "/endpoint/{param}">]
abstract GetPathParam : [<Path "param">] parameter : string * ?ct : CancellationToken -> Task<string>
[<WoofWare.Myriad.Plugins.HttpClient>]
[<BasePath "/foo">]
type IApiWithAbsoluteBasePathAndAbsoluteEndpoint =
// Example where we use the bundled attributes rather than RestEase's
[<WoofWare.Myriad.Plugins.RestEase.Get "/endpoint/{param}">]
abstract GetPathParam : [<Path "param">] parameter : string * ?cancellationToken : CancellationToken -> Task<string>
[<WoofWare.Myriad.Plugins.HttpClient>]
[<BaseAddress "https://whatnot.com/thing">]
[<BasePath "/foo">]
type IApiWithAbsoluteBasePathAndAddressAndAbsoluteEndpoint =
[<Get "/endpoint/{param}">]
abstract GetPathParam : [<Path "param">] parameter : string * ?ct : CancellationToken -> Task<string>
[<WoofWare.Myriad.Plugins.HttpClient>]
[<Header("Header-Name", "Header-Value")>]
type IApiWithHeaders =
@@ -148,6 +188,7 @@ type IApiWithHeaders =
abstract SomeOtherHeader : int
[<Get "endpoint/{param}">]
[<Header("Something-Else", "val")>]
abstract GetPathParam : [<Path "param">] parameter : string * ?ct : CancellationToken -> Task<string>
[<WoofWare.Myriad.Plugins.HttpClient>]

View File

@@ -50,6 +50,7 @@ type JsonRecordTypeWithBoth =
IntMeasureNullable : int<measure> Nullable
Enum : SomeEnum
Timestamp : DateTimeOffset
Unit : unit
}
[<WoofWare.Myriad.Plugins.JsonSerialize true>]
@@ -73,3 +74,21 @@ type Foo =
{
Message : HeaderAndValue option
}
[<WoofWare.Myriad.Plugins.JsonSerialize true>]
[<WoofWare.Myriad.Plugins.JsonParse true>]
type CollectRemaining =
{
Message : HeaderAndValue option
[<JsonExtensionData>]
Rest : Dictionary<string, System.Text.Json.Nodes.JsonNode>
}
[<WoofWare.Myriad.Plugins.JsonSerialize true>]
[<WoofWare.Myriad.Plugins.JsonParse true>]
type OuterCollectRemaining =
{
[<JsonExtensionData>]
Others : Dictionary<string, int>
Remaining : CollectRemaining
}

File diff suppressed because it is too large Load Diff

120
README.md
View File

@@ -14,7 +14,8 @@ Currently implemented:
* `JsonSerialize` (to stamp out `toJsonNode : 'T -> JsonNode` methods).
* `HttpClient` (to stamp out a [RestEase](https://github.com/canton7/RestEase)-style HTTP client).
* `GenerateMock` (to stamp out a record type corresponding to an interface, like a compile-time [Foq](https://github.com/fsprojects/Foq)).
* `ArgParser` (to stamp out a basic argument parser)
* `ArgParser` (to stamp out a basic argument parser).
* `SwaggerClient` (to stamp out an HTTP client for a Swagger API).
* `CreateCatamorphism` (to stamp out a non-stack-overflowing [catamorphism](https://fsharpforfunandprofit.com/posts/recursive-types-and-folds/) for a discriminated union).
* `RemoveOptions` (to strip `option` modifiers from a type) - this one is particularly half-baked!
@@ -156,6 +157,10 @@ For an example of using both `JsonParse` and `JsonSerialize` together with compl
Takes a record like this:
```fsharp
type DryRunMode =
| [<ArgumentFlag true> Dry
| [<ArgumentFlag false> Wet
[<ArgParser>]
type Foo =
{
@@ -166,12 +171,16 @@ type Foo =
B : Choice<int, int>
[<ArgumentDefaultEnvironmentVariable "MY_ENV_VAR">]
BWithEnv : Choice<int, int>
[<ArgumentDefaultFunction>]
DryRun : DryRunMode
[<ArgumentLongForm "longer-form-replaces-c">]
C : float list
// optionally:
[<PositionalArgs>]
Rest : string list // or e.g. `int list` if you want them parsed into a type too
}
static member DefaultB () = 4
static member DefaultDryRun () = DryRunMode.Wet
```
and stamps out a basic `parse` method of this signature:
@@ -218,6 +227,85 @@ This is very bare-bones, but do raise GitHub issues if you like (or if you find
It should work fine if you just want to compose a few primitive types, though.
## `SwaggerClient`
Takes a JSON-schema definition of a [Swagger API](https://swagger.io/), and stamps out a client like this:
```fsharp
/// A type which was defined in the Swagger spec
[<JsonParse true ; JsonSerialize true>]
type SwaggerType1 =
{
[<System.Text.Json.Serialization.JsonExtensionData>]
AdditionalProperties : System.Collections.Generic.Dictionary<string, System.Text.Json.Nodes.JsonNode>
Message : string
}
/// Documentation from the Swagger spec
[<HttpClient false ; RestEase.BasePath "/api/v1">]
type IGitea =
/// Returns the Person actor for a user
[<RestEase.Get "/activitypub/user/{username}">]
abstract ActivitypubPerson :
[<RestEase.Path "username">] username : string * ?ct : System.Threading.CancellationToken ->
ActivityPub System.Threading.Tasks.Task
```
Notice that we automatically decorate the type with our `[<HttpClient>]` attribute, so if you choose to do so, you can chain another Myriad generated file off this one and you'll get a RestEase-style client stamped out.
(See below, searching on the string `"Generated2SwaggerGitea.fs"`, for an example.)
You don't need to `Content Include` or `EmbeddedResource Include` the JSON schema.
`None Include` will do; we only need the source to be available at build time.
You *do* need to include the following configuration:
```xml
<Compile Include="GeneratedClient.fs">
<!-- This bit is normal: -->
<MyriadFile>swagger.json</MyriadFile>
<!-- This bit is new and required! -->
<MyriadParams>
<ClassName>GiteaClient</ClassName>
<!-- Optionally: -->
<GenerateMock>true</GenerateMock>
</MyriadParams>
</Compile>
```
The `<ClassName />` key tells us what to name the resulting interface (it gets an `I` prepended for you).
You can optionally also set `<GenerateMockVisibility>v</GenerateMockVisibility>` to add the `[<GenerateMock>]` attribute to the type
(where `v` should be `internal` or `public`, indicating "resulting mock type is internal" vs "is public"),
so that the following manoeuvre will result in a generated mock:
```xml
<None Include="swagger-gitea.json" />
<Compile Include="GeneratedSwaggerGitea.fs">
<MyriadFile>swagger-gitea.json</MyriadFile>
<MyriadParams>
<GenerateMockVisibility>public</GenerateMockVisibility>
<ClassName>Gitea</ClassName>
</MyriadParams>
</Compile>
<Compile Include="Generated2SwaggerGitea.fs">
<MyriadFile>GeneratedSwaggerGitea.fs</MyriadFile>
</Compile>
```
(Note that you do have to create the `GeneratedSwaggerGitea.fs` file manually before code generation happens. Myriad will throw if that file isn't there, because `Generated2SwaggerGitea.fs` depends on it so Myriad wants to compute its hash. Just make an empty file.)
### What's the point?
[`SwaggerProvider`](https://github.com/fsprojects/SwaggerProvider) is *absolutely magical*, but it's kind of witchcraft.
I fear no man, but that thing… it scares me.
Also, builds using `SwaggerProvider` appear to be inherently nondeterministic, even if the data source doesn't change.
## Limitations
Swagger API specs appear to be pretty cowboy in the wild.
I try to cope with invalid schemas I have seen, but I can't guarantee I do so correctly.
Definitely do perform integration tests and let me know of weird specs you encounter, and bits of the (very extensive) Swagger spec I have omitted!
## `RemoveOptions`
Takes a record like this:
@@ -516,6 +604,36 @@ For example, this specifies that Myriad is to use the contents of `Client.fs` to
</ItemGroup>
```
## Alternative use without the attributes
You can avoid taking a reference on the `WoofWare.Myriad.Plugins.Attributes` assembly, instead putting all the configuration into the project file.
This is implemented for everything except the SwaggerClientGenerator.
```xml
<Project>
<ItemGroup>
<Compile Include="Client.fs" />
<Compile Include="GeneratedClient.fs">
<MyriadFile>Client.fs</MyriadFile>
<MyriadParams>
<MyTypeName1>GenerateMock(false)!JsonParse</MyTypeName1>
<SomeOtherTypeName>GenerateMock</SomeOtherTypeName>
</MyriadParams>
</Compile>
</ItemGroup>
<ItemGroup>
<PackageReference Include="WoofWare.Myriad.Plugins" Version="$(WoofWareMyriadPluginVersion)" PrivateAssets="all" />
<PackageReference Include="Myriad.Sdk" Version="0.8.3" PrivateAssets="all" />
</ItemGroup>
</Project>
```
That is, you specify a `!`-delimited list of the attributes you *would* apply to the type.
Supply "arguments" to the attribute name in the project file as you would to the attribute itself.
(Yes, this is indeed incredibly cumbersome, and you're not interested in the reasons it's all so mad!
I'm hopefully going to get round to writing a more powerful source generation system which won't have these limitations.)
### Myriad Gotchas
* MsBuild doesn't always realise that it needs to invoke Myriad during rebuild.

View File

@@ -45,6 +45,9 @@ module RestEase =
/// Indicates that this interface represents a REST client which accesses an API whose paths are
/// all relative to the given address.
///
/// We will essentially unconditionally append a slash to this for you, on the grounds that you probably don't
/// intend the base path *itself* to be an endpoint.
type BaseAddressAttribute (addr : string) =
inherit Attribute ()
@@ -61,3 +64,21 @@ module RestEase =
inherit Attribute ()
new (path : string) = PathAttribute (Some path)
new () = PathAttribute None
/// Indicates that this argument to a method is passed to the remote API by being serialised into the request
/// body.
type BodyAttribute () =
inherit Attribute ()
/// This is interpolated into every URL, between the BaseAddress and the path specified by e.g. [<Get>].
/// Note that if the [<Get>]-specified path starts with a slash, the BasePath is ignored, because then [<Get>]
/// is considered to be relative to the URL root (i.e. the host part of the BaseAddress).
/// Similarly, if the [<BasePath>] starts with a slash, then any path component of the BaseAddress is ignored.
///
/// We will essentially unconditionally append a slash to this for you, on the grounds that you probably don't
/// intend the base path *itself* to be an endpoint.
///
/// Can contain {placeholders}; hopefully your methods define values for those placeholders with [<Path>]
/// attributes!
type BasePathAttribute (path : string) =
inherit Attribute ()

View File

@@ -49,6 +49,10 @@ WoofWare.Myriad.Plugins.RemoveOptionsAttribute..ctor [constructor]: unit
WoofWare.Myriad.Plugins.RestEase inherit obj
WoofWare.Myriad.Plugins.RestEase+BaseAddressAttribute inherit System.Attribute
WoofWare.Myriad.Plugins.RestEase+BaseAddressAttribute..ctor [constructor]: string
WoofWare.Myriad.Plugins.RestEase+BasePathAttribute inherit System.Attribute
WoofWare.Myriad.Plugins.RestEase+BasePathAttribute..ctor [constructor]: string
WoofWare.Myriad.Plugins.RestEase+BodyAttribute inherit System.Attribute
WoofWare.Myriad.Plugins.RestEase+BodyAttribute..ctor [constructor]: unit
WoofWare.Myriad.Plugins.RestEase+DeleteAttribute inherit System.Attribute
WoofWare.Myriad.Plugins.RestEase+DeleteAttribute..ctor [constructor]: string
WoofWare.Myriad.Plugins.RestEase+GetAttribute inherit System.Attribute

View File

@@ -1,5 +1,5 @@
{
"version": "3.5",
"version": "3.6",
"publicReleaseRefSpec": [
"^refs/heads/main$"
],

View File

@@ -623,30 +623,71 @@ Required argument '--exact' received no value"""
let ``Can collect *all* non-help args into positional args with includeFlagLike`` () =
let getEnvVar (_ : string) = failwith "do not call"
FlagsIntoPositionalArgs.parse' getEnvVar [ "--a" ; "foo" ; "--b=false" ; "--c=hi" ; "--" ; "--help" ]
FlagsIntoPositionalArgs.parse' getEnvVar [ "--a" ; "foo" ; "--b=false" ; "--c" ; "hi" ; "--" ; "--help" ]
|> shouldEqual
{
A = "foo"
GrabEverything = [ "--b=false" ; "--c=hi" ; "--help" ]
GrabEverything = [ "--b=false" ; "--c" ; "hi" ; "--help" ]
}
// Users might consider this eccentric!
// But we're only a simple arg parser; we don't look around to see whether this is "almost"
// a valid parse.
FlagsIntoPositionalArgs.parse' getEnvVar [ "--a" ; "--b=false" ; "--c=hi" ; "--" ; "--help" ]
FlagsIntoPositionalArgs.parse' getEnvVar [ "--a" ; "--b=false" ; "--c" ; "hi" ; "--" ; "--help" ]
|> shouldEqual
{
A = "--b=false"
GrabEverything = [ "--c=hi" ; "--help" ]
GrabEverything = [ "--c" ; "hi" ; "--help" ]
}
[<Test>]
let ``Can refuse to collect non-help args`` () =
let ``Can collect non-help args into positional args with Choice`` () =
let getEnvVar (_ : string) = failwith "do not call"
FlagsIntoPositionalArgsChoice.parse' getEnvVar [ "--a" ; "foo" ; "--b=false" ; "--c" ; "hi" ; "--" ; "--help" ]
|> shouldEqual
{
A = "foo"
GrabEverything =
[
Choice1Of2 "--b=false"
Choice1Of2 "--c"
Choice1Of2 "hi"
Choice2Of2 "--help"
]
}
[<Test>]
let ``Can collect non-help args into positional args, and we parse on the way`` () =
let getEnvVar (_ : string) = failwith "do not call"
FlagsIntoPositionalArgsInt.parse' getEnvVar [ "3" ; "--a" ; "foo" ; "5" ; "--" ; "98" ]
|> shouldEqual
{
A = "foo"
GrabEverything = [ 3 ; 5 ; 98 ]
}
[<Test>]
let ``Can collect non-help args into positional args with Choice, and we parse on the way`` () =
let getEnvVar (_ : string) = failwith "do not call"
FlagsIntoPositionalArgsIntChoice.parse' getEnvVar [ "3" ; "--a" ; "foo" ; "5" ; "--" ; "98" ]
|> shouldEqual
{
A = "foo"
GrabEverything = [ Choice1Of2 3 ; Choice1Of2 5 ; Choice2Of2 98 ]
}
[<Test>]
let ``Can refuse to collect non-help args with PositionalArgs false`` () =
let getEnvVar (_ : string) = failwith "do not call"
let exc =
Assert.Throws<exn> (fun () ->
FlagsIntoPositionalArgs'.parse' getEnvVar [ "--a" ; "foo" ; "--b=false" ; "--c=hi" ; "--" ; "--help" ]
FlagsIntoPositionalArgs'.parse'
getEnvVar
[ "--a" ; "foo" ; "--b=false" ; "--c" ; "hi" ; "--" ; "--help" ]
|> ignore<FlagsIntoPositionalArgs'>
)

View File

@@ -9,18 +9,18 @@ open FsUnitTyped
[<TestFixture>]
module TestBasePath =
let replyWithUrl (message : HttpRequestMessage) : HttpResponseMessage Async =
async {
message.Method |> shouldEqual HttpMethod.Get
let content = new StringContent (message.RequestUri.ToString ())
let resp = new HttpResponseMessage (HttpStatusCode.OK)
resp.Content <- content
return resp
}
[<Test>]
let ``Base address is respected`` () =
let proc (message : HttpRequestMessage) : HttpResponseMessage Async =
async {
message.Method |> shouldEqual HttpMethod.Get
let content = new StringContent (message.RequestUri.ToString ())
let resp = new HttpResponseMessage (HttpStatusCode.OK)
resp.Content <- content
return resp
}
use client = HttpClientMock.makeNoUri proc
use client = HttpClientMock.makeNoUri replyWithUrl
let api = PureGymApi.make client
let observedUri = api.GetPathParam("param").Result
@@ -28,38 +28,28 @@ module TestBasePath =
[<Test>]
let ``Without a base address attr but with BaseAddress on client, request goes through`` () =
let proc (message : HttpRequestMessage) : HttpResponseMessage Async =
async {
message.Method |> shouldEqual HttpMethod.Get
let content = new StringContent (message.RequestUri.ToString ())
let resp = new HttpResponseMessage (HttpStatusCode.OK)
resp.Content <- content
return resp
}
use client = HttpClientMock.make (System.Uri "https://baseaddress.com") proc
use client = HttpClientMock.make (Uri "https://baseaddress.com") replyWithUrl
let api = ApiWithoutBaseAddress.make client
let observedUri = api.GetPathParam("param").Result
observedUri |> shouldEqual "https://baseaddress.com/endpoint/param"
[<Test>]
let ``Without a base address attr or BaseAddress on client, request throws`` () =
let proc (message : HttpRequestMessage) : HttpResponseMessage Async =
async {
message.Method |> shouldEqual HttpMethod.Get
let content = new StringContent (message.RequestUri.ToString ())
let resp = new HttpResponseMessage (HttpStatusCode.OK)
resp.Content <- content
return resp
}
let ``Base address on client takes precedence`` () =
use client = HttpClientMock.make (Uri "https://baseaddress.com") replyWithUrl
let api = PureGymApi.make client
use client = HttpClientMock.makeNoUri proc
let observedUri = api.GetPathParam("param").Result
observedUri |> shouldEqual "https://baseaddress.com/endpoint/param"
[<Test>]
let ``Without a base address attr or BaseAddress on client, request throws`` () =
use client = HttpClientMock.makeNoUri replyWithUrl
let api = ApiWithoutBaseAddress.make client
let observedExc =
async {
let! result = api.GetPathParam ("param") |> Async.AwaitTask |> Async.Catch
let! result = api.GetPathParam "param" |> Async.AwaitTask |> Async.Catch
match result with
| Choice1Of2 _ -> return failwith "test failure"
@@ -78,3 +68,103 @@ module TestBasePath =
observedExc.Message
|> shouldEqual
"No base address was supplied on the type, and no BaseAddress was on the HttpClient. (Parameter 'BaseAddress')"
[<Test>]
let ``Relative base path, no base address, relative attribute`` () : unit =
do
use client = HttpClientMock.makeNoUri replyWithUrl
let api = ApiWithBasePath.make client
let exc =
Assert.Throws<AggregateException> (fun () -> api.GetPathParam("hi").Result |> ignore<string>)
exc.InnerException.Message
|> shouldEqual
"No base address was supplied on the type, and no BaseAddress was on the HttpClient. (Parameter 'BaseAddress')"
use client = HttpClientMock.make (Uri "https://whatnot.com/thing/") replyWithUrl
let api = ApiWithBasePath.make client
let result = api.GetPathParam("hi").Result
result |> shouldEqual "https://whatnot.com/thing/foo/endpoint/hi"
[<Test>]
let ``Relative base path, base address, relative attribute`` () : unit =
use client = HttpClientMock.makeNoUri replyWithUrl
let api = ApiWithBasePathAndAddress.make client
let result = api.GetPathParam("hi").Result
result |> shouldEqual "https://whatnot.com/thing/foo/endpoint/hi"
[<Test>]
let ``Absolute base path, no base address, relative attribute`` () : unit =
do
use client = HttpClientMock.makeNoUri replyWithUrl
let api = ApiWithAbsoluteBasePath.make client
let exc =
Assert.Throws<AggregateException> (fun () -> api.GetPathParam("hi").Result |> ignore<string>)
exc.InnerException.Message
|> shouldEqual
"No base address was supplied on the type, and no BaseAddress was on the HttpClient. (Parameter 'BaseAddress')"
use client = HttpClientMock.make (Uri "https://whatnot.com/thing/") replyWithUrl
let api = ApiWithAbsoluteBasePath.make client
let result = api.GetPathParam("hi").Result
result |> shouldEqual "https://whatnot.com/foo/endpoint/hi"
[<Test>]
let ``Absolute base path, base address, relative attribute`` () : unit =
use client = HttpClientMock.makeNoUri replyWithUrl
let api = ApiWithAbsoluteBasePathAndAddress.make client
let result = api.GetPathParam("hi").Result
result |> shouldEqual "https://whatnot.com/foo/endpoint/hi"
[<Test>]
let ``Relative base path, no base address, absolute attribute`` () : unit =
do
use client = HttpClientMock.makeNoUri replyWithUrl
let api = ApiWithBasePathAndAbsoluteEndpoint.make client
let exc =
Assert.Throws<AggregateException> (fun () -> api.GetPathParam("hi").Result |> ignore<string>)
exc.InnerException.Message
|> shouldEqual
"No base address was supplied on the type, and no BaseAddress was on the HttpClient. (Parameter 'BaseAddress')"
use client = HttpClientMock.make (Uri "https://whatnot.com/thing/") replyWithUrl
let api = ApiWithBasePathAndAbsoluteEndpoint.make client
let result = api.GetPathParam("hi").Result
result |> shouldEqual "https://whatnot.com/endpoint/hi"
[<Test>]
let ``Relative base path, base address, absolute attribute`` () : unit =
use client = HttpClientMock.makeNoUri replyWithUrl
let api = ApiWithBasePathAndAddressAndAbsoluteEndpoint.make client
let result = api.GetPathParam("hi").Result
result |> shouldEqual "https://whatnot.com/endpoint/hi"
[<Test>]
let ``Absolute base path, no base address, absolute attribute`` () : unit =
do
use client = HttpClientMock.makeNoUri replyWithUrl
let api = ApiWithAbsoluteBasePathAndAbsoluteEndpoint.make client
let exc =
Assert.Throws<AggregateException> (fun () -> api.GetPathParam("hi").Result |> ignore<string>)
exc.InnerException.Message
|> shouldEqual
"No base address was supplied on the type, and no BaseAddress was on the HttpClient. (Parameter 'BaseAddress')"
use client = HttpClientMock.make (Uri "https://whatnot.com/thing/") replyWithUrl
let api = ApiWithAbsoluteBasePathAndAbsoluteEndpoint.make client
let result = api.GetPathParam("hi").Result
result |> shouldEqual "https://whatnot.com/endpoint/hi"
[<Test>]
let ``Absolute base path, base address, absolute attribute`` () : unit =
use client = HttpClientMock.makeNoUri replyWithUrl
let api = ApiWithAbsoluteBasePathAndAddressAndAbsoluteEndpoint.make client
let result = api.GetPathParam("hi").Result
result |> shouldEqual "https://whatnot.com/endpoint/hi"

View File

@@ -52,7 +52,13 @@ module TestVariableHeader =
api.GetPathParam("param").Result.Split "\n"
|> Array.sort
|> shouldEqual [| "Authorization: -99" ; "Header-Name: Header-Value" ; "X-Foo: 11" |]
|> shouldEqual
[|
"Authorization: -99"
"Header-Name: Header-Value"
"Something-Else: val"
"X-Foo: 11"
|]
someHeaderCount.Value |> shouldEqual 11
someOtherHeaderCount.Value |> shouldEqual -99
@@ -98,11 +104,23 @@ module TestVariableHeader =
api.GetPathParam("param").Result.Split "\n"
|> Array.sort
|> shouldEqual [| "Authorization: -99" ; "Header-Name: Header-Value" ; "X-Foo: 11" |]
|> shouldEqual
[|
"Authorization: -99"
"Header-Name: Header-Value"
"Something-Else: val"
"X-Foo: 11"
|]
api.GetPathParam("param").Result.Split "\n"
|> Array.sort
|> shouldEqual [| "Authorization: -98" ; "Header-Name: Header-Value" ; "X-Foo: 12" |]
|> shouldEqual
[|
"Authorization: -98"
"Header-Name: Header-Value"
"Something-Else: val"
"X-Foo: 12"
|]
someHeaderCount.Value |> shouldEqual 12
someOtherHeaderCount.Value |> shouldEqual -98

View File

@@ -117,6 +117,7 @@ module TestJsonSerde =
IntMeasureNullable = intMeasureNullable
Enum = enum<SomeEnum> someEnum
Timestamp = timestamp
Unit = ()
}
}
@@ -168,6 +169,7 @@ module TestJsonSerde =
IntMeasureNullable = Nullable -883<measure>
Enum = enum<SomeEnum> 1
Timestamp = DateTimeOffset (2024, 07, 01, 17, 54, 00, TimeSpan.FromHours 1.0)
Unit = ()
}
let expected =
@@ -198,7 +200,8 @@ module TestJsonSerde =
"intMeasureOption": 981,
"intMeasureNullable": -883,
"enum": 1,
"timestamp": "2024-07-01T17:54:00.0000000\u002B01:00"
"timestamp": "2024-07-01T17:54:00.0000000\u002B01:00",
"unit": {}
}
"""
|> fun s -> s.ToCharArray ()
@@ -306,3 +309,166 @@ module TestJsonSerde =
for i in counts do
i |> shouldBeGreaterThan 0
let dict<'a, 'b when 'a : equality> (xs : ('a * 'b) seq) : Dictionary<'a, 'b> =
let result = Dictionary ()
for k, v in xs do
result.Add (k, v)
result
let inline makeJsonArr< ^t, ^u when ^u : (static member op_Implicit : ^t -> JsonNode) and ^u :> JsonNode>
(arr : ^t seq)
: JsonNode
=
let result = JsonArray ()
for a in arr do
result.Add a
result :> JsonNode
let normalise (d : Dictionary<'a, 'b>) : ('a * 'b) list =
d |> Seq.map (fun (KeyValue (a, b)) -> a, b) |> Seq.toList |> List.sortBy fst
[<Test>]
let ``Can collect extension data`` () =
let str =
"""{
"message": { "header": "hi", "value": "bye" },
"something": 3,
"arr": ["egg", "toast"],
"str": "whatnot"
}"""
|> JsonNode.Parse
let expected =
{
Rest =
[
"something", JsonNode.op_Implicit 3
"arr", makeJsonArr [| "egg" ; "toast" |]
"str", JsonNode.op_Implicit "whatnot"
]
|> dict
Message =
Some
{
Header = "hi"
Value = "bye"
}
}
let actual = CollectRemaining.jsonParse str
actual.Message |> shouldEqual expected.Message
normalise actual.Rest
|> List.map (fun (k, v) -> k, v.ToJsonString ())
|> shouldEqual (normalise expected.Rest |> List.map (fun (k, v) -> k, v.ToJsonString ()))
[<Test>]
let ``Can write out extension data`` () =
let expected =
"""{"message":{"header":"hi","value":"bye"},"something":3,"arr":["egg","toast"],"str":"whatnot"}"""
let toWrite =
{
Rest =
[
"something", JsonNode.op_Implicit 3
"arr", makeJsonArr [| "egg" ; "toast" |]
"str", JsonNode.op_Implicit "whatnot"
]
|> dict
Message =
Some
{
Header = "hi"
Value = "bye"
}
}
let actual = CollectRemaining.toJsonNode toWrite |> fun s -> s.ToJsonString ()
actual |> shouldEqual expected
[<Test>]
let ``Can collect extension data, nested`` () =
let str =
"""{
"thing": 99,
"baz": -123,
"remaining": {
"message": { "header": "hi", "value": "bye" },
"something": 3,
"arr": ["egg", "toast"],
"str": "whatnot"
}
}"""
|> JsonNode.Parse
let expected : OuterCollectRemaining =
{
Remaining =
{
Message =
Some
{
Header = "hi"
Value = "bye"
}
Rest =
[
"something", JsonNode.op_Implicit 3
"arr", makeJsonArr [| "egg" ; "toast" |]
"str", JsonNode.op_Implicit "whatnot"
]
|> dict
}
Others = [ "thing", 99 ; "baz", -123 ] |> dict
}
let actual = OuterCollectRemaining.jsonParse str
normalise actual.Others |> shouldEqual (normalise expected.Others)
let actual = actual.Remaining
let expected = expected.Remaining
actual.Message |> shouldEqual expected.Message
normalise actual.Rest
|> List.map (fun (k, v) -> k, v.ToJsonString ())
|> shouldEqual (normalise expected.Rest |> List.map (fun (k, v) -> k, v.ToJsonString ()))
[<Test>]
let ``Can write out extension data, nested`` () =
let expected =
"""{"thing":99,"baz":-123,"remaining":{"message":{"header":"hi","value":"bye"},"something":3,"arr":["egg","toast"],"str":"whatnot"}}"""
let toWrite : OuterCollectRemaining =
{
Others = [ "thing", 99 ; "baz", -123 ] |> dict
Remaining =
{
Rest =
[
"something", JsonNode.op_Implicit 3
"arr", makeJsonArr [| "egg" ; "toast" |]
"str", JsonNode.op_Implicit "whatnot"
]
|> dict
Message =
Some
{
Header = "hi"
Value = "bye"
}
}
}
let actual = OuterCollectRemaining.toJsonNode toWrite |> fun s -> s.ToJsonString ()
actual |> shouldEqual expected

View File

@@ -0,0 +1,36 @@
namespace WoofWare.Myriad.Plugins.Test
open System
open SomeNamespace
open NUnit.Framework
open FsUnitTyped
[<TestFixture>]
module TestMockGeneratorNoAttr =
[<Test>]
let ``Example of use: IPublicType`` () =
let mock : IPublicTypeNoAttr =
{ PublicTypeNoAttrMock.Empty with
Mem1 = fun (s, count) -> List.replicate count s
}
:> _
let _ =
Assert.Throws<NotImplementedException> (fun () -> mock.Mem2 "hi" |> ignore<int>)
mock.Mem1 ("hi", 3) |> shouldEqual [ "hi" ; "hi" ; "hi" ]
[<Test>]
let ``Example of use: curried args`` () =
let mock : CurriedNoAttr<_> =
{ CurriedNoAttrMock.Empty () with
Mem1 = fun i c -> Array.replicate i c |> String
Mem2 = fun (i, s) c -> String.concat $"%c{c}" (List.replicate i s)
Mem3 = fun (i, s) c -> String.concat $"%c{c}" (List.replicate i s)
}
:> _
mock.Mem1 3 'a' |> shouldEqual "aaa"
mock.Mem2 (3, "hi") 'a' |> shouldEqual "hiahiahi"
mock.Mem3 (3, "hi") 'a' |> shouldEqual "hiahiahi"

View File

@@ -0,0 +1,84 @@
namespace WoofWare.Myriad.Plugins.Test
open System.Text.Json.Nodes
open NUnit.Framework
open FsUnitTyped
open WoofWare.Myriad.Plugins
[<TestFixture>]
module TestSwaggerParse =
[<Test>]
let ``Can parse parameters`` () : unit =
let s =
"""{
"tags": [
"organization"
],
"summary": "Check if a user is a member of an organization",
"operationId": "orgIsMember",
"parameters": [
{
"type": "string",
"description": "name of the organization",
"name": "org",
"in": "path",
"required": true
},
{
"type": "string",
"description": "username of the user",
"name": "username",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "user is a member"
},
"303": {
"description": "redirection to /orgs/{org}/public_members/{username}"
},
"404": {
"description": "user is not a member"
}
}
}
"""
|> JsonNode.Parse
s.AsObject ()
|> SwaggerEndpoint.Parse
|> shouldEqual
{
Consumes = None
Produces = None
Tags = [ "organization" ]
Summary = "Check if a user is a member of an organization"
OperationId = OperationId "orgIsMember"
Parameters =
[
{
Type = Definition.String
Description = Some "name of the organization"
Name = "org"
In = ParameterIn.Path "org"
Required = Some true
}
{
Type = Definition.String
Description = Some "username of the user"
Name = "username"
In = ParameterIn.Path "username"
Required = Some true
}
]
|> Some
Responses =
[
204, Definition.Unspecified
303, Definition.Unspecified
404, Definition.Unspecified
]
|> Map.ofList
}

View File

@@ -26,6 +26,7 @@
<Compile Include="TestHttpClient\TestVaultClient.fs" />
<Compile Include="TestHttpClient\TestVariableHeader.fs" />
<Compile Include="TestMockGenerator\TestMockGenerator.fs" />
<Compile Include="TestMockGenerator\TestMockGeneratorNoAttr.fs" />
<Compile Include="TestJsonSerialize\TestJsonSerde.fs" />
<Compile Include="TestCataGenerator\TestCataGenerator.fs" />
<Compile Include="TestCataGenerator\TestDirectory.fs" />
@@ -33,6 +34,7 @@
<Compile Include="TestCataGenerator\TestMyList.fs" />
<Compile Include="TestCataGenerator\TestMyList2.fs" />
<Compile Include="TestArgParser\TestArgParser.fs" />
<Compile Include="TestSwagger\TestSwaggerParse.fs" />
<Compile Include="TestRemoveOptions.fs"/>
<Compile Include="TestSurface.fs"/>
<None Include="../.github/workflows/dotnet.yaml" />
@@ -41,7 +43,7 @@
<ItemGroup>
<PackageReference Include="ApiSurface" Version="4.1.5"/>
<PackageReference Include="FsCheck" Version="2.16.6"/>
<PackageReference Include="FsUnit" Version="6.0.0"/>
<PackageReference Include="FsUnit" Version="6.0.1"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1"/>
<PackageReference Include="NUnit" Version="4.2.2"/>
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0"/>

View File

@@ -1019,12 +1019,12 @@ module internal ArgParserGenerator =
recurseKey
]
let notMatched =
let posAttr =
match leftoverArgAcc with
| ChoicePositional.Choice a
| ChoicePositional.Normal a -> a
let posAttr =
match leftoverArgAcc with
| ChoicePositional.Choice a
| ChoicePositional.Normal a -> a
let notMatched =
let handleFailure =
[
SynMatchClause.create (SynPat.named "None") fail
@@ -1113,6 +1113,8 @@ module internal ArgParserGenerator =
let processValue =
// During failure, we've received an optional exception message that happened when we tried to parse
// the value; it's in the variable `exc`.
// `fail` is for the case where we're genuinely emitting an error.
// If we're in `PositionalArgs true` mode, though, we won't call `fail`.
let fail =
[
SynExpr.createIdent "failwithf"
@@ -1132,6 +1134,27 @@ module internal ArgParserGenerator =
]
|> SynExpr.createMatch (SynExpr.createIdent "exc")
let onFailure =
match posAttr with
| None -> fail
| Some includeFlagLike ->
[
SynExpr.createIdent "key"
|> SynExpr.pipeThroughFunction leftoverArgParser
|> fun i ->
match leftoverArgAcc with
| ChoicePositional.Choice _ ->
i |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Choice1Of2")
| ChoicePositional.Normal _ -> i
|> SynExpr.pipeThroughFunction (SynExpr.createLongIdent' [ leftoverArgs ; Ident.create "Add" ])
SynExpr.createIdent "go"
|> SynExpr.applyTo (SynExpr.createLongIdent' [ parseState ; Ident.create "AwaitingKey" ])
|> SynExpr.applyTo (SynExpr.listCons (SynExpr.createIdent "arg") (SynExpr.createIdent "args"))
]
|> SynExpr.sequential
|> SynExpr.ifThenElse includeFlagLike fail
[
SynMatchClause.create
(SynPat.nameWithArgs "Ok" [ SynPat.unit ])
@@ -1144,7 +1167,7 @@ module internal ArgParserGenerator =
(SynPat.nameWithArgs "Error" [ SynPat.named "exc" ])
(SynExpr.ifThenElse
(SynExpr.applyFunction (SynExpr.createIdent "setFlagValue") (SynExpr.createIdent "key"))
fail
onFailure
(SynExpr.createIdent "go"
|> SynExpr.applyTo (SynExpr.createLongIdent' [ parseState ; Ident.create "AwaitingKey" ])
|> SynExpr.applyTo (SynExpr.listCons (SynExpr.createIdent "arg") (SynExpr.createIdent "args"))))

View File

@@ -564,11 +564,12 @@ module internal CataGenerator =
let domain =
field.FieldName
|> Option.map Ident.lowerFirstLetter
|> SynType.signatureParamOfType place
|> SynType.signatureParamOfType [] place false
acc |> SynType.funFromDomain domain
)
|> SynMemberDefn.abstractMember
[]
case.CataMethodIdent
None
arity

View File

@@ -1,5 +1,6 @@
namespace WoofWare.Myriad.Plugins
open System.IO
open System.Net.Http
open Fantomas.FCS.Syntax
@@ -12,6 +13,17 @@ type internal HttpClientGeneratorOutputSpec =
module internal HttpClientGenerator =
open Fantomas.FCS.Text.Range
let outputFile = FileInfo "/tmp/output.txt"
// do
// use _ = File.Create outputFile.FullName
// ()
let log (line : string) =
// use w = outputFile.AppendText ()
// w.WriteLine line
()
[<RequireQualifiedAccess>]
type PathSpec =
| Verbatim of string
@@ -60,6 +72,9 @@ module internal HttpClientGenerator =
BaseAddress : SynExpr option
BasePath : SynExpr option
Accessibility : SynAccess option
/// Headers which apply *only* to this endpoint.
/// For example, SynConst "Authorization" and SynConst "token BLAH".
Headers : (SynExpr * SynExpr) list
}
let httpMethodString (m : HttpMethod) : string =
@@ -321,15 +336,33 @@ module internal HttpClientGenerator =
|> SynExpr.createMatch baseAddress
|> SynExpr.paren
let baseAddress =
match info.BasePath with
| None -> baseAddress
| Some basePath ->
[
yield baseAddress
yield
SynExpr.applyFunction
uriIdent
(SynExpr.tuple
[ basePath ; SynExpr.createLongIdent [ "System" ; "UriKind" ; "Relative" ] ])
]
|> SynExpr.tuple
|> SynExpr.applyFunction uriIdent
[
baseAddress
SynExpr.applyFunction
uriIdent
(SynExpr.tuple
[
requestUriTrailer
SynExpr.createLongIdent [ "System" ; "UriKind" ; "Relative" ]
])
yield baseAddress
yield
SynExpr.applyFunction
uriIdent
(SynExpr.tuple
[
requestUriTrailer
SynExpr.createLongIdent [ "System" ; "UriKind" ; "Relative" ]
])
]
|> SynExpr.tuple
|> SynExpr.applyFunction uriIdent
@@ -404,14 +437,54 @@ module internal HttpClientGenerator =
retType
(SynExpr.createIdent "jsonNode")
let contentTypeHeader, memberHeaders =
info.Headers
|> List.partition (fun (headerName, headerValue) ->
match headerName |> SynExpr.stripOptionalParen with
| SynExpr.Const (SynConst.String ("Content-Type", _, _), _) -> true
| _ -> false
)
let contentTypeHeader =
match contentTypeHeader with
| [] -> None
| [ _, ct ] -> Some (SynExpr.stripOptionalParen ct)
| _ -> failwith "Unexpectedly got multiple Content-Type headers"
let createStringContent (contents : SynExpr) =
SynExpr.createNew
(SynType.createLongIdent' [ "System" ; "Net" ; "Http" ; "StringContent" ])
(SynExpr.tupleNoParen
[
yield contents
match contentTypeHeader with
| None -> ()
| Some ch ->
yield SynExpr.createNull ()
// Sigh, Gitea in particular passes "json" here
match ch with
| SynExpr.Const (SynConst.String ("json", _, _), _) ->
yield SynExpr.CreateConst "application/json"
| SynExpr.Const (SynConst.String ("html", _, _), _) -> yield SynExpr.CreateConst "text/html"
| _ -> yield ch
])
let handleBodyParams =
match bodyParam with
| None -> []
| Some (bodyParamType, bodyParamName) ->
match bodyParamType with
| BodyParamMethods.StreamContent
| BodyParamMethods.ByteArrayContent
| BodyParamMethods.StringContent ->
[
Let ("queryParams", createStringContent (SynExpr.createIdent' bodyParamName))
Do (
SynExpr.assign
(SynLongIdent.createS' [ "httpMessage" ; "Content" ])
(SynExpr.createIdent "queryParams")
)
]
| BodyParamMethods.StreamContent
| BodyParamMethods.ByteArrayContent ->
[
Let (
"queryParams",
@@ -438,22 +511,22 @@ module internal HttpClientGenerator =
[
Let (
"queryParams",
SynExpr.createNew
(SynType.createLongIdent' [ "System" ; "Net" ; "Http" ; "StringContent" ])
(SynExpr.createIdent' bodyParamName
|> SynExpr.pipeThroughFunction (fst (JsonSerializeGenerator.serializeNode ty))
|> SynExpr.pipeThroughFunction (
SynExpr.createLambda
"node"
(SynExpr.ifThenElse
(SynExpr.applyFunction
(SynExpr.createIdent "isNull")
(SynExpr.createIdent "node"))
(SynExpr.applyFunction
(SynExpr.createLongIdent [ "node" ; "ToJsonString" ])
(SynExpr.CreateConst ()))
(SynExpr.CreateConst "null"))
))
createStringContent (
SynExpr.createIdent' bodyParamName
|> SynExpr.pipeThroughFunction (fst (JsonSerializeGenerator.serializeNode ty))
|> SynExpr.pipeThroughFunction (
SynExpr.createLambda
"node"
(SynExpr.ifThenElse
(SynExpr.applyFunction
(SynExpr.createIdent "isNull")
(SynExpr.createIdent "node"))
(SynExpr.applyFunction
(SynExpr.createLongIdent [ "node" ; "ToJsonString" ])
(SynExpr.CreateConst ()))
(SynExpr.CreateConst "null"))
)
)
)
Do (
SynExpr.assign
@@ -522,6 +595,16 @@ module internal HttpClientGenerator =
|> Do
)
let setMemberHeaders =
memberHeaders
|> List.map (fun (headerName, headerValue) ->
// Best-effort: assume this is a message header.
SynExpr.applyFunction
(SynExpr.createLongIdent [ "httpMessage" ; "Headers" ; "Add" ])
(SynExpr.tuple [ headerName ; headerValue ])
|> Do
)
[
yield LetBang ("ct", SynExpr.createLongIdent [ "Async" ; "CancellationToken" ])
yield Let ("uri", requestUri)
@@ -537,6 +620,7 @@ module internal HttpClientGenerator =
yield! setVariableHeaders
yield! setConstantHeaders
yield! setMemberHeaders
yield
LetBang (
@@ -563,6 +647,9 @@ module internal HttpClientGenerator =
yield jsonNode
| String -> yield responseString
| Stream -> yield responseStream
| Unit ->
// What we're returning doesn't depend on the content, so don't bother!
()
| _ ->
yield responseStream
yield jsonNode
@@ -647,6 +734,15 @@ module internal HttpClientGenerator =
| _ -> None
)
let insertTrailingSlash (path : SynExpr) : SynExpr =
match path |> SynExpr.stripOptionalParen with
| SynExpr.Const (SynConst.String (s, _, _), _) ->
if s.EndsWith '/' then
path
else
SynExpr.CreateConst (s + "/")
| _ -> SynExpr.plus (SynExpr.paren path) (SynExpr.CreateConst "/")
let createModule
(opens : SynOpenDeclTarget list)
(ns : LongIdent)
@@ -676,8 +772,17 @@ module internal HttpClientGenerator =
"Expected constant header parameters to be of the form [<Header (key, value)>], but got more than two args"
)
let baseAddress = extractBaseAddress interfaceType.Attributes
let basePath = extractBasePath interfaceType.Attributes
let baseAddress =
extractBaseAddress interfaceType.Attributes
// We artificially insert a trailing slash because this is almost certainly
// not meant to be an endpoint itself.
|> Option.map insertTrailingSlash
let basePath =
extractBasePath interfaceType.Attributes
// We artificially insert a trailing slash because this is almost certainly
// not meant to be an endpoint itself.
|> Option.map insertTrailingSlash
let properties =
interfaceType.Properties
@@ -705,6 +810,16 @@ module internal HttpClientGenerator =
|> List.map (fun mem ->
let httpMethod, url = extractHttpInformation mem.Attributes
let specificHeaders =
extractHeaderInformation mem.Attributes
|> List.map (fun l ->
match l with
| [ x ; y ] -> x, y
| _ ->
failwith
$"Expected Header attribute on member %s{mem.Identifier.idText} to have exactly two arguments."
)
let shouldEnsureSuccess = not (shouldAllowAnyStatusCode mem.Attributes)
let returnType =
@@ -745,6 +860,7 @@ module internal HttpClientGenerator =
BaseAddress = baseAddress
BasePath = basePath
Accessibility = mem.Accessibility
Headers = specificHeaders
}
)
|> List.map (constructMember constantHeaders properties)
@@ -876,6 +992,10 @@ type HttpClientGenerator () =
member _.ValidInputExtensions = [ ".fs" ]
member _.Generate (context : GeneratorContext) =
let targetedTypes =
MyriadParamParser.render context.AdditionalParameters
|> Map.map (fun _ v -> v.Split '!' |> Array.toList |> List.map DesiredGenerator.Parse)
let ast, _ =
Ast.fromFilename context.InputFilename |> Async.RunSynchronously |> Array.head
@@ -889,12 +1009,32 @@ type HttpClientGenerator () =
types
|> List.choose (fun typeDef ->
match Ast.getAttribute<HttpClientAttribute> typeDef with
| None -> None
| None ->
let name = SynTypeDefn.getName typeDef |> List.map _.idText |> String.concat "."
match Map.tryFind name targetedTypes with
| Some desired ->
desired
|> List.tryPick (fun generator ->
match generator with
| DesiredGenerator.HttpClient arg ->
let spec =
{
ExtensionMethods =
arg
|> Option.defaultValue
HttpClientAttribute.DefaultIsExtensionMethod
}
Some (typeDef, spec)
| _ -> None
)
| _ -> None
| Some attr ->
let arg =
match SynExpr.stripOptionalParen attr.ArgExpr with
| SynExpr.Const (SynConst.Bool value, _) -> value
| SynExpr.Const (SynConst.Unit, _) -> JsonParseAttribute.DefaultIsExtensionMethod
| SynExpr.Const (SynConst.Unit, _) -> HttpClientAttribute.DefaultIsExtensionMethod
| arg ->
failwith
$"Unrecognised argument %+A{arg} to [<%s{nameof HttpClientAttribute}>]. Literals are not supported. Use `true` or `false` (or unit) only."

View File

@@ -228,14 +228,11 @@ module internal InterfaceMockGenerator =
x.Type
let private constructMemberSinglePlace (tuple : TupledArg) : SynType =
match tuple.Args |> List.rev |> List.map buildType with
| [] -> failwith "no-arg functions not supported yet"
| [ x ] -> x
| last :: rest ->
([ SynTupleTypeSegment.Type last ], rest)
||> List.fold (fun ty nextArg -> SynTupleTypeSegment.Type nextArg :: SynTupleTypeSegment.Star range0 :: ty)
|> fun segs -> SynType.Tuple (false, segs, range0)
|> fun ty -> if tuple.HasParen then SynType.Paren (ty, range0) else ty
tuple.Args
|> List.map buildType
|> SynType.tupleNoParen
|> Option.defaultWith (fun () -> failwith "no-arg functions not supported yet")
|> if tuple.HasParen then SynType.paren else id
let constructMember (mem : MemberInfo) : SynField =
let inputType = mem.Args |> List.map constructMemberSinglePlace
@@ -286,6 +283,10 @@ type InterfaceMockGenerator () =
member _.ValidInputExtensions = [ ".fs" ]
member _.Generate (context : GeneratorContext) =
let targetedTypes =
MyriadParamParser.render context.AdditionalParameters
|> Map.map (fun _ v -> v.Split '!' |> Array.toList |> List.map DesiredGenerator.Parse)
let ast, _ =
Ast.fromFilename context.InputFilename |> Async.RunSynchronously |> Array.head
@@ -297,7 +298,27 @@ type InterfaceMockGenerator () =
types
|> List.choose (fun typeDef ->
match Ast.getAttribute<GenerateMockAttribute> typeDef with
| None -> None
| None ->
let name = SynTypeDefn.getName typeDef |> List.map _.idText |> String.concat "."
match Map.tryFind name targetedTypes with
| Some desired ->
desired
|> List.tryPick (fun generator ->
match generator with
| DesiredGenerator.InterfaceMock arg ->
let spec =
{
IsInternal =
arg
|> Option.defaultValue GenerateMockAttribute.DefaultIsInternal
}
Some (typeDef, spec)
| _ -> None
)
| _ -> None
| Some attr ->
let arg =
match SynExpr.stripOptionalParen attr.ArgExpr with

View File

@@ -59,7 +59,7 @@ module internal JsonParseGenerator =
| None -> node
| Some propertyName -> assertNotNull propertyName node
|> SynExpr.callMethod "AsValue"
|> SynExpr.callGenericMethod "GetValue" typeName
|> SynExpr.callGenericMethod (SynLongIdent.createS "GetValue") [ SynType.createLongIdent typeName ]
/// {node}.AsObject()
/// If `propertyName` is Some, uses `assertNotNull {node}` instead of `{node}`.
@@ -279,6 +279,8 @@ module internal JsonParseGenerator =
| Measure (_measure, primType) ->
parseNumberType options propertyName node primType
|> SynExpr.pipeThroughFunction (Measure.getLanguagePrimitivesMeasure primType)
| JsonNode -> node
| Unit -> SynExpr.CreateConst ()
| _ ->
// Let's just hope that we've also got our own type annotation!
let typeName =
@@ -375,9 +377,9 @@ module internal JsonParseGenerator =
)
let createRecordMaker (spec : JsonParseOutputSpec) (fields : SynFieldData<Ident> list) =
let assignments =
let propertyFields =
fields
|> List.mapi (fun i fieldData ->
|> List.map (fun fieldData ->
let propertyNameAttr =
fieldData.Attrs
|> List.tryFind (fun attr ->
@@ -385,7 +387,12 @@ module internal JsonParseGenerator =
.EndsWith ("JsonPropertyName", StringComparison.Ordinal)
)
let options = getParseOptions fieldData.Attrs
let extensionDataAttr =
fieldData.Attrs
|> List.tryFind (fun attr ->
(SynLongIdent.toString attr.TypeName)
.EndsWith ("JsonExtensionData", StringComparison.Ordinal)
)
let propertyName =
match propertyNameAttr with
@@ -401,8 +408,77 @@ module internal JsonParseGenerator =
sb.ToString () |> SynExpr.CreateConst
| Some name -> name.ArgExpr
propertyName, extensionDataAttr
)
let namedPropertyFields =
propertyFields
|> List.choose (fun (name, extension) ->
match extension with
| Some _ -> None
| None -> Some name
)
let isNamedPropertyField =
match namedPropertyFields with
| [] -> SynExpr.CreateConst false
| _ ->
namedPropertyFields
|> List.map (fun fieldName -> SynExpr.equals (SynExpr.createIdent "key") fieldName)
|> List.reduce SynExpr.booleanOr
let assignments =
List.zip fields propertyFields
|> List.mapi (fun i (fieldData, (propertyName, extensionDataAttr)) ->
let options = getParseOptions fieldData.Attrs
let accIdent = Ident.create $"arg_%i{i}"
match extensionDataAttr with
| Some _ ->
// Can't go through the usual parse logic here, because that will try and identify the node that's
// been labelled. The whole point of JsonExtensionData is that there is no such node!
let valType =
match fieldData.Type with
| DictionaryType (String, v) -> v
| _ -> failwith "Expected JsonExtensionData to be Dictionary<string, _>"
SynExpr.ifThenElse
isNamedPropertyField
(SynExpr.callMethodArg
"Add"
(SynExpr.tuple
[
SynExpr.createIdent "key"
createParseRhs options (SynExpr.createIdent "key") valType
])
(SynExpr.createIdent "result"))
(SynExpr.CreateConst ())
|> SynExpr.createForEach
(SynPat.nameWithArgs "KeyValue" [ SynPat.named "key" ; SynPat.named "value" ])
(SynExpr.createIdent "node")
|> fun forEach -> [ forEach ; SynExpr.createIdent "result" ]
|> SynExpr.sequential
|> SynExpr.createLet
[
SynBinding.basic
[ Ident.create "result" ]
[]
(SynExpr.typeApp
[ SynType.string ; valType ]
(SynExpr.createLongIdent [ "System" ; "Collections" ; "Generic" ; "Dictionary" ])
|> SynExpr.applyTo (SynExpr.CreateConst ()))
SynBinding.basic
[ Ident.create "node" ]
[]
(SynExpr.createIdent "node" |> SynExpr.callMethod "AsObject")
]
|> SynBinding.basic [ accIdent ] []
| None ->
createParseRhs options propertyName fieldData.Type
|> SynBinding.basic [ Ident.create $"arg_%i{i}" ] []
|> SynBinding.basic [ accIdent ] []
)
let finalConstruction =
@@ -483,9 +559,7 @@ module internal JsonParseGenerator =
|> SynExpr.index property
|> assertNotNull property
|> SynExpr.pipeThroughFunction (
SynExpr.createLambda
"v"
(SynExpr.callGenericMethod "GetValue" [ Ident.create "string" ] (SynExpr.createIdent "v"))
SynExpr.createLambda "v" (SynExpr.callGenericMethod' "GetValue" "string" (SynExpr.createIdent "v"))
)
|> SynBinding.basic [ Ident.create "ty" ] []
]
@@ -628,6 +702,10 @@ type JsonParseGenerator () =
member _.ValidInputExtensions = [ ".fs" ]
member _.Generate (context : GeneratorContext) =
let targetedTypes =
MyriadParamParser.render context.AdditionalParameters
|> Map.map (fun _ v -> v.Split '!' |> Array.toList |> List.map DesiredGenerator.Parse)
let ast, _ =
Ast.fromFilename context.InputFilename |> Async.RunSynchronously |> Array.head
@@ -650,7 +728,28 @@ type JsonParseGenerator () =
types
|> List.choose (fun typeDef ->
match Ast.getAttribute<JsonParseAttribute> typeDef with
| None -> None
| None ->
let name = SynTypeDefn.getName typeDef |> List.map _.idText |> String.concat "."
match Map.tryFind name targetedTypes with
| Some desired ->
desired
|> List.tryPick (fun generator ->
match generator with
| DesiredGenerator.JsonParse arg ->
let spec =
{
ExtensionMethods =
arg
|> Option.defaultValue
JsonParseAttribute.DefaultIsExtensionMethod
}
Some (typeDef, spec)
| _ -> None
)
| _ -> None
| Some attr ->
let arg =
match SynExpr.stripOptionalParen attr.ArgExpr with

View File

@@ -146,6 +146,13 @@ module internal JsonSerializeGenerator =
]
|> SynExpr.createLambda "field"
|> fun e -> e, false
| JsonNode -> SynExpr.createIdent "id", true
| Unit ->
SynExpr.createLambda
"value"
(SynExpr.createLongIdent [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonObject" ]
|> SynExpr.applyTo (SynExpr.CreateConst ())),
false
| _ ->
// {type}.toJsonNode
let typeName =
@@ -187,6 +194,14 @@ module internal JsonSerializeGenerator =
sb.ToString () |> SynExpr.CreateConst
| Some name -> name.ArgExpr
let getIsJsonExtension (attrs : SynAttribute list) : bool =
attrs
|> List.tryFind (fun attr ->
(SynLongIdent.toString attr.TypeName)
.EndsWith ("JsonExtensionData", StringComparison.Ordinal)
)
|> Option.isSome
/// `populateNode` will be inserted before we return the `node` variable.
///
/// That is, we give you access to a `JsonObject` called `node`,
@@ -256,7 +271,31 @@ module internal JsonSerializeGenerator =
fields
|> List.map (fun fieldData ->
let propertyName = getPropertyName fieldData.Ident fieldData.Attrs
createSerializeRhsRecord propertyName fieldData.Ident fieldData.Type
let isJsonExtension = getIsJsonExtension fieldData.Attrs
if isJsonExtension then
let valType =
match fieldData.Type with
| DictionaryType (String, v) -> v
| _ -> failwith "Expected JsonExtensionData to be a Dictionary<string, something>"
let serialise = fst (serializeNode valType)
SynExpr.createIdent "node"
|> SynExpr.callMethodArg
"Add"
(SynExpr.tuple
[
SynExpr.createIdent "key"
SynExpr.applyFunction serialise (SynExpr.createIdent "value")
])
|> SynExpr.createForEach
(SynPat.identWithArgs
[ Ident.create "KeyValue" ]
(SynArgPats.create [ SynPat.named "key" ; SynPat.named "value" ]))
(SynExpr.createLongIdent' [ Ident.create "input" ; fieldData.Ident ])
else
createSerializeRhsRecord propertyName fieldData.Ident fieldData.Type
)
|> SynExpr.sequential
|> fun expr -> SynExpr.Do (expr, range0)
@@ -480,6 +519,10 @@ type JsonSerializeGenerator () =
member _.ValidInputExtensions = [ ".fs" ]
member _.Generate (context : GeneratorContext) =
let targetedTypes =
MyriadParamParser.render context.AdditionalParameters
|> Map.map (fun _ v -> v.Split '!' |> Array.toList |> List.map DesiredGenerator.Parse)
let ast, _ =
Ast.fromFilename context.InputFilename |> Async.RunSynchronously |> Array.head
@@ -502,7 +545,28 @@ type JsonSerializeGenerator () =
types
|> List.choose (fun typeDef ->
match Ast.getAttribute<JsonSerializeAttribute> typeDef with
| None -> None
| None ->
let name = SynTypeDefn.getName typeDef |> List.map _.idText |> String.concat "."
match Map.tryFind name targetedTypes with
| Some desired ->
desired
|> List.tryPick (fun generator ->
match generator with
| DesiredGenerator.JsonSerialize arg ->
let spec =
{
ExtensionMethods =
arg
|> Option.defaultValue
JsonSerializeAttribute.DefaultIsExtensionMethod
}
Some (typeDef, spec)
| _ -> None
)
| _ -> None
| Some attr ->
let arg =
match SynExpr.stripOptionalParen attr.ArgExpr with

View File

@@ -12,3 +12,12 @@ module private List =
)
List.rev xs, List.rev ys
let allSome<'a> (l : 'a option list) : 'a list option =
let rec go acc (l : 'a option list) =
match l with
| [] -> Some (List.rev acc)
| None :: _ -> None
| Some head :: tail -> go (head :: acc) tail
go [] l

View File

@@ -0,0 +1,64 @@
namespace WoofWare.Myriad.Plugins
open System.Collections.Generic
type internal DesiredGenerator =
| InterfaceMock of isInternal : bool option
| JsonParse of extensionMethod : bool option
| JsonSerialize of extensionMethod : bool option
| HttpClient of extensionMethod : bool option
static member Parse (s : string) =
match s with
| "GenerateMock" -> DesiredGenerator.InterfaceMock None
| "GenerateMock(true)" -> DesiredGenerator.InterfaceMock (Some true)
| "GenerateMock(false)" -> DesiredGenerator.InterfaceMock (Some false)
| "JsonParse" -> DesiredGenerator.JsonParse None
| "JsonParse(true)" -> DesiredGenerator.JsonParse (Some true)
| "JsonParse(false)" -> DesiredGenerator.JsonParse (Some false)
| "JsonSerialize" -> DesiredGenerator.JsonSerialize None
| "JsonSerialize(true)" -> DesiredGenerator.JsonSerialize (Some true)
| "JsonSerialize(false)" -> DesiredGenerator.JsonSerialize (Some false)
| "HttpClient" -> DesiredGenerator.HttpClient None
| "HttpClient(true)" -> DesiredGenerator.HttpClient (Some true)
| "HttpClient(false)" -> DesiredGenerator.HttpClient (Some false)
| _ -> failwith $"Failed to parse as a generator specification: %s{s}"
[<RequireQualifiedAccess>]
module internal MyriadParamParser =
(*
An apparent bug in Myriad's argument parsing means that this:
<MyriadParams>
<Foo>bar</Foo>
<Baz>quux</Baz>
</MyriadParams>
leads to this:
Foo = "bar;Baz=quux"
I'm not going to put effort into fixing Myriad, though, because I want
to build something much more powerful instead.
*)
/// Call this with `context.AdditionalParameters`.
let render (pars : IDictionary<string, string>) : Map<string, string> =
match pars.Count with
| 0 -> Map.empty
| 1 ->
let (KeyValue (key, value)) = pars |> Seq.exactlyOne
match value.Split ';' |> Seq.toList with
| [] -> failwith "LOGIC ERROR"
| value :: rest ->
rest
|> Seq.map (fun v ->
let split = v.Split '='
split.[0], String.concat "=" split.[1..]
)
|> Seq.append (Seq.singleton (key, value))
|> Map.ofSeq
| _ ->
// assume the Myriad bug is fixed!
pars |> Seq.map (fun (KeyValue (k, v)) -> k, v) |> Map.ofSeq

View File

@@ -1,17 +1,298 @@
WoofWare.Myriad.Plugins.AdditionalProperties inherit obj, implements WoofWare.Myriad.Plugins.AdditionalProperties System.IEquatable, System.Collections.IStructuralEquatable - union type with 2 cases
WoofWare.Myriad.Plugins.AdditionalProperties+Constrained inherit WoofWare.Myriad.Plugins.AdditionalProperties
WoofWare.Myriad.Plugins.AdditionalProperties+Constrained.get_Item [method]: unit -> WoofWare.Myriad.Plugins.Definition
WoofWare.Myriad.Plugins.AdditionalProperties+Constrained.Item [property]: [read-only] WoofWare.Myriad.Plugins.Definition
WoofWare.Myriad.Plugins.AdditionalProperties+Tags inherit obj
WoofWare.Myriad.Plugins.AdditionalProperties+Tags.Constrained [static field]: int = 1
WoofWare.Myriad.Plugins.AdditionalProperties+Tags.Never [static field]: int = 0
WoofWare.Myriad.Plugins.AdditionalProperties.Equals [method]: (WoofWare.Myriad.Plugins.AdditionalProperties, System.Collections.IEqualityComparer) -> bool
WoofWare.Myriad.Plugins.AdditionalProperties.get_IsConstrained [method]: unit -> bool
WoofWare.Myriad.Plugins.AdditionalProperties.get_IsNever [method]: unit -> bool
WoofWare.Myriad.Plugins.AdditionalProperties.get_Never [static method]: unit -> WoofWare.Myriad.Plugins.AdditionalProperties
WoofWare.Myriad.Plugins.AdditionalProperties.get_Tag [method]: unit -> int
WoofWare.Myriad.Plugins.AdditionalProperties.IsConstrained [property]: [read-only] bool
WoofWare.Myriad.Plugins.AdditionalProperties.IsNever [property]: [read-only] bool
WoofWare.Myriad.Plugins.AdditionalProperties.Never [static property]: [read-only] WoofWare.Myriad.Plugins.AdditionalProperties
WoofWare.Myriad.Plugins.AdditionalProperties.NewConstrained [static method]: WoofWare.Myriad.Plugins.Definition -> WoofWare.Myriad.Plugins.AdditionalProperties
WoofWare.Myriad.Plugins.AdditionalProperties.Tag [property]: [read-only] int
WoofWare.Myriad.Plugins.ArgParserGenerator inherit obj, implements Myriad.Core.IMyriadGenerator
WoofWare.Myriad.Plugins.ArgParserGenerator..ctor [constructor]: unit
WoofWare.Myriad.Plugins.ArrayTypeDefinition inherit obj, implements WoofWare.Myriad.Plugins.ArrayTypeDefinition System.IEquatable, System.Collections.IStructuralEquatable
WoofWare.Myriad.Plugins.ArrayTypeDefinition..ctor [constructor]: WoofWare.Myriad.Plugins.Definition
WoofWare.Myriad.Plugins.ArrayTypeDefinition.Equals [method]: (WoofWare.Myriad.Plugins.ArrayTypeDefinition, System.Collections.IEqualityComparer) -> bool
WoofWare.Myriad.Plugins.ArrayTypeDefinition.get_Items [method]: unit -> WoofWare.Myriad.Plugins.Definition
WoofWare.Myriad.Plugins.ArrayTypeDefinition.Items [property]: [read-only] WoofWare.Myriad.Plugins.Definition
WoofWare.Myriad.Plugins.ArrayTypeDefinition.Parse [static method]: System.Text.Json.Nodes.JsonNode -> WoofWare.Myriad.Plugins.ArrayTypeDefinition
WoofWare.Myriad.Plugins.CreateCatamorphismGenerator inherit obj, implements Myriad.Core.IMyriadGenerator
WoofWare.Myriad.Plugins.CreateCatamorphismGenerator..ctor [constructor]: unit
WoofWare.Myriad.Plugins.Definition inherit obj, implements WoofWare.Myriad.Plugins.Definition System.IEquatable, System.Collections.IStructuralEquatable - union type with 8 cases
WoofWare.Myriad.Plugins.Definition+Array inherit WoofWare.Myriad.Plugins.Definition
WoofWare.Myriad.Plugins.Definition+Array.get_Item [method]: unit -> WoofWare.Myriad.Plugins.ArrayTypeDefinition
WoofWare.Myriad.Plugins.Definition+Array.Item [property]: [read-only] WoofWare.Myriad.Plugins.ArrayTypeDefinition
WoofWare.Myriad.Plugins.Definition+Handle inherit WoofWare.Myriad.Plugins.Definition
WoofWare.Myriad.Plugins.Definition+Handle.get_Item [method]: unit -> string
WoofWare.Myriad.Plugins.Definition+Handle.Item [property]: [read-only] string
WoofWare.Myriad.Plugins.Definition+Integer inherit WoofWare.Myriad.Plugins.Definition
WoofWare.Myriad.Plugins.Definition+Integer.format [property]: [read-only] string option
WoofWare.Myriad.Plugins.Definition+Integer.get_format [method]: unit -> string option
WoofWare.Myriad.Plugins.Definition+Object inherit WoofWare.Myriad.Plugins.Definition
WoofWare.Myriad.Plugins.Definition+Object.get_Item [method]: unit -> WoofWare.Myriad.Plugins.ObjectTypeDefinition
WoofWare.Myriad.Plugins.Definition+Object.Item [property]: [read-only] WoofWare.Myriad.Plugins.ObjectTypeDefinition
WoofWare.Myriad.Plugins.Definition+Tags inherit obj
WoofWare.Myriad.Plugins.Definition+Tags.Array [static field]: int = 2
WoofWare.Myriad.Plugins.Definition+Tags.Boolean [static field]: int = 4
WoofWare.Myriad.Plugins.Definition+Tags.File [static field]: int = 7
WoofWare.Myriad.Plugins.Definition+Tags.Handle [static field]: int = 0
WoofWare.Myriad.Plugins.Definition+Tags.Integer [static field]: int = 6
WoofWare.Myriad.Plugins.Definition+Tags.Object [static field]: int = 1
WoofWare.Myriad.Plugins.Definition+Tags.String [static field]: int = 3
WoofWare.Myriad.Plugins.Definition+Tags.Unspecified [static field]: int = 5
WoofWare.Myriad.Plugins.Definition.Boolean [static property]: [read-only] WoofWare.Myriad.Plugins.Definition
WoofWare.Myriad.Plugins.Definition.Equals [method]: (WoofWare.Myriad.Plugins.Definition, System.Collections.IEqualityComparer) -> bool
WoofWare.Myriad.Plugins.Definition.File [static property]: [read-only] WoofWare.Myriad.Plugins.Definition
WoofWare.Myriad.Plugins.Definition.get_Boolean [static method]: unit -> WoofWare.Myriad.Plugins.Definition
WoofWare.Myriad.Plugins.Definition.get_File [static method]: unit -> WoofWare.Myriad.Plugins.Definition
WoofWare.Myriad.Plugins.Definition.get_IsArray [method]: unit -> bool
WoofWare.Myriad.Plugins.Definition.get_IsBoolean [method]: unit -> bool
WoofWare.Myriad.Plugins.Definition.get_IsFile [method]: unit -> bool
WoofWare.Myriad.Plugins.Definition.get_IsHandle [method]: unit -> bool
WoofWare.Myriad.Plugins.Definition.get_IsInteger [method]: unit -> bool
WoofWare.Myriad.Plugins.Definition.get_IsObject [method]: unit -> bool
WoofWare.Myriad.Plugins.Definition.get_IsString [method]: unit -> bool
WoofWare.Myriad.Plugins.Definition.get_IsUnspecified [method]: unit -> bool
WoofWare.Myriad.Plugins.Definition.get_String [static method]: unit -> WoofWare.Myriad.Plugins.Definition
WoofWare.Myriad.Plugins.Definition.get_Tag [method]: unit -> int
WoofWare.Myriad.Plugins.Definition.get_Unspecified [static method]: unit -> WoofWare.Myriad.Plugins.Definition
WoofWare.Myriad.Plugins.Definition.IsArray [property]: [read-only] bool
WoofWare.Myriad.Plugins.Definition.IsBoolean [property]: [read-only] bool
WoofWare.Myriad.Plugins.Definition.IsFile [property]: [read-only] bool
WoofWare.Myriad.Plugins.Definition.IsHandle [property]: [read-only] bool
WoofWare.Myriad.Plugins.Definition.IsInteger [property]: [read-only] bool
WoofWare.Myriad.Plugins.Definition.IsObject [property]: [read-only] bool
WoofWare.Myriad.Plugins.Definition.IsString [property]: [read-only] bool
WoofWare.Myriad.Plugins.Definition.IsUnspecified [property]: [read-only] bool
WoofWare.Myriad.Plugins.Definition.NewArray [static method]: WoofWare.Myriad.Plugins.ArrayTypeDefinition -> WoofWare.Myriad.Plugins.Definition
WoofWare.Myriad.Plugins.Definition.NewHandle [static method]: string -> WoofWare.Myriad.Plugins.Definition
WoofWare.Myriad.Plugins.Definition.NewInteger [static method]: string option -> WoofWare.Myriad.Plugins.Definition
WoofWare.Myriad.Plugins.Definition.NewObject [static method]: WoofWare.Myriad.Plugins.ObjectTypeDefinition -> WoofWare.Myriad.Plugins.Definition
WoofWare.Myriad.Plugins.Definition.Parse [static method]: System.Text.Json.Nodes.JsonObject -> WoofWare.Myriad.Plugins.Definition
WoofWare.Myriad.Plugins.Definition.String [static property]: [read-only] WoofWare.Myriad.Plugins.Definition
WoofWare.Myriad.Plugins.Definition.Tag [property]: [read-only] int
WoofWare.Myriad.Plugins.Definition.Unspecified [static property]: [read-only] WoofWare.Myriad.Plugins.Definition
WoofWare.Myriad.Plugins.HttpClientGenerator inherit obj, implements Myriad.Core.IMyriadGenerator
WoofWare.Myriad.Plugins.HttpClientGenerator..ctor [constructor]: unit
WoofWare.Myriad.Plugins.HttpMethod inherit obj, implements WoofWare.Myriad.Plugins.HttpMethod System.IEquatable, System.Collections.IStructuralEquatable, WoofWare.Myriad.Plugins.HttpMethod System.IComparable, System.IComparable, System.Collections.IStructuralComparable - union type with 8 cases
WoofWare.Myriad.Plugins.HttpMethod+Tags inherit obj
WoofWare.Myriad.Plugins.HttpMethod+Tags.Delete [static field]: int = 2
WoofWare.Myriad.Plugins.HttpMethod+Tags.Get [static field]: int = 0
WoofWare.Myriad.Plugins.HttpMethod+Tags.Head [static field]: int = 5
WoofWare.Myriad.Plugins.HttpMethod+Tags.Options [static field]: int = 4
WoofWare.Myriad.Plugins.HttpMethod+Tags.Patch [static field]: int = 3
WoofWare.Myriad.Plugins.HttpMethod+Tags.Post [static field]: int = 1
WoofWare.Myriad.Plugins.HttpMethod+Tags.Put [static field]: int = 6
WoofWare.Myriad.Plugins.HttpMethod+Tags.Trace [static field]: int = 7
WoofWare.Myriad.Plugins.HttpMethod.Delete [static property]: [read-only] WoofWare.Myriad.Plugins.HttpMethod
WoofWare.Myriad.Plugins.HttpMethod.Equals [method]: (WoofWare.Myriad.Plugins.HttpMethod, System.Collections.IEqualityComparer) -> bool
WoofWare.Myriad.Plugins.HttpMethod.Get [static property]: [read-only] WoofWare.Myriad.Plugins.HttpMethod
WoofWare.Myriad.Plugins.HttpMethod.get_Delete [static method]: unit -> WoofWare.Myriad.Plugins.HttpMethod
WoofWare.Myriad.Plugins.HttpMethod.get_Get [static method]: unit -> WoofWare.Myriad.Plugins.HttpMethod
WoofWare.Myriad.Plugins.HttpMethod.get_Head [static method]: unit -> WoofWare.Myriad.Plugins.HttpMethod
WoofWare.Myriad.Plugins.HttpMethod.get_IsDelete [method]: unit -> bool
WoofWare.Myriad.Plugins.HttpMethod.get_IsGet [method]: unit -> bool
WoofWare.Myriad.Plugins.HttpMethod.get_IsHead [method]: unit -> bool
WoofWare.Myriad.Plugins.HttpMethod.get_IsOptions [method]: unit -> bool
WoofWare.Myriad.Plugins.HttpMethod.get_IsPatch [method]: unit -> bool
WoofWare.Myriad.Plugins.HttpMethod.get_IsPost [method]: unit -> bool
WoofWare.Myriad.Plugins.HttpMethod.get_IsPut [method]: unit -> bool
WoofWare.Myriad.Plugins.HttpMethod.get_IsTrace [method]: unit -> bool
WoofWare.Myriad.Plugins.HttpMethod.get_Options [static method]: unit -> WoofWare.Myriad.Plugins.HttpMethod
WoofWare.Myriad.Plugins.HttpMethod.get_Patch [static method]: unit -> WoofWare.Myriad.Plugins.HttpMethod
WoofWare.Myriad.Plugins.HttpMethod.get_Post [static method]: unit -> WoofWare.Myriad.Plugins.HttpMethod
WoofWare.Myriad.Plugins.HttpMethod.get_Put [static method]: unit -> WoofWare.Myriad.Plugins.HttpMethod
WoofWare.Myriad.Plugins.HttpMethod.get_Tag [method]: unit -> int
WoofWare.Myriad.Plugins.HttpMethod.get_Trace [static method]: unit -> WoofWare.Myriad.Plugins.HttpMethod
WoofWare.Myriad.Plugins.HttpMethod.Head [static property]: [read-only] WoofWare.Myriad.Plugins.HttpMethod
WoofWare.Myriad.Plugins.HttpMethod.IsDelete [property]: [read-only] bool
WoofWare.Myriad.Plugins.HttpMethod.IsGet [property]: [read-only] bool
WoofWare.Myriad.Plugins.HttpMethod.IsHead [property]: [read-only] bool
WoofWare.Myriad.Plugins.HttpMethod.IsOptions [property]: [read-only] bool
WoofWare.Myriad.Plugins.HttpMethod.IsPatch [property]: [read-only] bool
WoofWare.Myriad.Plugins.HttpMethod.IsPost [property]: [read-only] bool
WoofWare.Myriad.Plugins.HttpMethod.IsPut [property]: [read-only] bool
WoofWare.Myriad.Plugins.HttpMethod.IsTrace [property]: [read-only] bool
WoofWare.Myriad.Plugins.HttpMethod.Options [static property]: [read-only] WoofWare.Myriad.Plugins.HttpMethod
WoofWare.Myriad.Plugins.HttpMethod.Parse [static method]: string -> WoofWare.Myriad.Plugins.HttpMethod
WoofWare.Myriad.Plugins.HttpMethod.Patch [static property]: [read-only] WoofWare.Myriad.Plugins.HttpMethod
WoofWare.Myriad.Plugins.HttpMethod.Post [static property]: [read-only] WoofWare.Myriad.Plugins.HttpMethod
WoofWare.Myriad.Plugins.HttpMethod.Put [static property]: [read-only] WoofWare.Myriad.Plugins.HttpMethod
WoofWare.Myriad.Plugins.HttpMethod.Tag [property]: [read-only] int
WoofWare.Myriad.Plugins.HttpMethod.ToDotNet [method]: unit -> System.Net.Http.HttpMethod
WoofWare.Myriad.Plugins.HttpMethod.Trace [static property]: [read-only] WoofWare.Myriad.Plugins.HttpMethod
WoofWare.Myriad.Plugins.InterfaceMockGenerator inherit obj, implements Myriad.Core.IMyriadGenerator
WoofWare.Myriad.Plugins.InterfaceMockGenerator..ctor [constructor]: unit
WoofWare.Myriad.Plugins.JsonParseGenerator inherit obj, implements Myriad.Core.IMyriadGenerator
WoofWare.Myriad.Plugins.JsonParseGenerator..ctor [constructor]: unit
WoofWare.Myriad.Plugins.JsonSerializeGenerator inherit obj, implements Myriad.Core.IMyriadGenerator
WoofWare.Myriad.Plugins.JsonSerializeGenerator..ctor [constructor]: unit
WoofWare.Myriad.Plugins.MimeType inherit obj, implements WoofWare.Myriad.Plugins.MimeType System.IEquatable, System.Collections.IStructuralEquatable, WoofWare.Myriad.Plugins.MimeType System.IComparable, System.IComparable, System.Collections.IStructuralComparable - union type with 1 cases
WoofWare.Myriad.Plugins.MimeType.Equals [method]: (WoofWare.Myriad.Plugins.MimeType, System.Collections.IEqualityComparer) -> bool
WoofWare.Myriad.Plugins.MimeType.get_Item [method]: unit -> string
WoofWare.Myriad.Plugins.MimeType.get_Tag [method]: unit -> int
WoofWare.Myriad.Plugins.MimeType.Item [property]: [read-only] string
WoofWare.Myriad.Plugins.MimeType.NewMimeType [static method]: string -> WoofWare.Myriad.Plugins.MimeType
WoofWare.Myriad.Plugins.MimeType.Tag [property]: [read-only] int
WoofWare.Myriad.Plugins.ObjectTypeDefinition inherit obj, implements WoofWare.Myriad.Plugins.ObjectTypeDefinition System.IEquatable, System.Collections.IStructuralEquatable
WoofWare.Myriad.Plugins.ObjectTypeDefinition..ctor [constructor]: (string option, Map<string, WoofWare.Myriad.Plugins.Definition> option, Map<string, System.Text.Json.Nodes.JsonNode>, string list option, WoofWare.Myriad.Plugins.AdditionalProperties option, System.Text.Json.Nodes.JsonObject option)
WoofWare.Myriad.Plugins.ObjectTypeDefinition.AdditionalProperties [property]: [read-only] WoofWare.Myriad.Plugins.AdditionalProperties option
WoofWare.Myriad.Plugins.ObjectTypeDefinition.Description [property]: [read-only] string option
WoofWare.Myriad.Plugins.ObjectTypeDefinition.Equals [method]: (WoofWare.Myriad.Plugins.ObjectTypeDefinition, System.Collections.IEqualityComparer) -> bool
WoofWare.Myriad.Plugins.ObjectTypeDefinition.Example [property]: [read-only] System.Text.Json.Nodes.JsonObject option
WoofWare.Myriad.Plugins.ObjectTypeDefinition.Extras [property]: [read-only] Map<string, System.Text.Json.Nodes.JsonNode>
WoofWare.Myriad.Plugins.ObjectTypeDefinition.get_AdditionalProperties [method]: unit -> WoofWare.Myriad.Plugins.AdditionalProperties option
WoofWare.Myriad.Plugins.ObjectTypeDefinition.get_Description [method]: unit -> string option
WoofWare.Myriad.Plugins.ObjectTypeDefinition.get_Example [method]: unit -> System.Text.Json.Nodes.JsonObject option
WoofWare.Myriad.Plugins.ObjectTypeDefinition.get_Extras [method]: unit -> Map<string, System.Text.Json.Nodes.JsonNode>
WoofWare.Myriad.Plugins.ObjectTypeDefinition.get_Properties [method]: unit -> Map<string, WoofWare.Myriad.Plugins.Definition> option
WoofWare.Myriad.Plugins.ObjectTypeDefinition.get_Required [method]: unit -> string list option
WoofWare.Myriad.Plugins.ObjectTypeDefinition.Parse [static method]: System.Text.Json.Nodes.JsonObject -> WoofWare.Myriad.Plugins.ObjectTypeDefinition
WoofWare.Myriad.Plugins.ObjectTypeDefinition.Properties [property]: [read-only] Map<string, WoofWare.Myriad.Plugins.Definition> option
WoofWare.Myriad.Plugins.ObjectTypeDefinition.Required [property]: [read-only] string list option
WoofWare.Myriad.Plugins.OperationId inherit obj, implements WoofWare.Myriad.Plugins.OperationId System.IEquatable, System.Collections.IStructuralEquatable, WoofWare.Myriad.Plugins.OperationId System.IComparable, System.IComparable, System.Collections.IStructuralComparable - union type with 1 cases
WoofWare.Myriad.Plugins.OperationId.Equals [method]: (WoofWare.Myriad.Plugins.OperationId, System.Collections.IEqualityComparer) -> bool
WoofWare.Myriad.Plugins.OperationId.get_Item [method]: unit -> string
WoofWare.Myriad.Plugins.OperationId.get_Tag [method]: unit -> int
WoofWare.Myriad.Plugins.OperationId.Item [property]: [read-only] string
WoofWare.Myriad.Plugins.OperationId.NewOperationId [static method]: string -> WoofWare.Myriad.Plugins.OperationId
WoofWare.Myriad.Plugins.OperationId.Tag [property]: [read-only] int
WoofWare.Myriad.Plugins.ParameterIn inherit obj, implements WoofWare.Myriad.Plugins.ParameterIn System.IEquatable, System.Collections.IStructuralEquatable, WoofWare.Myriad.Plugins.ParameterIn System.IComparable, System.IComparable, System.Collections.IStructuralComparable - union type with 4 cases
WoofWare.Myriad.Plugins.ParameterIn+Path inherit WoofWare.Myriad.Plugins.ParameterIn
WoofWare.Myriad.Plugins.ParameterIn+Path.get_name [method]: unit -> string
WoofWare.Myriad.Plugins.ParameterIn+Path.name [property]: [read-only] string
WoofWare.Myriad.Plugins.ParameterIn+Query inherit WoofWare.Myriad.Plugins.ParameterIn
WoofWare.Myriad.Plugins.ParameterIn+Query.get_name [method]: unit -> string
WoofWare.Myriad.Plugins.ParameterIn+Query.name [property]: [read-only] string
WoofWare.Myriad.Plugins.ParameterIn+Tags inherit obj
WoofWare.Myriad.Plugins.ParameterIn+Tags.Body [static field]: int = 2
WoofWare.Myriad.Plugins.ParameterIn+Tags.Path [static field]: int = 0
WoofWare.Myriad.Plugins.ParameterIn+Tags.Query [static field]: int = 1
WoofWare.Myriad.Plugins.ParameterIn+Tags.Unrecognised [static field]: int = 3
WoofWare.Myriad.Plugins.ParameterIn+Unrecognised inherit WoofWare.Myriad.Plugins.ParameterIn
WoofWare.Myriad.Plugins.ParameterIn+Unrecognised.get_name [method]: unit -> string
WoofWare.Myriad.Plugins.ParameterIn+Unrecognised.get_op [method]: unit -> string
WoofWare.Myriad.Plugins.ParameterIn+Unrecognised.name [property]: [read-only] string
WoofWare.Myriad.Plugins.ParameterIn+Unrecognised.op [property]: [read-only] string
WoofWare.Myriad.Plugins.ParameterIn.Body [static property]: [read-only] WoofWare.Myriad.Plugins.ParameterIn
WoofWare.Myriad.Plugins.ParameterIn.Equals [method]: (WoofWare.Myriad.Plugins.ParameterIn, System.Collections.IEqualityComparer) -> bool
WoofWare.Myriad.Plugins.ParameterIn.get_Body [static method]: unit -> WoofWare.Myriad.Plugins.ParameterIn
WoofWare.Myriad.Plugins.ParameterIn.get_IsBody [method]: unit -> bool
WoofWare.Myriad.Plugins.ParameterIn.get_IsPath [method]: unit -> bool
WoofWare.Myriad.Plugins.ParameterIn.get_IsQuery [method]: unit -> bool
WoofWare.Myriad.Plugins.ParameterIn.get_IsUnrecognised [method]: unit -> bool
WoofWare.Myriad.Plugins.ParameterIn.get_Tag [method]: unit -> int
WoofWare.Myriad.Plugins.ParameterIn.IsBody [property]: [read-only] bool
WoofWare.Myriad.Plugins.ParameterIn.IsPath [property]: [read-only] bool
WoofWare.Myriad.Plugins.ParameterIn.IsQuery [property]: [read-only] bool
WoofWare.Myriad.Plugins.ParameterIn.IsUnrecognised [property]: [read-only] bool
WoofWare.Myriad.Plugins.ParameterIn.NewPath [static method]: string -> WoofWare.Myriad.Plugins.ParameterIn
WoofWare.Myriad.Plugins.ParameterIn.NewQuery [static method]: string -> WoofWare.Myriad.Plugins.ParameterIn
WoofWare.Myriad.Plugins.ParameterIn.NewUnrecognised [static method]: (string, string) -> WoofWare.Myriad.Plugins.ParameterIn
WoofWare.Myriad.Plugins.ParameterIn.Tag [property]: [read-only] int
WoofWare.Myriad.Plugins.RemoveOptionsGenerator inherit obj, implements Myriad.Core.IMyriadGenerator
WoofWare.Myriad.Plugins.RemoveOptionsGenerator..ctor [constructor]: unit
WoofWare.Myriad.Plugins.Response inherit obj, implements WoofWare.Myriad.Plugins.Response System.IEquatable, System.Collections.IStructuralEquatable
WoofWare.Myriad.Plugins.Response..ctor [constructor]: (string, WoofWare.Myriad.Plugins.Definition)
WoofWare.Myriad.Plugins.Response.Description [property]: [read-only] string
WoofWare.Myriad.Plugins.Response.Equals [method]: (WoofWare.Myriad.Plugins.Response, System.Collections.IEqualityComparer) -> bool
WoofWare.Myriad.Plugins.Response.get_Description [method]: unit -> string
WoofWare.Myriad.Plugins.Response.get_Schema [method]: unit -> WoofWare.Myriad.Plugins.Definition
WoofWare.Myriad.Plugins.Response.Parse [static method]: System.Text.Json.Nodes.JsonObject -> WoofWare.Myriad.Plugins.Response
WoofWare.Myriad.Plugins.Response.Schema [property]: [read-only] WoofWare.Myriad.Plugins.Definition
WoofWare.Myriad.Plugins.Scheme inherit obj, implements WoofWare.Myriad.Plugins.Scheme System.IEquatable, System.Collections.IStructuralEquatable, WoofWare.Myriad.Plugins.Scheme System.IComparable, System.IComparable, System.Collections.IStructuralComparable - union type with 1 cases
WoofWare.Myriad.Plugins.Scheme.Equals [method]: (WoofWare.Myriad.Plugins.Scheme, System.Collections.IEqualityComparer) -> bool
WoofWare.Myriad.Plugins.Scheme.get_Item [method]: unit -> string
WoofWare.Myriad.Plugins.Scheme.get_Tag [method]: unit -> int
WoofWare.Myriad.Plugins.Scheme.Item [property]: [read-only] string
WoofWare.Myriad.Plugins.Scheme.NewScheme [static method]: string -> WoofWare.Myriad.Plugins.Scheme
WoofWare.Myriad.Plugins.Scheme.Tag [property]: [read-only] int
WoofWare.Myriad.Plugins.Swagger inherit obj, implements WoofWare.Myriad.Plugins.Swagger System.IEquatable, System.Collections.IStructuralEquatable
WoofWare.Myriad.Plugins.Swagger..ctor [constructor]: (WoofWare.Myriad.Plugins.MimeType list, WoofWare.Myriad.Plugins.MimeType list, WoofWare.Myriad.Plugins.Scheme list, System.Version, WoofWare.Myriad.Plugins.SwaggerInfo, string, Map<string, Map<WoofWare.Myriad.Plugins.HttpMethod, WoofWare.Myriad.Plugins.SwaggerEndpoint>>, Map<string, WoofWare.Myriad.Plugins.Definition>, Map<string, WoofWare.Myriad.Plugins.Response>)
WoofWare.Myriad.Plugins.Swagger.BasePath [property]: [read-only] string
WoofWare.Myriad.Plugins.Swagger.Consumes [property]: [read-only] WoofWare.Myriad.Plugins.MimeType list
WoofWare.Myriad.Plugins.Swagger.Definitions [property]: [read-only] Map<string, WoofWare.Myriad.Plugins.Definition>
WoofWare.Myriad.Plugins.Swagger.Equals [method]: (WoofWare.Myriad.Plugins.Swagger, System.Collections.IEqualityComparer) -> bool
WoofWare.Myriad.Plugins.Swagger.get_BasePath [method]: unit -> string
WoofWare.Myriad.Plugins.Swagger.get_Consumes [method]: unit -> WoofWare.Myriad.Plugins.MimeType list
WoofWare.Myriad.Plugins.Swagger.get_Definitions [method]: unit -> Map<string, WoofWare.Myriad.Plugins.Definition>
WoofWare.Myriad.Plugins.Swagger.get_Info [method]: unit -> WoofWare.Myriad.Plugins.SwaggerInfo
WoofWare.Myriad.Plugins.Swagger.get_Paths [method]: unit -> Map<string, Map<WoofWare.Myriad.Plugins.HttpMethod, WoofWare.Myriad.Plugins.SwaggerEndpoint>>
WoofWare.Myriad.Plugins.Swagger.get_Produces [method]: unit -> WoofWare.Myriad.Plugins.MimeType list
WoofWare.Myriad.Plugins.Swagger.get_Responses [method]: unit -> Map<string, WoofWare.Myriad.Plugins.Response>
WoofWare.Myriad.Plugins.Swagger.get_Schemes [method]: unit -> WoofWare.Myriad.Plugins.Scheme list
WoofWare.Myriad.Plugins.Swagger.get_Swagger [method]: unit -> System.Version
WoofWare.Myriad.Plugins.Swagger.Info [property]: [read-only] WoofWare.Myriad.Plugins.SwaggerInfo
WoofWare.Myriad.Plugins.Swagger.Paths [property]: [read-only] Map<string, Map<WoofWare.Myriad.Plugins.HttpMethod, WoofWare.Myriad.Plugins.SwaggerEndpoint>>
WoofWare.Myriad.Plugins.Swagger.Produces [property]: [read-only] WoofWare.Myriad.Plugins.MimeType list
WoofWare.Myriad.Plugins.Swagger.Responses [property]: [read-only] Map<string, WoofWare.Myriad.Plugins.Response>
WoofWare.Myriad.Plugins.Swagger.Schemes [property]: [read-only] WoofWare.Myriad.Plugins.Scheme list
WoofWare.Myriad.Plugins.Swagger.Swagger [property]: [read-only] System.Version
WoofWare.Myriad.Plugins.SwaggerClientGenerator inherit obj, implements Myriad.Core.IMyriadGenerator
WoofWare.Myriad.Plugins.SwaggerClientGenerator..ctor [constructor]: unit
WoofWare.Myriad.Plugins.SwaggerEndpoint inherit obj, implements WoofWare.Myriad.Plugins.SwaggerEndpoint System.IEquatable, System.Collections.IStructuralEquatable
WoofWare.Myriad.Plugins.SwaggerEndpoint..ctor [constructor]: (WoofWare.Myriad.Plugins.MimeType list option, WoofWare.Myriad.Plugins.MimeType list option, string list, string, WoofWare.Myriad.Plugins.OperationId, WoofWare.Myriad.Plugins.SwaggerParameter list option, Map<int, WoofWare.Myriad.Plugins.Definition>)
WoofWare.Myriad.Plugins.SwaggerEndpoint.Consumes [property]: [read-only] WoofWare.Myriad.Plugins.MimeType list option
WoofWare.Myriad.Plugins.SwaggerEndpoint.Equals [method]: (WoofWare.Myriad.Plugins.SwaggerEndpoint, System.Collections.IEqualityComparer) -> bool
WoofWare.Myriad.Plugins.SwaggerEndpoint.get_Consumes [method]: unit -> WoofWare.Myriad.Plugins.MimeType list option
WoofWare.Myriad.Plugins.SwaggerEndpoint.get_OperationId [method]: unit -> WoofWare.Myriad.Plugins.OperationId
WoofWare.Myriad.Plugins.SwaggerEndpoint.get_Parameters [method]: unit -> WoofWare.Myriad.Plugins.SwaggerParameter list option
WoofWare.Myriad.Plugins.SwaggerEndpoint.get_Produces [method]: unit -> WoofWare.Myriad.Plugins.MimeType list option
WoofWare.Myriad.Plugins.SwaggerEndpoint.get_Responses [method]: unit -> Map<int, WoofWare.Myriad.Plugins.Definition>
WoofWare.Myriad.Plugins.SwaggerEndpoint.get_Summary [method]: unit -> string
WoofWare.Myriad.Plugins.SwaggerEndpoint.get_Tags [method]: unit -> string list
WoofWare.Myriad.Plugins.SwaggerEndpoint.OperationId [property]: [read-only] WoofWare.Myriad.Plugins.OperationId
WoofWare.Myriad.Plugins.SwaggerEndpoint.Parameters [property]: [read-only] WoofWare.Myriad.Plugins.SwaggerParameter list option
WoofWare.Myriad.Plugins.SwaggerEndpoint.Parse [static method]: System.Text.Json.Nodes.JsonObject -> WoofWare.Myriad.Plugins.SwaggerEndpoint
WoofWare.Myriad.Plugins.SwaggerEndpoint.Produces [property]: [read-only] WoofWare.Myriad.Plugins.MimeType list option
WoofWare.Myriad.Plugins.SwaggerEndpoint.Responses [property]: [read-only] Map<int, WoofWare.Myriad.Plugins.Definition>
WoofWare.Myriad.Plugins.SwaggerEndpoint.Summary [property]: [read-only] string
WoofWare.Myriad.Plugins.SwaggerEndpoint.Tags [property]: [read-only] string list
WoofWare.Myriad.Plugins.SwaggerInfo inherit obj, implements WoofWare.Myriad.Plugins.SwaggerInfo System.IEquatable, System.Collections.IStructuralEquatable
WoofWare.Myriad.Plugins.SwaggerInfo..ctor [constructor]: (string, string, WoofWare.Myriad.Plugins.SwaggerLicense, System.Version)
WoofWare.Myriad.Plugins.SwaggerInfo.Description [property]: [read-only] string
WoofWare.Myriad.Plugins.SwaggerInfo.Equals [method]: (WoofWare.Myriad.Plugins.SwaggerInfo, System.Collections.IEqualityComparer) -> bool
WoofWare.Myriad.Plugins.SwaggerInfo.get_Description [method]: unit -> string
WoofWare.Myriad.Plugins.SwaggerInfo.get_License [method]: unit -> WoofWare.Myriad.Plugins.SwaggerLicense
WoofWare.Myriad.Plugins.SwaggerInfo.get_Title [method]: unit -> string
WoofWare.Myriad.Plugins.SwaggerInfo.get_Version [method]: unit -> System.Version
WoofWare.Myriad.Plugins.SwaggerInfo.License [property]: [read-only] WoofWare.Myriad.Plugins.SwaggerLicense
WoofWare.Myriad.Plugins.SwaggerInfo.Parse [static method]: System.Text.Json.Nodes.JsonObject -> WoofWare.Myriad.Plugins.SwaggerInfo
WoofWare.Myriad.Plugins.SwaggerInfo.Title [property]: [read-only] string
WoofWare.Myriad.Plugins.SwaggerInfo.Version [property]: [read-only] System.Version
WoofWare.Myriad.Plugins.SwaggerLicense inherit obj, implements WoofWare.Myriad.Plugins.SwaggerLicense System.IEquatable, System.Collections.IStructuralEquatable
WoofWare.Myriad.Plugins.SwaggerLicense..ctor [constructor]: (string, System.Uri option, string option)
WoofWare.Myriad.Plugins.SwaggerLicense.Equals [method]: (WoofWare.Myriad.Plugins.SwaggerLicense, System.Collections.IEqualityComparer) -> bool
WoofWare.Myriad.Plugins.SwaggerLicense.get_Identifier [method]: unit -> string option
WoofWare.Myriad.Plugins.SwaggerLicense.get_Name [method]: unit -> string
WoofWare.Myriad.Plugins.SwaggerLicense.get_Url [method]: unit -> System.Uri option
WoofWare.Myriad.Plugins.SwaggerLicense.Identifier [property]: [read-only] string option
WoofWare.Myriad.Plugins.SwaggerLicense.Name [property]: [read-only] string
WoofWare.Myriad.Plugins.SwaggerLicense.Parse [static method]: System.Text.Json.Nodes.JsonObject -> WoofWare.Myriad.Plugins.SwaggerLicense
WoofWare.Myriad.Plugins.SwaggerLicense.Url [property]: [read-only] System.Uri option
WoofWare.Myriad.Plugins.SwaggerModule inherit obj
WoofWare.Myriad.Plugins.SwaggerModule.parse [static method]: string -> WoofWare.Myriad.Plugins.Swagger
WoofWare.Myriad.Plugins.SwaggerParameter inherit obj, implements WoofWare.Myriad.Plugins.SwaggerParameter System.IEquatable, System.Collections.IStructuralEquatable
WoofWare.Myriad.Plugins.SwaggerParameter..ctor [constructor]: (WoofWare.Myriad.Plugins.Definition, string option, WoofWare.Myriad.Plugins.ParameterIn, string, bool option)
WoofWare.Myriad.Plugins.SwaggerParameter.Description [property]: [read-only] string option
WoofWare.Myriad.Plugins.SwaggerParameter.Equals [method]: (WoofWare.Myriad.Plugins.SwaggerParameter, System.Collections.IEqualityComparer) -> bool
WoofWare.Myriad.Plugins.SwaggerParameter.get_Description [method]: unit -> string option
WoofWare.Myriad.Plugins.SwaggerParameter.get_In [method]: unit -> WoofWare.Myriad.Plugins.ParameterIn
WoofWare.Myriad.Plugins.SwaggerParameter.get_Name [method]: unit -> string
WoofWare.Myriad.Plugins.SwaggerParameter.get_Required [method]: unit -> bool option
WoofWare.Myriad.Plugins.SwaggerParameter.get_Type [method]: unit -> WoofWare.Myriad.Plugins.Definition
WoofWare.Myriad.Plugins.SwaggerParameter.In [property]: [read-only] WoofWare.Myriad.Plugins.ParameterIn
WoofWare.Myriad.Plugins.SwaggerParameter.Name [property]: [read-only] string
WoofWare.Myriad.Plugins.SwaggerParameter.Parse [static method]: System.Text.Json.Nodes.JsonObject -> WoofWare.Myriad.Plugins.SwaggerParameter
WoofWare.Myriad.Plugins.SwaggerParameter.Required [property]: [read-only] bool option
WoofWare.Myriad.Plugins.SwaggerParameter.Type [property]: [read-only] WoofWare.Myriad.Plugins.Definition
WoofWare.Myriad.Plugins.SynFieldData`1 inherit obj
WoofWare.Myriad.Plugins.SynFieldData`1..ctor [constructor]: (Fantomas.FCS.Syntax.SynAttribute list, 'Ident, Fantomas.FCS.Syntax.SynType)
WoofWare.Myriad.Plugins.SynFieldData`1.Attrs [property]: [read-only] Fantomas.FCS.Syntax.SynAttribute list

View File

@@ -0,0 +1,576 @@
namespace WoofWare.Myriad.Plugins
open System
open System.Text.Json.Nodes
[<AutoOpen>]
module internal JsonHelpers =
let inline asString (n : JsonNode) (key : string) : string =
match n.[key] with
| null -> failwith $"Expected node to have a key '%s{key}', but it did not: %s{n.ToJsonString ()}"
| s -> s.GetValue<string> ()
[<RequiresExplicitTypeArguments>]
let inline asOpt<'ret> (n : JsonNode) (key : string) : 'ret option =
match n.[key] with
| null -> None
| s -> s.GetValue<'ret> () |> Some
let inline asObj (n : JsonNode) (key : string) : JsonObject =
match n.[key] with
| null -> failwith $"Expected node to have a key '%s{key}', but it did not: %s{n.ToJsonString ()}"
| o -> o.AsObject ()
let inline asObjOpt (n : JsonNode) (key : string) : JsonObject option =
match n.[key] with
| null -> None
| o -> o.AsObject () |> Some
let inline asArr (n : JsonNode) (key : string) : JsonArray =
match n.[key] with
| null -> failwith $"Expected node to have a key '%s{key}', but it did not: %s{n.ToJsonString ()}"
| o -> o.AsArray ()
let inline asArrOpt (n : JsonNode) (key : string) : JsonArray option =
match n.[key] with
| null -> None
| o -> o.AsArray () |> Some
[<RequiresExplicitTypeArguments>]
let inline asArr'<'v> (n : JsonNode) (key : string) : 'v list =
match n.[key] with
| null -> failwith $"Expected node to have a key '%s{key}', but it did not: %s{n.ToJsonString ()}"
| o -> o.AsArray () |> Seq.map (fun v -> v.GetValue<'v> ()) |> Seq.toList
[<RequiresExplicitTypeArguments>]
let inline asArrOpt'<'v> (n : JsonNode) (key : string) : 'v list option =
match n.[key] with
| null -> None
| o -> o.AsArray () |> Seq.map (fun v -> v.GetValue<'v> ()) |> Seq.toList |> Some
/// A MIME type, like "application/json"
type MimeType =
/// A MIME type, like "application/json"
| MimeType of string
/// A URL scheme, like "https"
type Scheme =
/// A URL scheme, like "https"
| Scheme of string
/// "Licence information for the exposed API", whatever that means.
type SwaggerLicense =
{
/// "The license name used for the API", whatever that means.
Name : string
/// Link to the license used. Mutually exclusive with `Identifier`.
Url : Uri option
/// SPDX license identifier. Mutually exclusive with `Url`.
Identifier : string option
}
/// Render a JsonObject into the strongly-typed version, performing sanity
/// checks and throwing on input that can't be parsed.
static member Parse (node : JsonObject) : SwaggerLicense =
let name = asString node "name"
let url = asOpt<string> node "url" |> Option.map Uri
let identifier = asOpt<string> node "identifier"
match url, identifier with
| Some _, Some _ -> failwith "Invalid license spec: cannot supply both URL and identifier"
| _, _ -> ()
{
Name = name
Url = url
Identifier = identifier
}
/// Overall information about the API described by this Swagger spec.
type SwaggerInfo =
{
/// Human-readable description of what this Swagger API is for.
/// Supports GitHub-flavoured markdown, apparently.
Description : string
/// Human-readable title of the service to which this is an API.
Title : string
/// The license applying to this schema. It's very unclear what this means.
/// The spec just says:
/// "Licence information for the exposed API"
License : SwaggerLicense
/// The version of this API (not the version of Swagger or the file defining the API!).
/// Strictly speaking this can be anything, but I am assuming it's roughly
/// SemVer.
Version : Version
}
/// Render a JsonObject into the strongly-typed version, performing sanity
/// checks and throwing on input that can't be parsed.
static member Parse (node : JsonObject) : SwaggerInfo =
let description = asString node "description"
let title = asString node "title"
let version = asString node "version" |> Version.Parse
let license = asObj node "license" |> SwaggerLicense.Parse
{
Description = description
Title = title
License = license
Version = version
}
/// An "optional unique string used to describe an operation".
/// If present, these are assumed to be unique among all operations described
/// in the API.
type OperationId =
/// An "optional unique string used to describe an operation".
/// If present, these are assumed to be unique among all operations described
/// in the API.
| OperationId of string
/// Round-trip string representation.
override this.ToString () =
match this with
| OperationId.OperationId s -> s
/// Constraints on the `additionalProperties` (in the JSON schema sense).
/// "Additional properties" are properties of a JSON object which were not
/// listed in the schema.
type AdditionalProperties =
/// No additional properties are allowed: all properties must have been
/// mentioned in the schema.
| Never
/// Additional properties are permitted, but if they exist, they must
/// match this schema definition.
| Constrained of Definition
/// The Swagger schema lets you define types. An ObjectTypeDefinition
/// is specifically the information about types defined as `"type": "object"`.
and ObjectTypeDefinition =
{
/// Human-readable description of the purpose of this type.
Description : string option
/// Fields which any object must have to satisfy this type.
Properties : Map<string, Definition> option
/// Extra properties in the type description. In Gitea, these are
/// (for example) "x-go-package":"code.gitea.io/gitea/modules/structs".
Extras : Map<string, JsonNode>
/// List of fields which are required; all other fields are optional.
Required : string list option
/// Constraints, if any, placed on fields which are not mentioned in
/// the schema. If absent, there are no constraints.
AdditionalProperties : AdditionalProperties option
/// Example of an object which satisfies this schema.
Example : JsonObject option
}
/// Render a JsonObject into the strongly-typed version, performing sanity
/// checks and throwing on input that can't be parsed.
static member Parse (node : JsonObject) : ObjectTypeDefinition =
let description =
match asOpt<string> node "description", asOpt<string> node "title" with
| None, None -> None
| Some v, None
| None, Some v -> Some v
| Some v1, Some v2 -> failwith "both description and title were given"
let additionalProperties =
match node.["additionalProperties"] with
| null -> None
| :? JsonValue as p ->
if not (p.GetValue<bool> ()) then
Some AdditionalProperties.Never
else
failwith $"additionalProperties should be 'false' or an object, but was: %s{p.ToJsonString ()}"
| p ->
let p = p.AsObject ()
Definition.Parse p |> AdditionalProperties.Constrained |> Some
let properties =
match node.["properties"] with
| null -> None
| p ->
p.AsObject ()
|> Seq.map (fun (KeyValue (key, value)) ->
let value = value.AsObject ()
key, Definition.Parse value
)
|> Map.ofSeq
|> Some
let example = asObjOpt node "example"
let required = asArrOpt'<string> node "required"
let extras =
node.AsObject ()
|> Seq.choose (fun (KeyValue (key, value)) ->
match key with
| "type"
| "description"
| "title"
| "additionalProperties"
| "example"
| "required"
| "properties" -> None
| _ -> Some (key, value)
)
|> Map.ofSeq
{
Description = description
Properties = properties
AdditionalProperties = additionalProperties
Required = required
Extras = extras
Example = example
}
/// The Swagger schema lets you define types. An ArrayTypeDefinition
/// is specifically the information about types defined as `"type": "array"`.
and ArrayTypeDefinition =
{
/// The type is `'a array`; this field describes `'a`.
Items : Definition
}
/// Render a JsonNode into the strongly-typed version, performing sanity
/// checks and throwing on input that can't be parsed.
static member Parse (n : JsonNode) : ArrayTypeDefinition =
let items = asObj n "items" |> Definition.Parse
{
Items = items
}
/// Any definition of a type in the Swagger document. This is basically any
/// information associated with the `"type": "blah"` field.
and Definition =
/// For example, if `"$ref": "#/responses/Blah", then this is "#/responses/Blah".
| Handle of string
/// A type definition with "type": "object".
| Object of ObjectTypeDefinition
/// A type definition with "type": "array".
| Array of ArrayTypeDefinition
/// A type definition with "type": "string".
| String
/// A type definition with "type": "boolean".
| Boolean
/// A response without a body has no "schema" specified.
| Unspecified
/// A type definition with "type": "integer".
/// The format is an optional hint which could be e.g. "int64" or "int32".
/// https://swagger.io/docs/specification/data-models/data-types/#numbers
| Integer of format : string option
/// Not a JSON schema type, but a Swagger 2.0 type.
| File
/// Render a JsonObject into this strongly-typed specification.
static member Parse (n : JsonObject) : Definition =
match n.["$ref"] |> Option.ofObj with
| Some ref -> Definition.Handle (ref.GetValue<string> ())
| None ->
let ty = asOpt<string> n "type"
match ty with
| None -> Definition.Unspecified
| Some "object" -> ObjectTypeDefinition.Parse n |> Definition.Object
| Some "array" -> ArrayTypeDefinition.Parse n |> Definition.Array
| Some "string" -> Definition.String
| Some "boolean" -> Definition.Boolean
| Some "file" -> Definition.File
| Some "integer" ->
let format = asOpt<string> n "format"
Definition.Integer format
| Some ty -> failwith $"Unrecognised type: %s{ty}"
/// REST APIs allow their parameters to be passed in various ways. This describes
/// how one single parameter is passed.
type ParameterIn =
/// The parameter is interpolated into the path, e.g. "/foo/{blah}".
/// The "name" is what we replace in the path: e.g. "/foo/{person}" would
/// have a name of "person".
| Path of name : string
/// The parameter is appended to the URL's query params, e.g. "?<name>=blah"
| Query of name : string
/// The parameter is passed in the body of the HTTP request.
| Body
/// Some spec that WoofWare.Myriad doesn't support.
| Unrecognised of op : string * name : string
/// Description of a single input parameter to an endpoint.
type SwaggerParameter =
{
/// The type schema to which this parameter must conform.
Type : Definition
/// Optional human-readable description of this parameter.
Description : string option
/// How this parameter is passed.
In : ParameterIn
/// Name of this parameter. For most `In` values, this name is the
/// name of the parameter as supplied to the API at runtime, and in WoofWare's
/// strongly-typed domain types this information is also contained in the `In` field.
/// For `Body` parameters, this is purely for dev-time information.
Name : string
/// Whether this parameter is required for validation to succeed.
/// I think this defaults to "no".
Required : bool option
}
/// Render a JsonObject into this strongly-typed specification.
static member Parse (node : JsonObject) : SwaggerParameter =
let ty =
match asObjOpt node "schema" with
| None -> Definition.Parse node
| Some node -> Definition.Parse node
let description = asOpt<string> node "description"
let name = asString node "name"
let paramIn =
match asString node "in" with
| "path" -> ParameterIn.Path name
| "query" -> ParameterIn.Query name
| "body" -> ParameterIn.Body
| f -> ParameterIn.Unrecognised (f, name)
let required = asOpt<bool> node "required"
{
Type = ty
Description = description
In = paramIn
Name = name
Required = required
}
/// An "endpoint" is basically a single HTTP verb, applied to some path.
type SwaggerEndpoint =
{
/// The MIME types we should send our request body in.
/// This overrides (does not extend) any global definitions on the spec itself.
Consumes : MimeType list option
/// The MIME types we should expect to receive in response to this request.
/// This overrides (does not extend) any global definitions on the spec itself.
Produces : MimeType list option
/// Arbitrary list of [tags](https://swagger.io/docs/specification/2-0/grouping-operations-with-tags/).
Tags : string list
/// Human-readable description of the endpoint.
Summary : string
/// Arbitrary identifier of this endpoint; this must be unique across *all* endpoints
/// in this entire spec.
OperationId : OperationId
/// Parameters that must be supplied at HTTP-request-time to the endpoint.
/// (Each parameter knows how it needs to be supplied: e.g. if it's a query parameter or
/// if it's interpolated into the path.)
Parameters : SwaggerParameter list option
/// Map of HTTP response code to the type that we expect to receive in the body if we
/// get that response code back.
Responses : Map<int, Definition>
}
/// Render a JsonObject into this strongly-typed specification.
static member Parse (r : JsonObject) : SwaggerEndpoint =
let produces = asArrOpt'<string> r "produces" |> Option.map (List.map MimeType)
let consumes = asArrOpt'<string> r "consumes" |> Option.map (List.map MimeType)
let tags = asArr'<string> r "tags"
let summary = asString r "summary"
let operationId = asString r "operationId" |> OperationId
let responses =
asObj r "responses"
|> Seq.map (fun (KeyValue (key, value)) ->
let value = value.AsObject ()
Int32.Parse key, Definition.Parse value
)
|> Map.ofSeq
let parameters =
asArrOpt r "parameters"
|> Option.map (fun pars ->
pars
|> Seq.map (fun par -> par.AsObject () |> SwaggerParameter.Parse)
|> Seq.toList
)
{
Produces = produces
Consumes = consumes
Tags = tags
Summary = summary
OperationId = operationId
Parameters = parameters
Responses = responses
}
/// Specifies the form a response to an endpoint will take if it's complying with this spec.
type Response =
{
/// Human-readable description.
Description : string
/// Specification of the type to which responses will conform under this spec.
Schema : Definition
}
/// Render a JsonObject into this strongly-typed specification.
static member Parse (r : JsonObject) : Response =
let desc = asString r "description"
let schema =
match asObjOpt r "schema" with
| None -> Definition.Unspecified
| Some s -> Definition.Parse s
{
Description = desc
Schema = schema
}
/// An HTTP method. This is System.Net.Http.HttpMethod, but
/// a proper discriminated union.
type HttpMethod =
/// HTTP Get
| Get
/// HTTP Post
| Post
/// HTTP Delete
| Delete
/// HTTP Patch
| Patch
/// HTTP Options
| Options
/// HTTP Head
| Head
/// HTTP Put
| Put
/// HTTP Trace
| Trace
/// Convert to the standard library's enum type.
member this.ToDotNet () : System.Net.Http.HttpMethod =
match this with
| HttpMethod.Get -> System.Net.Http.HttpMethod.Get
| HttpMethod.Post -> System.Net.Http.HttpMethod.Post
| HttpMethod.Delete -> System.Net.Http.HttpMethod.Delete
| HttpMethod.Patch -> System.Net.Http.HttpMethod.Patch
| HttpMethod.Options -> System.Net.Http.HttpMethod.Options
| HttpMethod.Head -> System.Net.Http.HttpMethod.Head
| HttpMethod.Put -> System.Net.Http.HttpMethod.Put
| HttpMethod.Trace -> System.Net.Http.HttpMethod.Trace
/// Human-readable string representation.
override this.ToString () : string =
match this with
| HttpMethod.Get -> "Get"
| HttpMethod.Post -> "Post"
| HttpMethod.Delete -> "Delete"
| HttpMethod.Patch -> "Post"
| HttpMethod.Options -> "Options"
| HttpMethod.Head -> "Head"
| HttpMethod.Put -> "Put"
| HttpMethod.Trace -> "Trace"
/// Throws on invalid inputs.
static member Parse (s : string) : HttpMethod =
if String.Equals (s, "get", StringComparison.OrdinalIgnoreCase) then
HttpMethod.Get
elif String.Equals (s, "post", StringComparison.OrdinalIgnoreCase) then
HttpMethod.Post
elif String.Equals (s, "patch", StringComparison.OrdinalIgnoreCase) then
HttpMethod.Patch
elif String.Equals (s, "delete", StringComparison.OrdinalIgnoreCase) then
HttpMethod.Delete
elif String.Equals (s, "head", StringComparison.OrdinalIgnoreCase) then
HttpMethod.Head
elif String.Equals (s, "options", StringComparison.OrdinalIgnoreCase) then
HttpMethod.Options
elif String.Equals (s, "put", StringComparison.OrdinalIgnoreCase) then
HttpMethod.Put
else
failwith $"Unrecognised method: %s{s}"
/// A Swagger API specification.
type Swagger =
{
/// Global collection of MIME types which any endpoint expects to consume its inputs in.
/// This may be overridden on any individual endpoint by that endpoint.
Consumes : MimeType list
/// Global collection of MIME types which any endpoint will produce.
/// This may be overridden on any individual endpoint by that endpoint.
Produces : MimeType list
/// HTTP or HTTPS, for example. Indicates which scheme to access the API on.
Schemes : Scheme list
/// The version of OpenAPI this specification is written against.
/// (As of this writing, we only support 2.0.)
Swagger : Version
/// General information about this API.
Info : SwaggerInfo
/// Path under the URI host, which should be prefixed (with trailing slash if necessary)
/// to all requests.
BasePath : string
/// Map from relative path to "what is served at that path".
Paths : Map<string, Map<HttpMethod, SwaggerEndpoint>>
/// Types defined in the schema. Requests may use these definitions just like in any other JSON schema.
/// Key is a domain type name, e.g. "APIError".
Definitions : Map<string, Definition>
/// Types of each response.
/// Key is a domain type name, e.g. "AccessToken".
Responses : Map<string, Response>
}
[<RequireQualifiedAccess>]
module Swagger =
/// Parse a JSON-schema-based specification of a Swagger 2.0 API and
/// build the strongly-typed version. Throws on invalid inputs.
let parse (s : string) : Swagger =
let node = JsonNode.Parse s
let consumes = asArr'<string> node "consumes" |> List.map MimeType
let produces = asArr'<string> node "produces" |> List.map MimeType
let schemes = asArr'<string> node "schemes" |> List.map Scheme
let swagger = asString node "swagger" |> Version.Parse
let info = asObj node "info" |> SwaggerInfo.Parse
let basePath = asString node "basePath"
let definitions =
asObj node "definitions"
|> Seq.map (fun (KeyValue (key, value)) ->
let value = value.AsObject ()
key, Definition.Parse value
)
|> Map.ofSeq
let paths =
asObj node "paths"
|> Seq.map (fun (KeyValue (key, value)) ->
let contents =
value.AsObject ()
|> Seq.map (fun (KeyValue (endpoint, contents)) ->
let contents = contents.AsObject ()
HttpMethod.Parse endpoint, SwaggerEndpoint.Parse contents
)
|> Map.ofSeq
key, contents
)
|> Map.ofSeq
let responses =
asObj node "responses"
|> Seq.map (fun (KeyValue (key, value)) ->
let value = value.AsObject ()
key, Response.Parse value
)
|> Map.ofSeq
{
Consumes = consumes
Produces = produces
Schemes = schemes
Swagger = swagger
Info = info
BasePath = basePath
Paths = paths
Definitions = definitions
Responses = responses
}

View File

@@ -0,0 +1,724 @@
namespace WoofWare.Myriad.Plugins
open System.Collections.Generic
open System.IO
open System.Threading
open Fantomas.FCS.Syntax
open Fantomas.FCS.Xml
open Fantomas.FCS.Text.Range
type internal SwaggerClientConfig =
{
/// Additionally create a mock with `InterfaceMockGenerator`, with the given boolean arg.
/// (`None` means "no mock".)
CreateMock : bool option
ClassName : string
}
type internal Produces =
// TODO: this will cope with decoding JSON, plain text, etc
| Produces of string
type internal Endpoint =
{
DocString : PreXmlDoc
Produces : Produces
ReturnType : Definition
Method : WoofWare.Myriad.Plugins.HttpMethod
Operation : OperationId
Parameters : SwaggerParameter list
Endpoint : string
}
type internal TypeEntry =
{
/// If we had to define a type for this, here it is.
FSharpDefinition : SynTypeDefn option
/// SynType you use in e.g. a type annotation to refer to this type in F# code.
Signature : SynType
}
type internal Types =
{
ByHandle : IReadOnlyDictionary<string, TypeEntry>
ByDefinition : IReadOnlyDictionary<Definition, TypeEntry>
}
[<RequireQualifiedAccess>]
module internal SwaggerClientGenerator =
let outputFile = FileInfo "/tmp/output.txt"
// do
// use _ = File.Create outputFile.FullName
// ()
let log (line : string) =
// use w = outputFile.AppendText ()
// w.WriteLine line
()
let renderType (types : Types) (defn : Definition) : SynType option =
match types.ByDefinition.TryGetValue defn with
| true, v -> Some v.Signature
| false, _ ->
match defn with
| Definition.Handle h ->
match types.ByHandle.TryGetValue h with
| false, _ -> None
| true, v -> Some v.Signature
| Definition.Object _ -> failwith "should not hit"
| Definition.Array _ -> failwith "should not hit"
| Definition.Unspecified -> failwith "should not hit"
| Definition.String -> SynType.string |> Some
| Definition.Boolean -> SynType.bool |> Some
| Definition.Integer _ -> SynType.int |> Some
| Definition.File -> SynType.createLongIdent' [ "System" ; "IO" ; "Stream" ] |> Some
/// Returns None if we lacked the information required to do this.
/// bigCache is a map of e.g. {"securityDefinition": {Defn : F# type}}.
let rec defnToType
(anonymousTypeCount : int ref)
(handlesMap : Dictionary<string, TypeEntry>)
(bigCache : Dictionary<string, Dictionary<Definition, TypeEntry>>)
(thisKey : string)
(typeName : string option)
(d : Definition)
: TypeEntry option
=
let cache =
match bigCache.TryGetValue thisKey with
| false, _ ->
let d = Dictionary ()
bigCache.Add (thisKey, d)
d
| true, d -> d
let handleKey =
match typeName with
| None -> None
| Some typeName -> $"#/%s{thisKey}/%s{typeName}" |> Some
match handleKey with
| Some hk when handlesMap.ContainsKey hk ->
let result = handlesMap.[hk]
cache.[d] <- result
Some result
| _ ->
match cache.TryGetValue d with
| true, v ->
match handleKey with
| None -> ()
| Some key -> handlesMap.Add (key, v)
Some v
| false, _ ->
let result =
match d with
| Definition.Object obj ->
let requiredFields = obj.Required |> Option.defaultValue [] |> Set.ofList
let namedProperties =
obj.Properties
|> Option.map Seq.cast
|> Option.defaultValue Seq.empty
|> Seq.map (fun (KeyValue (fieldName, defn)) ->
// TODO this is a horrible hack and is incomplete, e.g. if we contain an array of ourself
// Special case for when this is a reference to this very type
let isOurself =
match defn with
| Definition.Handle h ->
match h.Split '/' with
| [| "#" ; location ; ty |] when location = thisKey && Some ty = typeName ->
SynType.named ty |> Some
| _ -> None
| _ -> None
let jsonPropertyName =
SynExpr.CreateConst (fieldName : string)
|> SynAttribute.create (
SynLongIdent.createS'
[ "System" ; "Text" ; "Json" ; "Serialization" ; "JsonPropertyName" ]
)
match isOurself with
| Some alreadyDone ->
let ty =
if Set.contains fieldName requiredFields then
alreadyDone
else
SynType.option alreadyDone
{
Attrs = [ jsonPropertyName ]
Type = ty
Ident = Some (Ident.createSanitisedTypeName fieldName)
}
|> SynField.make
|> Some
| None ->
let defn' = defnToType anonymousTypeCount handlesMap bigCache thisKey None defn
match defn' with
| None -> None
| Some defn' ->
let ty =
if Set.contains fieldName requiredFields then
defn'.Signature
else
defn'.Signature |> SynType.option
{
Attrs = [ jsonPropertyName ]
Ident = Ident.createSanitisedTypeName fieldName |> Some
Type = ty
}
|> SynField.make
|> Some
)
|> Seq.toList
let additionalProperties =
match obj.AdditionalProperties with
| None ->
{
Attrs =
[
SynAttribute.create
(SynLongIdent.createS'
[ "System" ; "Text" ; "Json" ; "Serialization" ; "JsonExtensionData" ])
(SynExpr.CreateConst ())
]
Ident = Ident.create "AdditionalProperties" |> Some
Type =
SynType.app'
(SynType.createLongIdent' [ "System" ; "Collections" ; "Generic" ; "Dictionary" ])
[
SynType.string
SynType.createLongIdent' [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonNode" ]
]
}
|> SynField.make
|> List.singleton
|> Some
| Some AdditionalProperties.Never -> Some []
| Some (AdditionalProperties.Constrained defn) ->
let defn' = defnToType anonymousTypeCount handlesMap bigCache thisKey None defn
match defn' with
| None -> None
| Some defn' ->
{
Attrs =
[
SynAttribute.create
(SynLongIdent.createS'
[ "System" ; "Text" ; "Json" ; "Serialization" ; "JsonExtensionData" ])
(SynExpr.CreateConst ())
]
Ident = Ident.create "AdditionalProperties" |> Some
Type =
SynType.app'
(SynType.createLongIdent'
[ "System" ; "Collections" ; "Generic" ; "Dictionary" ])
[ SynType.string ; defn'.Signature ]
}
|> SynField.make
|> List.singleton
|> Some
match additionalProperties with
| None -> None
| Some additionalProperties ->
match List.allSome namedProperties with
| None -> None
| Some namedProperties ->
let fSharpTypeName =
match typeName with
| None -> $"Type%i{Interlocked.Increment anonymousTypeCount}"
| Some typeName -> typeName
let properties = additionalProperties @ namedProperties
let properties =
if properties.IsEmpty then
// sigh, they didn't give us any properties at all; let's make one up
{
Attrs = []
Ident = Some (Ident.create "_SchemaUnspecified")
Type = SynType.obj
}
|> SynField.make
|> List.singleton
else
properties
let defn =
let sci =
SynComponentInfo.create (Ident.createSanitisedTypeName fSharpTypeName)
|> SynComponentInfo.addAttributes
[
SynAttribute.create (SynLongIdent.createS' [ "JsonParse" ]) (SynExpr.CreateConst true)
SynAttribute.create
(SynLongIdent.createS' [ "JsonSerialize" ])
(SynExpr.CreateConst true)
]
|> fun sci ->
match obj.Description with
| None -> sci
| Some doc -> sci |> SynComponentInfo.withDocString (PreXmlDoc.create doc)
properties |> SynTypeDefnRepr.record |> SynTypeDefn.create sci
let defn =
{
Signature = SynType.named fSharpTypeName
FSharpDefinition = Some defn
}
defn |> Some
| Definition.Array elt ->
let child = defnToType anonymousTypeCount handlesMap bigCache thisKey None elt.Items
match child with
| None -> None
| Some child ->
let defn =
{
Signature = SynType.list child.Signature
FSharpDefinition = None
}
Some defn
| Definition.String ->
{
Signature = SynType.string
FSharpDefinition = None
}
|> Some
| Definition.Boolean ->
{
Signature = SynType.bool
FSharpDefinition = None
}
|> Some
| Definition.Unspecified ->
{
Signature = SynType.unit
FSharpDefinition = None
}
|> Some
| Definition.Integer _ ->
{
Signature = SynType.createLongIdent' [ "int" ]
FSharpDefinition = None
}
|> Some
| Definition.File ->
{
Signature = SynType.createLongIdent' [ "System" ; "IO" ; "Stream" ]
FSharpDefinition = None
}
|> Some
| Definition.Handle s ->
let split = s.Split '/' |> List.ofArray
match split with
| [ "#" ; _location ; _handle ] ->
match handlesMap.TryGetValue s with
| false, _ -> None
| true, computed ->
let defn =
{
FSharpDefinition = None
Signature = computed.Signature
}
defn |> Some
| _ -> failwith $"we don't know how to deal with object handle %s{s}"
match result with
| None -> None
| Some result ->
match handleKey with
| None -> ()
| Some handleKey -> handlesMap.Add (handleKey, result)
cache.Add (d, result)
Some result
let instantiateRequiredTypes (types : Types) : SynModuleDecl =
types.ByDefinition
|> Seq.choose (fun (KeyValue (_defn, typeEntry)) -> typeEntry.FSharpDefinition)
|> Seq.toList
|> SynModuleDecl.createTypes
type private IsIn =
| Path of str : string
| Query of str : string
| Body
let computeType
(options : SwaggerClientConfig)
(basePath : string)
(types : Types)
(clientDocString : PreXmlDoc)
(endpoints : Endpoint list)
: SynModuleDecl list
=
endpoints
|> List.choose (fun ep ->
let name = (Ident.createSanitisedTypeName (ep.Operation.ToString ())).idText
match renderType types ep.ReturnType with
| None ->
log $"Skipping %O{ep.Operation}: Couldn't render return type: %O{ep.ReturnType}"
None
| Some returnType ->
let pars =
ep.Parameters
|> List.map (fun par ->
let inParam =
match par.In with
| ParameterIn.Unrecognised (f, name) ->
log
$"Skipping %O{ep.Operation} at %s{ep.Endpoint}: unrecognised In parameter %s{f} with name %s{name}"
None
| ParameterIn.Body -> Some IsIn.Body
| ParameterIn.Query name -> Some (IsIn.Query name)
| ParameterIn.Path name -> Some (IsIn.Path name)
match inParam with
| None -> None
| Some inParam ->
match renderType types par.Type with
| None ->
// Couldn't render the return type
// failwith "Did not have a type here"
log $"Skipping %O{ep.Operation}: Couldn't render parameter: %O{par.Type}"
None
| Some v -> Some (Ident.createSanitisedParamName par.Name, inParam, v)
)
|> List.allSome
match pars with
| None -> None
| Some pars ->
let arity =
SynValInfo.SynValInfo (
[
ep.Parameters
|> List.map (fun par ->
let name = par.Name |> Ident.create |> Some
SynArgInfo.SynArgInfo ([], false, name)
)
|> fun l -> l @ [ SynArgInfo.SynArgInfo ([], true, Some (Ident.create "ct")) ]
],
SynArgInfo.SynArgInfo ([], false, None)
)
let domain =
let ctParam =
SynType.signatureParamOfType
[]
(SynType.createLongIdent' [ "System" ; "Threading" ; "CancellationToken" ])
true
(Some (Ident.create "ct"))
let argParams =
pars
|> List.map (fun (ident, isIn, t) ->
let attr : SynAttribute list =
match isIn with
| IsIn.Path name ->
SynAttribute.create
(SynLongIdent.createS' [ "RestEase" ; "Path" ])
(SynExpr.CreateConst name)
|> List.singleton
| IsIn.Query name ->
SynAttribute.create
(SynLongIdent.createS' [ "RestEase" ; "Query" ])
(SynExpr.CreateConst name)
|> List.singleton
| IsIn.Body ->
SynAttribute.create
(SynLongIdent.createS' [ "RestEase" ; "Body" ])
(SynExpr.CreateConst ())
|> List.singleton
SynType.signatureParamOfType attr t false (Some ident)
)
SynType.tupleNoParen (argParams @ [ ctParam ]) |> Option.get
let attrs =
[
SynAttribute.create
(SynLongIdent.createS' [ "RestEase" ; ep.Method.ToString () ])
// Gitea, at least, starts with a `/`, which `Uri` then takes to indicate an absolute path.
(SynExpr.CreateConst (ep.Endpoint.TrimStart '/'))
match ep.Produces with
| Produces.Produces contentType ->
SynAttribute.create
(SynLongIdent.createS' [ "RestEase" ; "Header" ])
// Gitea, at least, starts with a `/`, which `Uri` then takes to indicate an absolute path.
(SynExpr.tuple [ SynExpr.CreateConst "Content-Type" ; SynExpr.CreateConst contentType ])
]
returnType
|> SynType.task
|> SynType.toFun [ domain ]
|> SynMemberDefn.abstractMember attrs (SynIdent.createS name) None arity ep.DocString
|> Some
)
|> SynTypeDefnRepr.interfaceType
|> SynTypeDefn.create (
let attrs =
[
yield SynAttribute.create (SynLongIdent.createS' [ "HttpClient" ]) (SynExpr.CreateConst false)
yield
SynAttribute.create
(SynLongIdent.createS' [ "RestEase" ; "BasePath" ])
(SynExpr.CreateConst basePath)
match options.CreateMock with
| None -> ()
| Some createMockValue ->
yield
SynAttribute.create
(SynLongIdent.createS' [ "GenerateMock" ])
(SynExpr.CreateConst createMockValue)
]
SynComponentInfo.create (Ident.create ("I" + options.ClassName))
|> SynComponentInfo.withDocString clientDocString
|> SynComponentInfo.addAttributes attrs
)
|> List.singleton
|> SynModuleDecl.createTypes
|> List.singleton
open Myriad.Core
/// Myriad generator that stamps out an interface and class to access a Swagger-specified API.
[<MyriadGenerator("swagger-client")>]
type SwaggerClientGenerator () =
interface IMyriadGenerator with
member _.ValidInputExtensions = [ ".json" ]
member _.Generate (context : GeneratorContext) =
let contents = File.ReadAllText context.InputFilename |> Swagger.parse
let scheme =
let preferred = Scheme "https"
if List.isEmpty contents.Schemes then
failwith "no schemes specified in API spec!"
if List.contains preferred contents.Schemes then
preferred
else
List.head contents.Schemes
let clientDocstring = contents.Info.Description |> PreXmlDoc.create
let basePath = contents.BasePath
let typeDefs =
let bigCache = Dictionary<_, Dictionary<_, _>> ()
let countAll () =
(0, bigCache) ||> Seq.fold (fun count (KeyValue (_, v)) -> count + v.Count)
let byHandle = Dictionary ()
let anonymousTypeCount = ref 0
let rec go (contents : ((string * Definition) * string) list) =
let lastRound = countAll ()
contents
|> List.filter (fun ((name, defn), defnClass) ->
let doIt =
SwaggerClientGenerator.defnToType
anonymousTypeCount
byHandle
bigCache
defnClass
(Some name)
defn
match doIt with
| None -> true
| Some _ -> false
)
|> fun remaining ->
if not remaining.IsEmpty then
let currentCount = countAll ()
if currentCount = lastRound then
for (name, remaining), kind in remaining do
SwaggerClientGenerator.log $"Remaining: %s{name} (%s{kind})"
SwaggerClientGenerator.log "--------"
for KeyValue (handle, defn) in byHandle do
SwaggerClientGenerator.log $"Known: %s{handle} %O{defn}"
// TODO: ohh noooooo the Gitea spec is genuinely circular,
// it's impossible to construct a Repository type
// we're going to have to somehow detect this case and break the cycle
// by artificially making a property optional
// :sob: Gitea why are you like this
// failwith "Made no further progress rendering types"
()
else
go remaining
seq {
for defnClass in [ "definitions" ; "responses" ] do
match defnClass with
| "definitions" ->
for KeyValue (k, v) in contents.Definitions do
yield (k, v), defnClass
| "responses" ->
for KeyValue (k, v) in contents.Responses do
yield (k, v.Schema), defnClass
| _ -> failwith "oh no"
}
|> Seq.toList
|> go
let result = Dictionary ()
for KeyValue (_container, types) in bigCache do
for KeyValue (defn, rendered) in types do
result.TryAdd (defn, rendered) |> ignore<bool>
{
ByHandle = byHandle
ByDefinition = result :> IReadOnlyDictionary<_, _>
}
let summary =
contents.Paths
|> Seq.collect (fun (KeyValue (path, endpoints)) ->
endpoints
|> Seq.choose (fun (KeyValue (method, endpoint)) ->
let docstring = endpoint.Summary |> PreXmlDoc.create
let produces =
match endpoint.Produces with
| None -> Produces "json"
| Some [] -> failwith $"API specified empty Produces: %s{path} (%O{method})"
| Some [ MimeType "application/json" ] -> Produces "json"
| Some [ MimeType (StartsWith "text/" t) ] -> Produces t
| Some [ MimeType s ] ->
failwithf
$"we don't support non-JSON Produces right now, got: %s{s} (%s{path} %O{method})"
| Some (_ :: _) ->
failwith $"we don't support multiple Produces right now, at %s{path} (%O{method})"
let returnType =
endpoint.Responses
|> Seq.choose (fun (KeyValue (response, defn)) ->
if 200 <= response && response < 300 then
Some defn
else
None
)
|> Seq.toList
let returnType =
match returnType with
| [ t ] -> Some t
| [] -> failwith $"got no successful response results, %s{path} %O{method}"
| _ ->
SwaggerClientGenerator.log
$"Ignoring %s{path} %O{method} due to multiple success responses"
// can't be bothered to work out how to deal with multiple success
// results right now
None
match returnType with
| None -> None
| Some returnType ->
{
Method = method
Produces = produces
DocString = docstring
ReturnType = returnType
Operation = endpoint.OperationId
Parameters = endpoint.Parameters |> Option.defaultValue []
Endpoint = path
}
|> Some
)
|> Seq.toList
)
|> Seq.toList
let config =
let pars = MyriadParamParser.render context.AdditionalParameters
let pars =
pars
|> Map.toSeq
|> Seq.map (fun (k, v) -> k.ToUpperInvariant (), v)
|> Map.ofSeq
if pars.IsEmpty then
failwith "No parameters given. You must supply the <ClassName /> parameter in <MyriadParams />."
let createMock =
match Map.tryFind "GENERATEMOCKVISIBILITY" pars with
| None -> None
| Some v ->
match v.ToLowerInvariant () with
| "internal" -> Some true
| "public" -> Some false
| _ ->
failwith
$"Expected GenerateMockVisibility parameter to be 'internal' or 'public', but was: '%s{v.ToLowerInvariant ()}'"
let className =
match Map.tryFind "CLASSNAME" pars with
| None -> failwith "You must supply the <ClassName /> parameter in <MyriadParams />."
| Some v -> v
{
CreateMock = createMock
ClassName = className
}
let ty =
SwaggerClientGenerator.computeType config basePath typeDefs clientDocstring summary
[
yield
SynModuleDecl.Open (
SynOpenDeclTarget.ModuleOrNamespace (
SynLongIdent.createS' [ "WoofWare" ; "Myriad" ; "Plugins" ],
range0
),
range0
)
yield SwaggerClientGenerator.instantiateRequiredTypes typeDefs
yield! ty
]
|> SynModuleOrNamespace.createNamespace [ Ident.create config.ClassName ]
|> List.singleton
|> Output.Ast

View File

@@ -2,6 +2,7 @@ namespace WoofWare.Myriad.Plugins
open System
open System.Text
open System.Text.RegularExpressions
open Fantomas.FCS.Syntax
open Fantomas.FCS.Text.Range
@@ -9,6 +10,54 @@ open Fantomas.FCS.Text.Range
module internal Ident =
let inline create (s : string) = Ident (s, range0)
/// Fantomas bug, perhaps? "type" is not rendered as ``type``, although the ASTs are identical
/// apart from the ranges?
/// Awful hack: here is a function that does this sort of thing.
let createSanitisedParamName (s : string) =
match s with
| "type" -> create "type'"
| "private" -> create "private'"
| _ ->
let result = StringBuilder ()
for i = 0 to s.Length - 1 do
if Char.IsLetter s.[i] then
result.Append s.[i] |> ignore<StringBuilder>
elif Char.IsNumber s.[i] then
if result.Length > 0 then
result.Append s.[i] |> ignore<StringBuilder>
elif s.[i] = '_' || s.[i] = '-' then
result.Append '_' |> ignore<StringBuilder>
else
failwith $"could not convert to ident: %s{s}"
create (result.ToString ())
let private alnum = Regex @"^[a-zA-Z][a-zA-Z0-9]*$"
let createSanitisedTypeName (s : string) =
let result = StringBuilder ()
let mutable capitalize = true
for i = 0 to s.Length - 1 do
if Char.IsLetter s.[i] then
if capitalize then
result.Append (Char.ToUpperInvariant s.[i]) |> ignore<StringBuilder>
capitalize <- false
else
result.Append s.[i] |> ignore<StringBuilder>
elif Char.IsNumber s.[i] then
if result.Length > 0 then
result.Append s.[i] |> ignore<StringBuilder>
elif s.[i] = '_' then
capitalize <- true
if result.Length = 0 then
failwith $"String %s{s} was not suitable as a type identifier"
Ident (result.ToString (), range0)
let lowerFirstLetter (x : Ident) : Ident =
let result = StringBuilder x.idText.Length
result.Append (Char.ToLowerInvariant x.idText.[0]) |> ignore

View File

@@ -6,7 +6,12 @@ open Fantomas.FCS.Text.Range
[<RequireQualifiedAccess>]
module internal PreXmlDoc =
let create (s : string) : PreXmlDoc =
PreXmlDoc.Create ([| " " + s |], range0)
let s = s.Split "\n"
for i = 0 to s.Length - 1 do
s.[i] <- " " + s.[i]
PreXmlDoc.Create (s, range0)
let create' (s : string seq) : PreXmlDoc =
PreXmlDoc.Create (Array.ofSeq s, range0)

View File

@@ -9,12 +9,12 @@ module internal SynArgPats =
match caseNames.Length with
| 0 -> SynArgPats.Pats []
| 1 ->
SynPat.Named (SynIdent.SynIdent (Ident.create caseNames.[0], None), false, None, range0)
SynPat.Named (SynIdent.createS caseNames.[0], false, None, range0)
|> List.singleton
|> SynArgPats.Pats
| len ->
caseNames
|> List.map (fun name -> SynPat.Named (SynIdent.SynIdent (Ident.create name, None), false, None, range0))
|> List.map (fun name -> SynPat.Named (SynIdent.createS name, false, None, range0))
|> fun t -> SynPat.Tuple (false, t, List.replicate (len - 1) range0, range0)
|> fun t -> SynPat.Paren (t, range0)
|> List.singleton

View File

@@ -5,32 +5,23 @@ open Fantomas.FCS.Text.Range
[<RequireQualifiedAccess>]
module internal SynAttribute =
let internal compilationRepresentation : SynAttribute =
let inline create (typeName : SynLongIdent) (arg : SynExpr) : SynAttribute =
{
TypeName = SynLongIdent.createS "CompilationRepresentation"
ArgExpr =
[ "CompilationRepresentationFlags" ; "ModuleSuffix" ]
|> SynExpr.createLongIdent
|> SynExpr.paren
TypeName = typeName
ArgExpr = arg
Target = None
AppliesToGetterAndSetter = false
Range = range0
}
let internal compilationRepresentation : SynAttribute =
[ "CompilationRepresentationFlags" ; "ModuleSuffix" ]
|> SynExpr.createLongIdent
|> SynExpr.paren
|> create (SynLongIdent.createS "CompilationRepresentation")
let internal requireQualifiedAccess : SynAttribute =
{
TypeName = SynLongIdent.createS "RequireQualifiedAccess"
ArgExpr = SynExpr.CreateConst ()
Target = None
AppliesToGetterAndSetter = false
Range = range0
}
create (SynLongIdent.createS "RequireQualifiedAccess") (SynExpr.CreateConst ())
let internal autoOpen : SynAttribute =
{
TypeName = SynLongIdent.createS "AutoOpen"
ArgExpr = SynExpr.CreateConst ()
Target = None
AppliesToGetterAndSetter = false
Range = range0
}
create (SynLongIdent.createS "AutoOpen") (SynExpr.CreateConst ())

View File

@@ -85,6 +85,11 @@ module internal SynExpr =
SynExpr.CreateAppInfix (SynExpr.CreateLongIdent SynLongIdent.booleanAnd, a)
|> applyTo b
/// {a} || {b}
let booleanOr (a : SynExpr) (b : SynExpr) =
SynExpr.CreateAppInfix (SynExpr.CreateLongIdent SynLongIdent.booleanOr, a)
|> applyTo b
/// {a} + {b}
let plus (a : SynExpr) (b : SynExpr) =
SynExpr.CreateAppInfix (
@@ -136,16 +141,15 @@ module internal SynExpr =
let typeApp (types : SynType list) (operand : SynExpr) =
SynExpr.TypeApp (operand, range0, types, List.replicate (types.Length - 1) range0, Some range0, range0, range0)
let callGenericMethod (meth : string) (ty : LongIdent) (obj : SynExpr) : SynExpr =
SynExpr.DotGet (obj, range0, SynLongIdent.createS meth, range0)
|> typeApp [ SynType.LongIdent (SynLongIdent.create ty) ]
/// {obj}.{meth}<types,...>()
let callGenericMethod (meth : SynLongIdent) (types : SynType list) (obj : SynExpr) : SynExpr =
SynExpr.DotGet (obj, range0, meth, range0)
|> typeApp types
|> applyTo (SynExpr.CreateConst ())
/// {obj}.{meth}<ty>()
let callGenericMethod' (meth : string) (ty : string) (obj : SynExpr) : SynExpr =
SynExpr.DotGet (obj, range0, SynLongIdent.createS meth, range0)
|> typeApp [ SynType.createLongIdent' [ ty ] ]
|> applyTo (SynExpr.CreateConst ())
callGenericMethod (SynLongIdent.createS meth) [ SynType.createLongIdent' [ ty ] ] obj
let inline index (property : SynExpr) (obj : SynExpr) : SynExpr =
SynExpr.DotIndexedGet (obj, property, range0, range0)
@@ -237,6 +241,8 @@ module internal SynExpr =
let inline createLet (bindings : SynBinding list) (body : SynExpr) : SynExpr =
SynExpr.LetOrUse (false, false, bindings, body, range0, SynExprLetOrUseTrivia.empty)
let inline createDo (body : SynExpr) : SynExpr = SynExpr.Do (body, range0)
let inline createMatch (matchOn : SynExpr) (cases : SynMatchClause list) : SynExpr =
SynExpr.Match (
DebugPointAtBinding.Yes range0,
@@ -368,3 +374,6 @@ module internal SynExpr =
|> paren
let assign (lhs : SynLongIdent) (rhs : SynExpr) : SynExpr = SynExpr.LongIdentSet (lhs, rhs, range0)
let assignIndex (lhs : SynExpr) (index : SynExpr) (rhs : SynExpr) : SynExpr =
SynExpr.DotIndexedSet (lhs, index, rhs, range0, range0, range0)

View File

@@ -0,0 +1,10 @@
namespace WoofWare.Myriad.Plugins
open Fantomas.FCS.Syntax
[<RequireQualifiedAccess>]
module internal SynIdent =
let inline createI (i : Ident) : SynIdent = SynIdent.SynIdent (i, None)
let inline createS (i : string) : SynIdent =
SynIdent.SynIdent (Ident.create i, None)

View File

@@ -36,6 +36,9 @@ module internal SynLongIdent =
let booleanAnd =
SynLongIdent.SynLongIdent ([ Ident.create "op_BooleanAnd" ], [], [ Some (IdentTrivia.OriginalNotation "&&") ])
let booleanOr =
SynLongIdent.SynLongIdent ([ Ident.create "op_BooleanOr" ], [], [ Some (IdentTrivia.OriginalNotation "||") ])
let pipe =
SynLongIdent.SynLongIdent ([ Ident.create "op_PipeRight" ], [], [ Some (IdentTrivia.OriginalNotation "|>") ])

View File

@@ -17,8 +17,8 @@ module internal SynMemberDefn =
SynMemberFlags.MemberKind = SynMemberKind.Member
}
let abstractMember
(attrs : SynAttribute list)
(ident : SynIdent)
(typars : SynTyparDecls option)
(arity : SynValInfo)
@@ -28,7 +28,13 @@ module internal SynMemberDefn =
=
let slot =
SynValSig.SynValSig (
[],
attrs
|> List.map (fun attr ->
{
Attributes = [ attr ]
Range = range0
}
),
ident,
SynValTyparDecls.SynValTyparDecls (typars, true),
returnType,

View File

@@ -181,6 +181,29 @@ module internal SynTypePatterns =
_) -> Some (ident, outer)
| _ -> None
let (|JsonNode|_|) (fieldType : SynType) : unit option =
match fieldType with
| SynType.LongIdent (SynLongIdent.SynLongIdent (ident, _, _)) ->
match ident |> List.map (fun i -> i.idText) with
| [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonNode" ]
| [ "Text" ; "Json" ; "Nodes" ; "JsonNode" ]
| [ "Json" ; "Nodes" ; "JsonNode" ]
| [ "Nodes" ; "JsonNode" ]
| [ "JsonNode" ] -> Some ()
| _ -> None
| _ -> None
let (|Unit|_|) (fieldType : SynType) : unit option =
match fieldType with
| SynType.LongIdent (SynLongIdent.SynLongIdent (ident, _, _)) ->
match ident |> List.map (fun i -> i.idText.ToLowerInvariant ()) with
| [ "microsoft" ; "fsharp" ; "core" ; "unit" ]
| [ "fsharp" ; "core" ; "unit" ]
| [ "core" ; "unit" ]
| [ "unit" ] -> Some ()
| _ -> None
| _ -> None
let (|DateOnly|_|) (fieldType : SynType) =
match fieldType with
| SynType.LongIdent (SynLongIdent.SynLongIdent (ident, _, _)) ->
@@ -267,6 +290,8 @@ module internal SynType =
| SynType.Paren (ty, _) -> stripOptionalParen ty
| ty -> ty
let inline paren (ty : SynType) : SynType = SynType.Paren (ty, range0)
let inline createLongIdent (ident : LongIdent) : SynType =
SynType.LongIdent (SynLongIdent.create ident)
@@ -283,6 +308,17 @@ module internal SynType =
let inline app (name : string) (args : SynType list) : SynType = app' (named name) args
/// Returns None if the input list was empty.
let inline tupleNoParen (ty : SynType list) : SynType option =
match List.rev ty with
| [] -> None
| [ t ] -> Some t
| t :: rest ->
([ SynTupleTypeSegment.Type t ], rest)
||> List.fold (fun ty nextArg -> SynTupleTypeSegment.Type nextArg :: SynTupleTypeSegment.Star range0 :: ty)
|> fun segs -> SynType.Tuple (false, segs, range0)
|> Some
let inline appPostfix (name : string) (arg : SynType) : SynType =
SynType.App (named name, None, [ arg ], [], None, true, range0)
@@ -299,16 +335,54 @@ module internal SynType =
}
)
let inline signatureParamOfType (ty : SynType) (name : Ident option) : SynType =
SynType.SignatureParameter ([], false, name, ty, range0)
let inline signatureParamOfType
(attrs : SynAttribute list)
(ty : SynType)
(optional : bool)
(name : Ident option)
: SynType
=
SynType.SignatureParameter (
attrs
|> List.map (fun attr ->
{
Attributes = [ attr ]
Range = range0
}
),
optional,
name,
ty,
range0
)
let inline var (ty : SynTypar) : SynType = SynType.Var (ty, range0)
let unit : SynType = named "unit"
let obj : SynType = named "obj"
let bool : SynType = named "bool"
let int : SynType = named "int"
let array (elt : SynType) : SynType = SynType.Array (1, elt, range0)
let list (elt : SynType) : SynType =
SynType.App (named "list", None, [ elt ], [], None, true, range0)
let option (elt : SynType) : SynType =
SynType.App (named "option", None, [ elt ], [], None, true, range0)
let anon : SynType = SynType.Anon range0
let task (elt : SynType) : SynType =
SynType.App (
createLongIdent' [ "System" ; "Threading" ; "Tasks" ; "Task" ],
None,
[ elt ],
[],
None,
true,
range0
)
let string : SynType = named "string"
/// Given ['a1, 'a2] and 'ret, returns 'a1 -> 'a2 -> 'ret.

View File

@@ -25,3 +25,7 @@ module internal SynTypeDefn =
match r with
| SynTypeDefn (typeInfo, typeRepr, _, ctor, range, trivia) ->
SynTypeDefn.SynTypeDefn (typeInfo, typeRepr, members, ctor, range, trivia)
let getName (defn : SynTypeDefn) : LongIdent =
match defn with
| SynTypeDefn (SynComponentInfo.SynComponentInfo (_, _, _, id, _, _, _, _), _, _, _, _, _) -> id

View File

@@ -44,7 +44,7 @@ module internal SynUnionCase =
SynUnionCase.SynUnionCase (
SynAttributes.ofAttrs case.Attributes,
SynIdent.SynIdent (case.Name, None),
SynIdent.createI case.Name,
SynUnionCaseKind.Fields fields,
case.XmlDoc |> Option.defaultValue PreXmlDoc.Empty,
case.Access,

View File

@@ -0,0 +1,11 @@
namespace WoofWare.Myriad.Plugins
open System
[<AutoOpen>]
module internal Text =
let (|StartsWith|_|) (prefix : string) (s : string) : string option =
if s.StartsWith (prefix, StringComparison.Ordinal) then
Some (s.Substring prefix.Length)
else
None

View File

@@ -25,11 +25,13 @@
<ItemGroup>
<Compile Include="List.fs"/>
<Compile Include="Text.fs" />
<Compile Include="Teq.fs" />
<Compile Include="Primitives.fs" />
<Compile Include="SynExpr\SynAttributes.fs" />
<Compile Include="SynExpr\PreXmlDoc.fs" />
<Compile Include="SynExpr\Ident.fs" />
<Compile Include="SynExpr\SynIdent.fs" />
<Compile Include="SynExpr\SynLongIdent.fs" />
<Compile Include="SynExpr\SynExprLetOrUseTrivia.fs" />
<Compile Include="SynExpr\SynArgPats.fs" />
@@ -51,12 +53,15 @@
<Compile Include="Measure.fs" />
<Compile Include="AstHelper.fs" />
<Compile Include="RemoveOptionsGenerator.fs"/>
<Compile Include="MyriadParamParser.fs" />
<Compile Include="InterfaceMockGenerator.fs"/>
<Compile Include="JsonSerializeGenerator.fs"/>
<Compile Include="JsonParseGenerator.fs"/>
<Compile Include="HttpClientGenerator.fs"/>
<Compile Include="CataGenerator.fs" />
<Compile Include="ArgParserGenerator.fs" />
<Compile Include="Swagger.fs" />
<Compile Include="SwaggerClientGenerator.fs" />
<None Include="TeqLicence.txt" />
<EmbeddedResource Include="version.json"/>
<EmbeddedResource Include="SurfaceBaseline.txt"/>

View File

@@ -1,5 +1,5 @@
{
"version": "2.3",
"version": "3.1",
"publicReleaseRefSpec": [
"^refs/heads/main$"
],
@@ -11,4 +11,4 @@
":/README.md",
":/Directory.Build.props"
]
}
}

12
flake.lock generated
View File

@@ -5,11 +5,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"type": "github"
},
"original": {
@@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1725534445,
"narHash": "sha256-Yd0FK9SkWy+ZPuNqUgmVPXokxDgMJoGuNpMEtkfcf84=",
"lastModified": 1727524699,
"narHash": "sha256-k6YxGj08voz9NvuKExojiGXAVd69M8COtqWSKr6sQS4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9bb1e7571aadf31ddb4af77fc64b2d59580f9a39",
"rev": "b5b2fecd0cadd82ef107c9583018f381ae70f222",
"type": "github"
},
"original": {

View File

@@ -52,7 +52,7 @@
projectFile = "./WoofWare.Myriad.Plugins/WoofWare.Myriad.Plugins.fsproj";
testProjectFile = "./WoofWare.Myriad.Plugins.Test/WoofWare.Myriad.Plugins.Test.fsproj";
disabledTests = ["WoofWare.Myriad.Plugins.Test.TestSurface.CheckVersionAgainstRemote"];
nugetDeps = ./nix/deps.nix; # `nix build .#default.passthru.fetch-deps && ./result` and put the result here
nugetDeps = ./nix/deps.nix; # `nix build .#default.passthru.fetch-deps && ./result nix/deps.nix`
doCheck = true;
};
};

View File

@@ -8,8 +8,8 @@
})
(fetchNuGet {
pname = "fantomas";
version = "6.3.12";
hash = "sha256-LFZn2cO72FlsmLI0vTLz52Bn4XBeGILTOr8rz/EuXeg=";
version = "6.3.15";
hash = "sha256-Gjw7MxjUNckMWSfnOye4UTe5fZWnor6RHCls3PNsuG8=";
})
(fetchNuGet {
pname = "Fantomas.Core";
@@ -48,8 +48,8 @@
})
(fetchNuGet {
pname = "FsUnit";
version = "6.0.0";
hash = "sha256-q87WQf6MqGhzvaQ7WkkUlCdoE94DY0CD5PaXEj64A6M=";
version = "6.0.1";
hash = "sha256-vka/aAgWhDCl5tu+kgO7GtSaHOOvlSaWxG+tExwGXpI=";
})
(fetchNuGet {
pname = "Microsoft.AspNetCore.App.Ref";