Compare commits

..

3 Commits

Author SHA1 Message Date
Patrick Stevens
bdce82fb7a Handle env vars with value 0 and 1 for bools (#241) 2024-09-04 21:06:22 +01:00
Patrick Stevens
8f9f933971 Handle URI in arg parser (#240) 2024-09-04 20:20:51 +01:00
Patrick Stevens
3a55ba1242 Allow positional args to be Choice<'a, 'a> to indicate whether they came before any positional marker (#238) 2024-09-04 20:09:40 +01:00
8 changed files with 626 additions and 152 deletions

1
.gitignore vendored
View File

@@ -11,3 +11,4 @@ result
analysis.sarif analysis.sarif
.direnv/ .direnv/
.venv/ .venv/
.vs/

View File

@@ -111,7 +111,7 @@ type ChildRecordWithPositional =
{ {
Thing1 : int Thing1 : int
[<PositionalArgs>] [<PositionalArgs>]
Thing2 : string list Thing2 : Uri list
} }
[<ArgParser true>] [<ArgParser true>]
@@ -128,3 +128,17 @@ type ParentRecordSelfPos =
[<PositionalArgs>] [<PositionalArgs>]
AndAnother : bool list AndAnother : bool list
} }
[<ArgParser true>]
type ChoicePositionals =
{
[<PositionalArgs>]
Args : Choice<string, string> list
}
[<ArgParser true>]
type ContainsBoolEnvVar =
{
[<ArgumentDefaultEnvironmentVariable "CONSUMEPLUGIN_THINGS">]
BoolVar : Choice<bool, bool>
}

View File

@@ -228,8 +228,8 @@ module Basic =
(sprintf "--bar string%s%s" "" "") (sprintf "--bar string%s%s" "" "")
(sprintf "--baz bool%s%s" "" "") (sprintf "--baz bool%s%s" "" "")
(sprintf (sprintf
"--rest string (positional args)%s%s" "--rest string%s%s"
" (can be repeated)" " (positional args) (can be repeated)"
(sprintf " : %s" ("Here's where the rest of the args go"))) (sprintf " : %s" ("Here's where the rest of the args go")))
] ]
|> String.concat "\n" |> String.concat "\n"
@@ -415,7 +415,7 @@ module BasicWithIntPositionals =
(sprintf "--foo int32%s%s" "" "") (sprintf "--foo int32%s%s" "" "")
(sprintf "--bar string%s%s" "" "") (sprintf "--bar string%s%s" "" "")
(sprintf "--baz bool%s%s" "" "") (sprintf "--baz bool%s%s" "" "")
(sprintf "--rest int32 (positional args)%s%s" " (can be repeated)" "") (sprintf "--rest int32%s%s" " (positional args) (can be repeated)" "")
] ]
|> String.concat "\n" |> String.concat "\n"
@@ -619,7 +619,7 @@ module LoadsOfTypes =
"--yet-another-optional-thing string%s%s" "--yet-another-optional-thing string%s%s"
("CONSUMEPLUGIN_THINGS" |> sprintf " (default value populated from env var %s)") ("CONSUMEPLUGIN_THINGS" |> sprintf " (default value populated from env var %s)")
"") "")
(sprintf "--positionals int32 (positional args)%s%s" " (can be repeated)" "") (sprintf "--positionals int32%s%s" " (positional args) (can be repeated)" "")
] ]
|> String.concat "\n" |> String.concat "\n"
@@ -1801,11 +1801,11 @@ module ParentRecordChildPosArgParse =
[ [
(sprintf "--and-another bool%s%s" "" "") (sprintf "--and-another bool%s%s" "" "")
(sprintf "--thing1 int32%s%s" "" "") (sprintf "--thing1 int32%s%s" "" "")
(sprintf "--thing2 string (positional args)%s%s" " (can be repeated)" "") (sprintf "--thing2 URI%s%s" " (positional args) (can be repeated)" "")
] ]
|> String.concat "\n" |> String.concat "\n"
let arg_1 : string ResizeArray = ResizeArray () let arg_1 : Uri ResizeArray = ResizeArray ()
let mutable arg_2 : bool option = None let mutable arg_2 : bool option = None
let mutable arg_0 : int option = None let mutable arg_0 : int option = None
@@ -1840,7 +1840,7 @@ module ParentRecordChildPosArgParse =
with _ as exc -> with _ as exc ->
exc.Message |> Some |> Error exc.Message |> Some |> Error
else if System.String.Equals (key, "--thing2", System.StringComparison.OrdinalIgnoreCase) then else if System.String.Equals (key, "--thing2", System.StringComparison.OrdinalIgnoreCase) then
value |> (fun x -> x) |> arg_1.Add value |> (fun x -> System.Uri x) |> arg_1.Add
() |> Ok () |> Ok
else else
Error None Error None
@@ -1873,7 +1873,7 @@ module ParentRecordChildPosArgParse =
"Trailing argument %s had no value. Use a double-dash to separate positional args from key-value args." "Trailing argument %s had no value. Use a double-dash to separate positional args from key-value args."
key key
|> ArgParser_errors.Add |> ArgParser_errors.Add
| "--" :: rest -> arg_1.AddRange (rest |> Seq.map (fun x -> x)) | "--" :: rest -> arg_1.AddRange (rest |> Seq.map (fun x -> System.Uri x))
| arg :: args -> | arg :: args ->
match state with match state with
| ParseState_ParentRecordChildPos.AwaitingKey -> | ParseState_ParentRecordChildPos.AwaitingKey ->
@@ -1897,7 +1897,7 @@ module ParentRecordChildPosArgParse =
sprintf "%s (at arg %s)" msg arg |> ArgParser_errors.Add sprintf "%s (at arg %s)" msg arg |> ArgParser_errors.Add
go ParseState_ParentRecordChildPos.AwaitingKey args go ParseState_ParentRecordChildPos.AwaitingKey args
else else
arg |> (fun x -> x) |> arg_1.Add arg |> (fun x -> System.Uri x) |> arg_1.Add
go ParseState_ParentRecordChildPos.AwaitingKey args go ParseState_ParentRecordChildPos.AwaitingKey args
| ParseState_ParentRecordChildPos.AwaitingValue key -> | ParseState_ParentRecordChildPos.AwaitingValue key ->
match processKeyValue key arg with match processKeyValue key arg with
@@ -1972,7 +1972,7 @@ module ParentRecordSelfPosArgParse =
[ [
(sprintf "--thing1 int32%s%s" "" "") (sprintf "--thing1 int32%s%s" "" "")
(sprintf "--thing2 string%s%s" "" "") (sprintf "--thing2 string%s%s" "" "")
(sprintf "--and-another bool (positional args)%s%s" " (can be repeated)" "") (sprintf "--and-another bool%s%s" " (positional args) (can be repeated)" "")
] ]
|> String.concat "\n" |> String.concat "\n"
@@ -2108,3 +2108,271 @@ module ParentRecordSelfPosArgParse =
static member parse (args : string list) : ParentRecordSelfPos = static member parse (args : string list) : ParentRecordSelfPos =
ParentRecordSelfPos.parse' System.Environment.GetEnvironmentVariable args ParentRecordSelfPos.parse' System.Environment.GetEnvironmentVariable args
namespace ConsumePlugin
open System
open System.IO
open WoofWare.Myriad.Plugins
/// Methods to parse arguments for the type ChoicePositionals
[<AutoOpen>]
module ChoicePositionalsArgParse =
type private ParseState_ChoicePositionals =
| AwaitingKey
| AwaitingValue of key : string
/// Extension methods for argument parsing
type ChoicePositionals with
static member parse' (getEnvironmentVariable : string -> string) (args : string list) : ChoicePositionals =
let ArgParser_errors = ResizeArray ()
let helpText () =
[ (sprintf "--args string%s%s" " (positional args) (can be repeated)" "") ]
|> String.concat "\n"
let arg_0 : Choice<string, string> ResizeArray = ResizeArray ()
/// 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, "--args", System.StringComparison.OrdinalIgnoreCase) then
value |> (fun x -> x) |> Choice1Of2 |> arg_0.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_ChoicePositionals) (args : string list) =
match args with
| [] ->
match state with
| ParseState_ChoicePositionals.AwaitingKey -> ()
| ParseState_ChoicePositionals.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_0.AddRange (rest |> Seq.map (fun x -> x) |> Seq.map Choice2Of2)
| arg :: args ->
match state with
| ParseState_ChoicePositionals.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_ChoicePositionals.AwaitingValue arg)
else
let key = arg.[0 .. equals - 1]
let value = arg.[equals + 1 ..]
match processKeyValue key value with
| Ok () -> go ParseState_ChoicePositionals.AwaitingKey args
| Error None ->
failwithf "Unable to process argument %s as key %s and value %s" arg key value
| Error (Some msg) ->
sprintf "%s (at arg %s)" msg arg |> ArgParser_errors.Add
go ParseState_ChoicePositionals.AwaitingKey args
else
arg |> (fun x -> x) |> Choice1Of2 |> arg_0.Add
go ParseState_ChoicePositionals.AwaitingKey args
| ParseState_ChoicePositionals.AwaitingValue key ->
match processKeyValue key arg with
| Ok () -> go ParseState_ChoicePositionals.AwaitingKey args
| Error exc ->
if setFlagValue key then
go ParseState_ChoicePositionals.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_ChoicePositionals.AwaitingKey args
let arg_0 = arg_0 |> Seq.toList
if 0 = ArgParser_errors.Count then
{
Args = arg_0
}
else
ArgParser_errors |> String.concat "\n" |> failwithf "Errors during parse!\n%s"
static member parse (args : string list) : ChoicePositionals =
ChoicePositionals.parse' System.Environment.GetEnvironmentVariable args
namespace ConsumePlugin
open System
open System.IO
open WoofWare.Myriad.Plugins
/// Methods to parse arguments for the type ContainsBoolEnvVar
[<AutoOpen>]
module ContainsBoolEnvVarArgParse =
type private ParseState_ContainsBoolEnvVar =
| AwaitingKey
| AwaitingValue of key : string
/// Extension methods for argument parsing
type ContainsBoolEnvVar with
static member parse' (getEnvironmentVariable : string -> string) (args : string list) : ContainsBoolEnvVar =
let ArgParser_errors = ResizeArray ()
let helpText () =
[
(sprintf
"--bool-var bool%s%s"
("CONSUMEPLUGIN_THINGS" |> sprintf " (default value populated from env var %s)")
"")
]
|> String.concat "\n"
let parser_LeftoverArgs : string ResizeArray = ResizeArray ()
let mutable arg_0 : bool 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, "--bool-var", System.StringComparison.OrdinalIgnoreCase) then
match arg_0 with
| Some x ->
sprintf "Argument '%s' was supplied multiple times: %O and %O" "--bool-var" x value
|> ArgParser_errors.Add
Ok ()
| None ->
try
arg_0 <- value |> (fun x -> System.Boolean.Parse x) |> Some
Ok ()
with _ as exc ->
exc.Message |> Some |> Error
else
Error None
/// Returns false if we didn't set a value.
let setFlagValue (key : string) : bool =
if System.String.Equals (key, "--bool-var", System.StringComparison.OrdinalIgnoreCase) then
match arg_0 with
| Some x ->
sprintf "Flag '%s' was supplied multiple times" "--bool-var"
|> ArgParser_errors.Add
true
| None ->
arg_0 <- Some true
true
else
false
let rec go (state : ParseState_ContainsBoolEnvVar) (args : string list) =
match args with
| [] ->
match state with
| ParseState_ContainsBoolEnvVar.AwaitingKey -> ()
| ParseState_ContainsBoolEnvVar.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 -> parser_LeftoverArgs.AddRange (rest |> Seq.map (fun x -> x))
| arg :: args ->
match state with
| ParseState_ContainsBoolEnvVar.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_ContainsBoolEnvVar.AwaitingValue arg)
else
let key = arg.[0 .. equals - 1]
let value = arg.[equals + 1 ..]
match processKeyValue key value with
| Ok () -> go ParseState_ContainsBoolEnvVar.AwaitingKey args
| Error None ->
failwithf "Unable to process argument %s as key %s and value %s" arg key value
| Error (Some msg) ->
sprintf "%s (at arg %s)" msg arg |> ArgParser_errors.Add
go ParseState_ContainsBoolEnvVar.AwaitingKey args
else
arg |> (fun x -> x) |> parser_LeftoverArgs.Add
go ParseState_ContainsBoolEnvVar.AwaitingKey args
| ParseState_ContainsBoolEnvVar.AwaitingValue key ->
match processKeyValue key arg with
| Ok () -> go ParseState_ContainsBoolEnvVar.AwaitingKey args
| Error exc ->
if setFlagValue key then
go ParseState_ContainsBoolEnvVar.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_ContainsBoolEnvVar.AwaitingKey args
let parser_LeftoverArgs =
if 0 = parser_LeftoverArgs.Count then
()
else
parser_LeftoverArgs
|> String.concat " "
|> sprintf "There were leftover args: %s"
|> ArgParser_errors.Add
Unchecked.defaultof<_>
let arg_0 =
match arg_0 with
| None ->
match "CONSUMEPLUGIN_THINGS" |> getEnvironmentVariable with
| null ->
sprintf
"No value was supplied for %s, nor was environment variable %s set"
"--bool-var"
"CONSUMEPLUGIN_THINGS"
|> ArgParser_errors.Add
Unchecked.defaultof<_>
| x ->
if System.String.Equals (x, "1", System.StringComparison.OrdinalIgnoreCase) then
true
else if System.String.Equals (x, "0", System.StringComparison.OrdinalIgnoreCase) then
false
else
x |> (fun x -> System.Boolean.Parse x)
|> Choice2Of2
| Some x -> Choice1Of2 x
if 0 = ArgParser_errors.Count then
{
BoolVar = arg_0
}
else
ArgParser_errors |> String.concat "\n" |> failwithf "Errors during parse!\n%s"
static member parse (args : string list) : ContainsBoolEnvVar =
ContainsBoolEnvVar.parse' System.Environment.GetEnvironmentVariable args

