feat: 4 function helpers for accepting a known set of values bound to a typed value - #37
Conversation
…typed value (acceptOnlyFromChoices, acceptOnlyFromChoicesWith, acceptManyFromChoices, acceptManyFromChoicesWith, acceptOnlyFromChoicesIgnoreCase, acceptManyFromChoicesIgnoreCase)
JordanMarr
left a comment
There was a problem hiding this comment.
Thanks for this! I really like the core idea. Collapsing "validate the string, then map it to a typed value" into a single step is a genuine improvement, and it pairs beautifully with discriminated unions:
type Env = Dev | Staging | Prod
option<Env> "--env" |> mapFromAmong [ "dev", Dev; "staging", Staging; "prod", Prod ]I'd like to merge it with a few changes to keep the API surface small and consistent with the rest of the library.
1. Rename to mapFromAmong / mapFromAmongWith.
I try to keep Input helper names in parity with the S.CL member they wrap (setAction ~ SetAction, acceptOnlyFromAmong ~ AcceptOnlyFromAmong). This one has no S.CL counterpart, so the next best thing is a name that sits next to its sibling and explains the difference on sight: one accepts from among a set of strings, the other maps from among them to a typed value. The With suffix for the comparer overload follows FSharp.Core (List.sortWith, Seq.compareWith).
2. Drop the IgnoreCase pair.
mapFromAmongWith StringComparer.OrdinalIgnoreCase [...] covers it, and I'd rather have two functions than four. I'll add a README example showing the ignore-case usage.
3. Hold the Many variant for a follow-up PR.
Two reasons. First, there's an ordering bug: the fold builds the list with cons and never reverses it, so --project main --project tests yields [tests; main]. Second, it targets 'T list, and an omitted option<'T list> currently throws a NullReferenceException before the parser ever runs (that's a pre-existing issue in the library, not something you introduced, but it makes the Many helper a foot-gun unless paired with required). I'd rather land the single-value version cleanly now and think about the multi-value shape separately.
4. Use one code path.
Right now the case-sensitive branch goes through AcceptOnlyFromAmong, so a bad value produces S.CL's stock "Argument 'x' not recognized. Must be one of:" message, while the case-insensitive branch produces your custom "'x' is not a valid choice from: ..." message. The completion-sources branch already drives the <dev|staging|prod> help rendering, so I think you can drop the AcceptOnlyFromAmong call and the comparer.Equals("a", "A") heuristic entirely and always use completion sources + tryParse. Same behavior, one message, less code.
5. Tests and a README bullet.
A couple of NUnit cases in Tests/ (valid value, invalid value, ignore-case via mapFromAmongWith) plus a bullet under acceptOnlyFromAmong in the README's Input Properties section, ideally with a DU as the example.
Two tiny doc nits while you're in there: the summary says "Maps an option" but it works for arguments too, and the sentence about the parser closing over the table reads more like an implementation note than user docs.
Happy to answer questions on any of this. Thanks again for the contribution, and for the input { } work too, which I'm genuinely excited about.
|
Should we lift a helper for completion sources? Maybe an Vis-a-vis the rest; no worries, thanks! |
…g to typed value Added tests; and bullet points in readme.md Tests demonstrate failing, correct, and undefined behaviours.
|
Merging as-is — thanks for turning the changes around so quickly! I'll fold a couple of tiny README touch-ups into main directly so we can skip another round. Let's leave the completion-source helper for a separate discussion if it comes up again. |
- README example was missing the option name argument - Drop the "tryParse overrides mapFromAmong" note, remarks, and the test asserting that undefined behaviour - Reword doc comments with inline examples Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PsmkZPCobsju7AauvVH8du
- README example was missing the option name argument - Drop the "tryParse overrides mapFromAmong" note, remarks, and the test asserting that undefined behaviour - Reword doc comments with inline examples Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PsmkZPCobsju7AauvVH8du
Similar to
Input.allowOnlyFromAmong. For an option/arg whose legal values are a known set, each bound to a typed value. Just takes the ceremony out of mapping from a string to your typed value for simple cases.The lowest level utility function takes a string comparer ontop of the sequence of
string * typematches which is used in the parsing. A duplicate helper is for where the action input is a range of the values.Each comes with a case sensitive, and case insensitive function without the string comparer input.