View File

@@ -1,10 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject> <IsTestProject>true</IsTestProject>
<!--
Known high severity vulnerability
I have not yet seen a single instance where I care about this warning
-->
<NoWarn>$(NoWarn),NU1903</NoWarn>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -367,18 +367,19 @@ Required argument '--exact' received no value"""
let parsed = let parsed =
ParentRecordChildPos.parse' ParentRecordChildPos.parse'
getEnvVar getEnvVar
[ "--and-another=true" ; "--thing1=9" ; "--thing2=some" ; "--thing2=thing" ] [
"--and-another=true"
"--thing1=9"
"--thing2=https://example.com"
"--thing2=http://example.com"
]
parsed parsed.AndAnother |> shouldEqual true
|> shouldEqual parsed.Child.Thing1 |> shouldEqual 9
{
Child = parsed.Child.Thing2
{ |> List.map (fun (x : Uri) -> x.ToString ())
Thing1 = 9 |> shouldEqual [ "https://example.com/" ; "http://example.com/" ]
Thing2 = [ "some" ; "thing" ]
}
AndAnother = true
}
[<Test>] [<Test>]
let ``Can consume stacked record, child has no positionals, parent has positionals`` () = let ``Can consume stacked record, child has no positionals, parent has positionals`` () =
@@ -421,3 +422,40 @@ Required argument '--exact' received no value"""
--thing1 int32 --thing1 int32
--thing2 string --thing2 string
--and-another bool (positional args) (can be repeated)""" --and-another bool (positional args) (can be repeated)"""
[<Test>]
let ``Positionals are tagged with Choice`` () =
let getEnvVar (_ : string) = failwith "should not call"
ChoicePositionals.parse' getEnvVar [ "a" ; "b" ; "--" ; "--c" ; "--help" ]
|> shouldEqual
{
Args = [ Choice1Of2 "a" ; Choice1Of2 "b" ; Choice2Of2 "--c" ; Choice2Of2 "--help" ]
}
[<TestCase("1", true)>]
[<TestCase("0", false)>]
[<TestCase("true", true)>]
[<TestCase("false", false)>]
[<TestCase("TRUE", true)>]
[<TestCase("FALSE", false)>]
let ``Bool env vars can be populated`` (envValue : string, boolValue : bool) =
let getEnvVar (s : string) =
s |> shouldEqual "CONSUMEPLUGIN_THINGS"
envValue
ContainsBoolEnvVar.parse' getEnvVar []
|> shouldEqual
{
BoolVar = Choice2Of2 boolValue
}
[<Test>]
let ``Bools can be treated with arity 0`` () =
let getEnvVar (_ : string) = failwith "do not call"
ContainsBoolEnvVar.parse' getEnvVar [ "--bool-var" ]
|> shouldEqual
{
BoolVar = Choice1Of2 true
}

View File

@@ -4,6 +4,11 @@
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject> <IsTestProject>true</IsTestProject>
<!--
Known high severity vulnerability
I have not yet seen a single instance where I care about this warning
-->
<NoWarn>$(NoWarn),NU1903</NoWarn>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -22,13 +22,13 @@ type private ArgumentDefaultSpec =
/// we would use `MyArgs.DefaultThing () : int`. /// we would use `MyArgs.DefaultThing () : int`.
| FunctionCall of name : Ident | FunctionCall of name : Ident
type private Accumulation = type private Accumulation<'choice> =
| Required | Required
| Optional | Optional
| Choice of ArgumentDefaultSpec | Choice of 'choice
| List | List of Accumulation<'choice>
type private ParseFunction = type private ParseFunction<'acc> =
{ {
FieldName : Ident FieldName : Ident
TargetVariable : Ident TargetVariable : Ident
@@ -42,21 +42,25 @@ type private ParseFunction =
/// If `Accumulation` is `List`, then this is the type of the list *element*; analogously for optionals /// If `Accumulation` is `List`, then this is the type of the list *element*; analogously for optionals
/// and choices and so on. /// and choices and so on.
TargetType : SynType TargetType : SynType
Accumulation : Accumulation Accumulation : 'acc
} }
[<RequireQualifiedAccess>]
type private ChoicePositional =
| Normal
| Choice
type private ParseFunctionPositional = ParseFunction<ChoicePositional>
type private ParseFunctionNonPositional = ParseFunction<Accumulation<ArgumentDefaultSpec>>
type private ParserSpec = type private ParserSpec =
{ {
NonPositionals : ParseFunction list NonPositionals : ParseFunctionNonPositional list
/// The variable into which positional arguments will be accumulated. /// The variable into which positional arguments will be accumulated.
/// In this case, the TargetVariable is a `ResizeArray` rather than the usual `option`. /// In this case, the TargetVariable is a `ResizeArray` rather than the usual `option`.
Positionals : ParseFunction option Positionals : ParseFunctionPositional option
} }
type private ArgToParse =
| Positional of ParseFunction
| NonPositional of ParseFunction
type private HasPositional = HasPositional type private HasPositional = HasPositional
type private HasNoPositional = HasNoPositional type private HasNoPositional = HasNoPositional
@@ -67,8 +71,8 @@ module private TeqUtils =
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
type private ParseTree<'hasPositional> = type private ParseTree<'hasPositional> =
| NonPositionalLeaf of ParseFunction * Teq<'hasPositional, HasNoPositional> | NonPositionalLeaf of ParseFunctionNonPositional * Teq<'hasPositional, HasNoPositional>
| PositionalLeaf of ParseFunction * Teq<'hasPositional, HasPositional> | PositionalLeaf of ParseFunctionPositional * Teq<'hasPositional, HasPositional>
/// `assemble` takes the SynExpr's (e.g. each record field contents) corresponding to each `Ident` in /// `assemble` takes the SynExpr's (e.g. each record field contents) corresponding to each `Ident` in
/// the branch (e.g. each record field name), /// the branch (e.g. each record field name),
/// and composes them into a `SynExpr` (e.g. the record-typed object). /// and composes them into a `SynExpr` (e.g. the record-typed object).
@@ -142,7 +146,7 @@ module private ParseTree =
go None ([], None) subs go None ([], None) subs
let rec accumulatorsNonPos (tree : ParseTree<HasNoPositional>) : ParseFunction list = let rec accumulatorsNonPos (tree : ParseTree<HasNoPositional>) : ParseFunctionNonPositional list =
match tree with match tree with
| ParseTree.PositionalLeaf (_, teq) -> exFalso teq | ParseTree.PositionalLeaf (_, teq) -> exFalso teq
| ParseTree.BranchPos (_, _, _, _, teq) -> exFalso teq | ParseTree.BranchPos (_, _, _, _, teq) -> exFalso teq
@@ -150,7 +154,10 @@ module private ParseTree =
| ParseTree.Branch (trees, _, _) -> trees |> List.collect (snd >> accumulatorsNonPos) | ParseTree.Branch (trees, _, _) -> trees |> List.collect (snd >> accumulatorsNonPos)
/// Returns the positional arg separately. /// Returns the positional arg separately.
let rec accumulatorsPos (tree : ParseTree<HasPositional>) : ParseFunction list * ParseFunction = let rec accumulatorsPos
(tree : ParseTree<HasPositional>)
: ParseFunctionNonPositional list * ParseFunctionPositional
=
match tree with match tree with
| ParseTree.PositionalLeaf (pf, _) -> [], pf | ParseTree.PositionalLeaf (pf, _) -> [], pf
| ParseTree.NonPositionalLeaf (_, teq) -> exFalso' teq | ParseTree.NonPositionalLeaf (_, teq) -> exFalso' teq
@@ -164,7 +171,7 @@ module private ParseTree =
/// Collect all the ParseFunctions which are necessary to define variables, throwing away /// Collect all the ParseFunctions which are necessary to define variables, throwing away
/// all information relevant to composing the resulting variables into records. /// all information relevant to composing the resulting variables into records.
/// Returns the list of non-positional parsers, and any positional parser that exists. /// Returns the list of non-positional parsers, and any positional parser that exists.
let accumulators<'a> (tree : ParseTree<'a>) : ParseFunction list * ParseFunction option = let accumulators<'a> (tree : ParseTree<'a>) : ParseFunctionNonPositional list * ParseFunctionPositional option =
// Sad duplication of some code here, but it was the easiest way to make it type-safe :( // Sad duplication of some code here, but it was the easiest way to make it type-safe :(
match tree with match tree with
| ParseTree.PositionalLeaf (pf, _) -> [], Some pf | ParseTree.PositionalLeaf (pf, _) -> [], Some pf
@@ -178,8 +185,7 @@ module private ParseTree =
|> fun (nonPos, pos) -> |> fun (nonPos, pos) ->
let duplicateArgs = let duplicateArgs =
Option.toList pos @ nonPos Option.toList (pos |> Option.map _.ArgForm) @ (nonPos |> List.map _.ArgForm)
|> List.map (fun pf -> pf.ArgForm)
|> List.groupBy id |> List.groupBy id
|> List.choose (fun (key, v) -> if v.Length > 1 then Some key else None) |> List.choose (fun (key, v) -> if v.Length > 1 then Some key else None)
@@ -234,11 +240,12 @@ module internal ArgParserGenerator =
/// for example, maybe it returns a `ty option` or a `ty list`). /// for example, maybe it returns a `ty option` or a `ty list`).
/// The resulting SynType is the type of the *element* being parsed; so if the Accumulation is List, the SynType /// The resulting SynType is the type of the *element* being parsed; so if the Accumulation is List, the SynType
/// is the list element. /// is the list element.
let rec private createParseFunction let rec private createParseFunction<'choice>
(choice : ArgumentDefaultSpec option -> 'choice)
(fieldName : Ident) (fieldName : Ident)
(attrs : SynAttribute list) (attrs : SynAttribute list)
(ty : SynType) (ty : SynType)
: SynExpr * Accumulation * SynType : SynExpr * Accumulation<'choice> * SynType
= =
match ty with match ty with
| String -> SynExpr.createLambda "x" (SynExpr.createIdent "x"), Accumulation.Required, SynType.string | String -> SynExpr.createLambda "x" (SynExpr.createIdent "x"), Accumulation.Required, SynType.string
@@ -250,6 +257,12 @@ module internal ArgParserGenerator =
(SynExpr.createIdent "x")), (SynExpr.createIdent "x")),
Accumulation.Required, Accumulation.Required,
ty ty
| Uri ->
SynExpr.createLambda
"x"
(SynExpr.applyFunction (SynExpr.createLongIdent [ "System" ; "Uri" ]) (SynExpr.createIdent "x")),
Accumulation.Required,
ty
| TimeSpan -> | TimeSpan ->
let parseExact = let parseExact =
attrs attrs
@@ -321,7 +334,7 @@ module internal ArgParserGenerator =
Accumulation.Required, Accumulation.Required,
ty ty
| OptionType eltTy -> | OptionType eltTy ->
let parseElt, acc, childTy = createParseFunction fieldName attrs eltTy let parseElt, acc, childTy = createParseFunction choice fieldName attrs eltTy
match acc with match acc with
| Accumulation.Optional -> | Accumulation.Optional ->
@@ -330,7 +343,7 @@ module internal ArgParserGenerator =
| Accumulation.Choice _ -> | Accumulation.Choice _ ->
failwith failwith
$"ArgParser does not support optionals containing choices at field %s{fieldName.idText}: %O{ty}" $"ArgParser does not support optionals containing choices at field %s{fieldName.idText}: %O{ty}"
| Accumulation.List -> | Accumulation.List _ ->
failwith $"ArgParser does not support optional lists at field %s{fieldName.idText}: %O{ty}" failwith $"ArgParser does not support optional lists at field %s{fieldName.idText}: %O{ty}"
| Accumulation.Required -> parseElt, Accumulation.Optional, childTy | Accumulation.Required -> parseElt, Accumulation.Optional, childTy
| ChoiceType elts -> | ChoiceType elts ->
@@ -340,13 +353,13 @@ module internal ArgParserGenerator =
failwith failwith
$"ArgParser was unable to prove types %O{elt1} and %O{elt2} to be equal in a Choice. We require them to be equal." $"ArgParser was unable to prove types %O{elt1} and %O{elt2} to be equal in a Choice. We require them to be equal."
let parseElt, acc, childTy = createParseFunction fieldName attrs elt1 let parseElt, acc, childTy = createParseFunction choice fieldName attrs elt1
match acc with match acc with
| Accumulation.Optional -> | Accumulation.Optional ->
failwith failwith
$"ArgParser does not support choices containing options at field %s{fieldName.idText}: %O{ty}" $"ArgParser does not support choices containing options at field %s{fieldName.idText}: %O{ty}"
| Accumulation.List -> | Accumulation.List _ ->
failwith failwith
$"ArgParser does not support choices containing lists at field %s{fieldName.idText}: %O{ty}" $"ArgParser does not support choices containing lists at field %s{fieldName.idText}: %O{ty}"
| Accumulation.Choice _ -> | Accumulation.Choice _ ->
@@ -384,31 +397,22 @@ module internal ArgParserGenerator =
let relevantAttr = let relevantAttr =
match relevantAttrs with match relevantAttrs with
| [] -> | [] -> None
failwith | [ x ] -> Some x
$"Expected Choice to be annotated with ArgumentDefaultFunction or similar, but it was not. Field: %s{fieldName.idText}"
| [ x ] -> x
| _ -> | _ ->
failwith failwith
$"Expected Choice to be annotated with exactly one ArgumentDefaultFunction or similar, but it was annotated with multiple. Field: %s{fieldName.idText}" $"Expected Choice to be annotated with at most one ArgumentDefaultFunction or similar, but it was annotated with multiple. Field: %s{fieldName.idText}"
parseElt, Accumulation.Choice relevantAttr, childTy parseElt, Accumulation.Choice (choice relevantAttr), childTy
| elts -> | elts ->
let elts = elts |> List.map string<SynType> |> String.concat ", " let elts = elts |> List.map string<SynType> |> String.concat ", "
failwith failwith
$"ArgParser requires Choice to be of the form Choice<'a, 'a>; that is, two arguments, both the same. For field %s{fieldName.idText}, got: %s{elts}" $"ArgParser requires Choice to be of the form Choice<'a, 'a>; that is, two arguments, both the same. For field %s{fieldName.idText}, got: %s{elts}"
| ListType eltTy -> | ListType eltTy ->
let parseElt, acc, childTy = createParseFunction fieldName attrs eltTy let parseElt, acc, childTy = createParseFunction choice fieldName attrs eltTy
match acc with parseElt, Accumulation.List acc, childTy
| Accumulation.List ->
failwith $"ArgParser does not support nested lists at field %s{fieldName.idText}: %O{ty}"
| Accumulation.Choice _ ->
failwith $"ArgParser does not support lists containing choices at field %s{fieldName.idText}: %O{ty}"
| Accumulation.Optional ->
failwith $"ArgParser does not support lists of options at field %s{fieldName.idText}: %O{ty}"
| Accumulation.Required -> parseElt, Accumulation.List, childTy
| _ -> failwith $"Could not decide how to parse arguments for field %s{fieldName.idText} of type %O{ty}" | _ -> failwith $"Could not decide how to parse arguments for field %s{fieldName.idText} of type %O{ty}"
let rec private toParseSpec let rec private toParseSpec
@@ -489,25 +493,61 @@ module internal ArgParserGenerator =
counter, (ident, spec) :: acc counter, (ident, spec) :: acc
| None -> | None ->
let parser, accumulation, parseTy = createParseFunction ident attrs fieldType
match positionalArgAttr with match positionalArgAttr with
| Some _ -> | Some _ ->
let getChoice (spec : ArgumentDefaultSpec option) : unit =
match spec with
| Some _ ->
failwith
"Positional Choice args cannot have default values. Remove [<ArgumentDefault*>] from the positional arg."
| None -> ()
let parser, accumulation, parseTy =
createParseFunction<unit> getChoice ident attrs fieldType
match accumulation with match accumulation with
| Accumulation.List -> | Accumulation.List (Accumulation.List _) ->
failwith "A list of positional args cannot contain lists."
| Accumulation.List Accumulation.Optional ->
failwith "A list of positional args cannot contain optionals. What would that even mean?"
| Accumulation.List (Accumulation.Choice ()) ->
{ {
FieldName = ident FieldName = ident
Parser = parser Parser = parser
TargetVariable = Ident.create $"arg_%i{counter}" TargetVariable = Ident.create $"arg_%i{counter}"
Accumulation = accumulation Accumulation = ChoicePositional.Choice
TargetType = parseTy TargetType = parseTy
ArgForm = argify ident ArgForm = argify ident
Help = helpText Help = helpText
} }
|> fun t -> ParseTree.PositionalLeaf (t, Teq.refl) |> fun t -> ParseTree.PositionalLeaf (t, Teq.refl)
|> ParseTreeCrate.make | Accumulation.List Accumulation.Required ->
| _ -> failwith $"Expected positional arg accumulation type to be List, but it was %O{fieldType}" {
FieldName = ident
Parser = parser
TargetVariable = Ident.create $"arg_%i{counter}"
Accumulation = ChoicePositional.Normal
TargetType = parseTy
ArgForm = argify ident
Help = helpText
}
|> fun t -> ParseTree.PositionalLeaf (t, Teq.refl)
| Accumulation.Choice _
| Accumulation.Optional
| Accumulation.Required ->
failwith $"Expected positional arg accumulation type to be List, but it was %O{fieldType}"
|> ParseTreeCrate.make
| None -> | None ->
let getChoice (spec : ArgumentDefaultSpec option) : ArgumentDefaultSpec =
match spec with
| None ->
failwith
"Non-positional Choice args must have an `[<ArgumentDefault*>]` attribute on them."
| Some spec -> spec
let parser, accumulation, parseTy =
createParseFunction getChoice ident attrs fieldType
{ {
FieldName = ident FieldName = ident
Parser = parser Parser = parser
@@ -537,11 +577,36 @@ module internal ArgParserGenerator =
/// let helpText : string = ... /// let helpText : string = ...
let private helpText let private helpText
(typeName : Ident) (typeName : Ident)
(positional : ParseFunction option) (positional : ParseFunctionPositional option)
(args : ParseFunction list) (args : ParseFunctionNonPositional list)
: SynBinding : SynBinding
= =
let toPrintable (prefix : string) (arg : ParseFunction) : SynExpr = let describeNonPositional (acc : Accumulation<ArgumentDefaultSpec>) : SynExpr =
match acc with
| Accumulation.Required -> SynExpr.CreateConst ""
| Accumulation.Optional -> SynExpr.CreateConst " (optional)"
| Accumulation.Choice (ArgumentDefaultSpec.EnvironmentVariable var) ->
// We don't print out the default value in case it's a secret. People often pass secrets
// through env vars!
var
|> SynExpr.pipeThroughFunction (
SynExpr.applyFunction
(SynExpr.createIdent "sprintf")
(SynExpr.CreateConst " (default value populated from env var %s)")
)
|> SynExpr.paren
| Accumulation.Choice (ArgumentDefaultSpec.FunctionCall var) ->
SynExpr.callMethod var.idText (SynExpr.createIdent' typeName)
|> SynExpr.pipeThroughFunction (
SynExpr.applyFunction (SynExpr.createIdent "sprintf") (SynExpr.CreateConst " (default value: %O)")
)
|> SynExpr.paren
| Accumulation.List _ -> SynExpr.CreateConst " (can be repeated)"
let describePositional _ =
SynExpr.CreateConst " (positional args) (can be repeated)"
let toPrintable (describe : 'a -> SynExpr) (arg : ParseFunction<'a>) : SynExpr =
let ty = arg.TargetType |> SynType.toHumanReadableString let ty = arg.TargetType |> SynType.toHumanReadableString
let helpText = let helpText =
@@ -552,31 +617,9 @@ module internal ArgParserGenerator =
|> SynExpr.applyTo (SynExpr.paren helpText) |> SynExpr.applyTo (SynExpr.paren helpText)
|> SynExpr.paren |> SynExpr.paren
let descriptor = let descriptor = describe arg.Accumulation
match arg.Accumulation with
| Accumulation.Required -> SynExpr.CreateConst ""
| Accumulation.Optional -> SynExpr.CreateConst " (optional)"
| Accumulation.Choice (ArgumentDefaultSpec.EnvironmentVariable var) ->
// We don't print out the default value in case it's a secret. People often pass secrets
// through env vars!
var
|> SynExpr.pipeThroughFunction (
SynExpr.applyFunction
(SynExpr.createIdent "sprintf")
(SynExpr.CreateConst " (default value populated from env var %s)")
)
|> SynExpr.paren
| Accumulation.Choice (ArgumentDefaultSpec.FunctionCall var) ->
SynExpr.callMethod var.idText (SynExpr.createIdent' typeName)
|> SynExpr.pipeThroughFunction (
SynExpr.applyFunction
(SynExpr.createIdent "sprintf")
(SynExpr.CreateConst " (default value: %O)")
)
|> SynExpr.paren
| Accumulation.List -> SynExpr.CreateConst " (can be repeated)"
let prefix = $"%s{arg.ArgForm} %s{ty}%s{prefix}" let prefix = $"%s{arg.ArgForm} %s{ty}"
SynExpr.applyFunction (SynExpr.createIdent "sprintf") (SynExpr.CreateConst (prefix + "%s%s")) SynExpr.applyFunction (SynExpr.createIdent "sprintf") (SynExpr.CreateConst (prefix + "%s%s"))
|> SynExpr.applyTo descriptor |> SynExpr.applyTo descriptor
@@ -584,11 +627,11 @@ module internal ArgParserGenerator =
|> SynExpr.paren |> SynExpr.paren
args args
|> List.map (toPrintable "") |> List.map (toPrintable describeNonPositional)
|> fun l -> |> fun l ->
match positional with match positional with
| None -> l | None -> l
| Some pos -> l @ [ toPrintable " (positional args)" pos ] | Some pos -> l @ [ toPrintable describePositional pos ]
|> SynExpr.listLiteral |> SynExpr.listLiteral
|> SynExpr.pipeThroughFunction ( |> SynExpr.pipeThroughFunction (
SynExpr.applyFunction (SynExpr.createLongIdent [ "String" ; "concat" ]) (SynExpr.CreateConst @"\n") SynExpr.applyFunction (SynExpr.createLongIdent [ "String" ; "concat" ]) (SynExpr.CreateConst @"\n")
@@ -599,69 +642,106 @@ module internal ArgParserGenerator =
/// Returns a possible error. /// Returns a possible error.
/// A parse failure might not be fatal (e.g. maybe the input was optionally of arity 0, and we failed to do /// A parse failure might not be fatal (e.g. maybe the input was optionally of arity 0, and we failed to do
/// the parse because in fact the key decided not to take this argument); in that case we return Error None. /// the parse because in fact the key decided not to take this argument); in that case we return Error None.
let private processKeyValue (argParseErrors : Ident) (args : ParseFunction list) : SynBinding = let private processKeyValue
(SynExpr.applyFunction (SynExpr.createIdent "Error") (SynExpr.createIdent "None"), args) (argParseErrors : Ident)
||> List.fold (fun finalBranch arg -> (pos : ParseFunctionPositional option)
match arg.Accumulation with (args : ParseFunctionNonPositional list)
| Accumulation.Required : SynBinding
| Accumulation.Choice _ =
| Accumulation.Optional -> let args =
let multipleErrorMessage = args
SynExpr.createIdent "sprintf" |> List.map (fun arg ->
|> SynExpr.applyTo (SynExpr.CreateConst "Argument '%s' was supplied multiple times: %O and %O") match arg.Accumulation with
|> SynExpr.applyTo (SynExpr.CreateConst arg.ArgForm) | Accumulation.Required
|> SynExpr.applyTo (SynExpr.createIdent "x") | Accumulation.Choice _
|> SynExpr.applyTo (SynExpr.createIdent "value") | Accumulation.Optional ->
let multipleErrorMessage =
SynExpr.createIdent "sprintf"
|> SynExpr.applyTo (SynExpr.CreateConst "Argument '%s' was supplied multiple times: %O and %O")
|> SynExpr.applyTo (SynExpr.CreateConst arg.ArgForm)
|> SynExpr.applyTo (SynExpr.createIdent "x")
|> SynExpr.applyTo (SynExpr.createIdent "value")
let performAssignment = let performAssignment =
[
SynExpr.createIdent "value"
|> SynExpr.pipeThroughFunction arg.Parser
|> SynExpr.pipeThroughFunction (SynExpr.createIdent "Some")
|> SynExpr.assign (SynLongIdent.createI arg.TargetVariable)
SynExpr.applyFunction (SynExpr.createIdent "Ok") (SynExpr.CreateConst ())
]
|> SynExpr.sequential
[
SynMatchClause.create
(SynPat.nameWithArgs "Some" [ SynPat.named "x" ])
(SynExpr.sequential
[
multipleErrorMessage
|> SynExpr.pipeThroughFunction (
SynExpr.dotGet "Add" (SynExpr.createIdent' argParseErrors)
)
SynExpr.applyFunction (SynExpr.createIdent "Ok") (SynExpr.CreateConst ())
])
SynMatchClause.create
(SynPat.named "None")
(SynExpr.pipeThroughTryWith
SynPat.anon
(SynExpr.createLongIdent [ "exc" ; "Message" ]
|> SynExpr.pipeThroughFunction (SynExpr.createIdent "Some")
|> SynExpr.pipeThroughFunction (SynExpr.createIdent "Error"))
performAssignment)
]
|> SynExpr.createMatch (SynExpr.createIdent' arg.TargetVariable)
| Accumulation.List (Accumulation.List _)
| Accumulation.List Accumulation.Optional
| Accumulation.List (Accumulation.Choice _) ->
failwith
"WoofWare.Myriad invariant violated: expected a list to contain only a Required accumulation. Non-positional lists cannot be optional or Choice, nor can they themselves contain lists."
| Accumulation.List Accumulation.Required ->
[ [
SynExpr.createIdent "value" SynExpr.createIdent "value"
|> SynExpr.pipeThroughFunction arg.Parser |> SynExpr.pipeThroughFunction arg.Parser
|> SynExpr.pipeThroughFunction (SynExpr.createIdent "Some") |> SynExpr.pipeThroughFunction (
|> SynExpr.assign (SynLongIdent.createI arg.TargetVariable) SynExpr.createLongIdent' [ arg.TargetVariable ; Ident.create "Add" ]
)
SynExpr.applyFunction (SynExpr.createIdent "Ok") (SynExpr.CreateConst ()) SynExpr.CreateConst () |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Ok")
] ]
|> SynExpr.sequential |> SynExpr.sequential
|> fun expr -> arg.ArgForm, expr
)
[ let posArg =
SynMatchClause.create match pos with
(SynPat.nameWithArgs "Some" [ SynPat.named "x" ]) | None -> []
(SynExpr.sequential | Some pos ->
[
multipleErrorMessage
|> SynExpr.pipeThroughFunction (
SynExpr.dotGet "Add" (SynExpr.createIdent' argParseErrors)
)
SynExpr.applyFunction (SynExpr.createIdent "Ok") (SynExpr.CreateConst ())
])
SynMatchClause.create
(SynPat.named "None")
(SynExpr.pipeThroughTryWith
SynPat.anon
(SynExpr.createLongIdent [ "exc" ; "Message" ]
|> SynExpr.pipeThroughFunction (SynExpr.createIdent "Some")
|> SynExpr.pipeThroughFunction (SynExpr.createIdent "Error"))
performAssignment)
]
|> SynExpr.createMatch (SynExpr.createIdent' arg.TargetVariable)
| Accumulation.List ->
[ [
SynExpr.createIdent "value" SynExpr.createIdent "value"
|> SynExpr.pipeThroughFunction arg.Parser |> SynExpr.pipeThroughFunction pos.Parser
|> fun p ->
match pos.Accumulation with
| ChoicePositional.Choice -> p |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Choice1Of2")
| ChoicePositional.Normal -> p
|> SynExpr.pipeThroughFunction ( |> SynExpr.pipeThroughFunction (
SynExpr.createLongIdent' [ arg.TargetVariable ; Ident.create "Add" ] SynExpr.createLongIdent' [ pos.TargetVariable ; Ident.create "Add" ]
) )
SynExpr.CreateConst () |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Ok") SynExpr.CreateConst () |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Ok")
] ]
|> SynExpr.sequential |> SynExpr.sequential
|> fun expr -> pos.ArgForm, expr
|> List.singleton
(SynExpr.applyFunction (SynExpr.createIdent "Error") (SynExpr.createIdent "None"), posArg @ args)
||> List.fold (fun finalBranch (argForm, arg) ->
arg
|> SynExpr.ifThenElse |> SynExpr.ifThenElse
(SynExpr.applyFunction (SynExpr.applyFunction
(SynExpr.createLongIdent [ "System" ; "String" ; "Equals" ]) (SynExpr.createLongIdent [ "System" ; "String" ; "Equals" ])
(SynExpr.tuple (SynExpr.tuple
[ [
SynExpr.createIdent "key" SynExpr.createIdent "key"
SynExpr.CreateConst arg.ArgForm SynExpr.CreateConst argForm
SynExpr.createLongIdent [ "System" ; "StringComparison" ; "OrdinalIgnoreCase" ] SynExpr.createLongIdent [ "System" ; "StringComparison" ; "OrdinalIgnoreCase" ]
])) ]))
finalBranch finalBranch
@@ -685,7 +765,7 @@ module internal ArgParserGenerator =
) )
/// `let setFlagValue (key : string) : bool = ...` /// `let setFlagValue (key : string) : bool = ...`
let private setFlagValue (argParseErrors : Ident) (flags : ParseFunction list) : SynBinding = let private setFlagValue (argParseErrors : Ident) (flags : ParseFunction<'a> list) : SynBinding =
(SynExpr.CreateConst false, flags) (SynExpr.CreateConst false, flags)
||> List.fold (fun finalExpr flag -> ||> List.fold (fun finalExpr flag ->
let multipleErrorMessage = let multipleErrorMessage =
@@ -734,6 +814,7 @@ module internal ArgParserGenerator =
let private mainLoop let private mainLoop
(parseState : Ident) (parseState : Ident)
(errorAcc : Ident) (errorAcc : Ident)
(leftoverArgAcc : ChoicePositional)
(leftoverArgs : Ident) (leftoverArgs : Ident)
(leftoverArgParser : SynExpr) (leftoverArgParser : SynExpr)
: SynBinding : SynBinding
@@ -780,6 +861,11 @@ module internal ArgParserGenerator =
[ [
SynExpr.createIdent "arg" SynExpr.createIdent "arg"
|> SynExpr.pipeThroughFunction leftoverArgParser |> SynExpr.pipeThroughFunction leftoverArgParser
|> fun p ->
match leftoverArgAcc with
| ChoicePositional.Normal -> p
| ChoicePositional.Choice ->
p |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Choice1Of2")
|> SynExpr.pipeThroughFunction (SynExpr.createLongIdent' [ leftoverArgs ; Ident.create "Add" ]) |> SynExpr.pipeThroughFunction (SynExpr.createLongIdent' [ leftoverArgs ; Ident.create "Add" ])
recurseKey recurseKey
@@ -945,6 +1031,16 @@ module internal ArgParserGenerator =
|> SynExpr.pipeThroughFunction ( |> SynExpr.pipeThroughFunction (
SynExpr.applyFunction (SynExpr.createLongIdent [ "Seq" ; "map" ]) leftoverArgParser SynExpr.applyFunction (SynExpr.createLongIdent [ "Seq" ; "map" ]) leftoverArgParser
) )
|> fun p ->
match leftoverArgAcc with
| ChoicePositional.Normal -> p
| ChoicePositional.Choice ->
p
|> SynExpr.pipeThroughFunction (
SynExpr.applyFunction
(SynExpr.createLongIdent [ "Seq" ; "map" ])
(SynExpr.createIdent "Choice2Of2")
)
)) ))
(SynExpr.createIdent' leftoverArgs)) (SynExpr.createIdent' leftoverArgs))
SynMatchClause.create (SynPat.listCons (SynPat.named "arg") (SynPat.named "args")) argBody SynMatchClause.create (SynPat.listCons (SynPat.named "arg") (SynPat.named "args")) argBody
@@ -983,7 +1079,12 @@ module internal ArgParserGenerator =
|> SynBinding.basic [ pf.TargetVariable ] [] |> SynBinding.basic [ pf.TargetVariable ] []
|> SynBinding.withMutability true |> SynBinding.withMutability true
|> SynBinding.withReturnAnnotation (SynType.appPostfix "option" pf.TargetType) |> SynBinding.withReturnAnnotation (SynType.appPostfix "option" pf.TargetType)
| Accumulation.List -> | Accumulation.List (Accumulation.List _)
| Accumulation.List Accumulation.Optional
| Accumulation.List (Accumulation.Choice _) ->
failwith
"WoofWare.Myriad invariant violated: expected a list to contain only a Required accumulation. Non-positional lists cannot be optional or Choice, nor can they themselves contain lists."
| Accumulation.List Accumulation.Required ->
SynExpr.createIdent "ResizeArray" SynExpr.createIdent "ResizeArray"
|> SynExpr.applyTo (SynExpr.CreateConst ()) |> SynExpr.applyTo (SynExpr.CreateConst ())
|> SynBinding.basic [ pf.TargetVariable ] [] |> SynBinding.basic [ pf.TargetVariable ] []
@@ -997,7 +1098,11 @@ module internal ArgParserGenerator =
Ident.create "parser_LeftoverArgs", Ident.create "parser_LeftoverArgs",
(SynExpr.createLambda "x" (SynExpr.createIdent "x")), (SynExpr.createLambda "x" (SynExpr.createIdent "x")),
SynType.string SynType.string
| Some pf -> pf.TargetVariable, pf.Parser, pf.TargetType | Some pf ->
match pf.Accumulation with
| ChoicePositional.Choice ->
pf.TargetVariable, pf.Parser, SynType.app "Choice" [ pf.TargetType ; pf.TargetType ]
| ChoicePositional.Normal -> pf.TargetVariable, pf.Parser, pf.TargetType
let bindings = let bindings =
SynExpr.createIdent "ResizeArray" SynExpr.createIdent "ResizeArray"
@@ -1036,6 +1141,36 @@ module internal ArgParserGenerator =
name name
|> SynExpr.pipeThroughFunction (SynExpr.createIdent "getEnvironmentVariable") |> SynExpr.pipeThroughFunction (SynExpr.createIdent "getEnvironmentVariable")
/// Assumes access to a non-null variable `x` containing the string value.
let parser =
match pf.TargetType with
| PrimitiveType ident when ident |> List.map _.idText = [ "System" ; "Boolean" ] ->
// We permit environment variables to be populated with 0 and 1 as well.
SynExpr.ifThenElse
(SynExpr.applyFunction
(SynExpr.createLongIdent [ "System" ; "String" ; "Equals" ])
(SynExpr.tuple
[
SynExpr.createIdent "x"
SynExpr.CreateConst "1"
SynExpr.createLongIdent
[ "System" ; "StringComparison" ; "OrdinalIgnoreCase" ]
]))
(SynExpr.ifThenElse
(SynExpr.applyFunction
(SynExpr.createLongIdent [ "System" ; "String" ; "Equals" ])
(SynExpr.tuple
[
SynExpr.createIdent "x"
SynExpr.CreateConst "0"
SynExpr.createLongIdent
[ "System" ; "StringComparison" ; "OrdinalIgnoreCase" ]
]))
(SynExpr.createIdent "x" |> SynExpr.pipeThroughFunction pf.Parser)
(SynExpr.CreateConst false))
(SynExpr.CreateConst true)
| _ -> (SynExpr.createIdent "x" |> SynExpr.pipeThroughFunction pf.Parser)
let errorMessage = let errorMessage =
SynExpr.createIdent "sprintf" SynExpr.createIdent "sprintf"
|> SynExpr.applyTo ( |> SynExpr.applyTo (
@@ -1057,9 +1192,7 @@ module internal ArgParserGenerator =
unchecked unchecked
]) ])
SynMatchClause.create SynMatchClause.create (SynPat.named "x") parser
(SynPat.named "x")
(SynExpr.createIdent "x" |> SynExpr.pipeThroughFunction pf.Parser)
] ]
|> SynExpr.createMatch result |> SynExpr.createMatch result
| ArgumentDefaultSpec.FunctionCall name -> | ArgumentDefaultSpec.FunctionCall name ->
@@ -1078,7 +1211,12 @@ module internal ArgParserGenerator =
|> SynBinding.basic [ pf.TargetVariable ] [] |> SynBinding.basic [ pf.TargetVariable ] []
| Accumulation.Optional -> | Accumulation.Optional ->
SynBinding.basic [ pf.TargetVariable ] [] (SynExpr.createIdent' pf.TargetVariable) SynBinding.basic [ pf.TargetVariable ] [] (SynExpr.createIdent' pf.TargetVariable)
| Accumulation.List -> | Accumulation.List (Accumulation.List _)
| Accumulation.List Accumulation.Optional
| Accumulation.List (Accumulation.Choice _) ->
failwith
"WoofWare.Myriad invariant violated: expected a list to contain only a Required accumulation. Non-positional lists cannot be optional or Choice, nor can they themselves contain lists."
| Accumulation.List Accumulation.Required ->
SynBinding.basic SynBinding.basic
[ pf.TargetVariable ] [ pf.TargetVariable ]
[] []
@@ -1176,6 +1314,11 @@ module internal ArgParserGenerator =
| _ -> false | _ -> false
) )
let leftoverArgAcc =
match pos with
| None -> ChoicePositional.Normal
| Some pos -> pos.Accumulation
[ [
SynExpr.createIdent "go" SynExpr.createIdent "go"
|> SynExpr.applyTo (SynExpr.createLongIdent' [ parseState ; Ident.create "AwaitingKey" ]) |> SynExpr.applyTo (SynExpr.createLongIdent' [ parseState ; Ident.create "AwaitingKey" ])
@@ -1187,9 +1330,9 @@ module internal ArgParserGenerator =
|> SynExpr.createLet ( |> SynExpr.createLet (
bindings bindings
@ [ @ [
processKeyValue argParseErrors (Option.toList pos @ nonPos) processKeyValue argParseErrors pos nonPos
setFlagValue argParseErrors flags setFlagValue argParseErrors flags
mainLoop parseState argParseErrors leftoverArgsName leftoverArgsParser mainLoop parseState argParseErrors leftoverArgAcc leftoverArgsName leftoverArgsParser
] ]
) )

View File

@@ -1,6 +1,6 @@
{ {
"sdk": { "sdk": {
"version": "8.0.100", "version": "8.0.100",
"rollForward": "latestFeature" "rollForward": "latestMajor"
} }
} }