Skip to content

Commit a217eeb

Browse files
committed
Added new Input.inject feature.
1 parent fde89e9 commit a217eeb

8 files changed

Lines changed: 113 additions & 22 deletions

File tree

.claude/settings.local.json

Lines changed: 0 additions & 13 deletions
This file was deleted.

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,5 @@ Desktop.ini
5151
*.dbmdl
5252
*.jfm
5353
/src/Tests/TestData/AdventureWorksLT_Temp.db
54-
**/Verify/*.received.*
54+
**/Verify/*.received.*
55+
.claude

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ _[Click here to view the old beta 4 README](README-beta4.md)_
1111
* `Input.option` helper avoids the need to use the `System.CommandLine.Option` type directly (which conflicts with the F# `Option` type)
1212
* `Input.optionMaybe` and `Input.argumentMaybe` helpers allow you to use F# `option` types in your handler function.
1313
* `Input.context` helper allows you to pass the `ActionContext` to your action function which is necessary for some operations.
14+
* `Input.inject` helper allows you to inject pre-resolved dependencies (e.g., loggers, services) into your action function alongside parsed CLI inputs.
1415
* `Input.validate` helper allows you to validate against parsed value using the F# `Result` type.
1516

1617
## Example
@@ -70,6 +71,7 @@ The new `Input` module contains functions for the underlying System.CommandLine
7071
* `argumentMaybe` creates a named `Argument<'T option>` that defaults to `None`.
7172
* `option` creates a named `Option<'T>`
7273
* `optionMaybe` creates a named `Option<'T option>` that defaults to `None`.
74+
* `inject` wraps a pre-resolved dependency value for injection into the action inputs tuple.
7375

7476
### Input Properties
7577
* `acceptLegalFileNamesOnly` sets the option or argument to accept only values representing legal file names.
@@ -290,6 +292,37 @@ let main argv =
290292

291293
</details>
292294

295+
<details>
296+
<summary><b>Injecting Dependencies</b></summary>
297+
298+
You can use `Input.inject` to pass pre-resolved dependencies into your action handler alongside parsed CLI inputs. This is useful for injecting loggers, database connections, or any other service.
299+
300+
```F#
301+
open Serilog
302+
open FSharp.SystemCommandLine
303+
open Input
304+
305+
[<EntryPoint>]
306+
let main argv =
307+
let logger =
308+
LoggerConfiguration()
309+
.WriteTo.Console()
310+
.CreateLogger()
311+
|> Input.inject
312+
313+
let name = option<string> "--name" |> desc "Your name"
314+
315+
rootCommand argv {
316+
description "Greets a user"
317+
inputs (logger, name)
318+
setAction (fun (logger: ILogger, name) ->
319+
logger.Information("Hello, {Name}!", name)
320+
)
321+
}
322+
```
323+
324+
</details>
325+
293326
<details>
294327
<summary><b>Microsoft.Extensions.Hosting</b></summary>
295328

src/FSharp.SystemCommandLine/CommandBuilders.fs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ let private parseInput<'V> (handlerInput: ActionInput) (pr: ParseResult) (cancel
1414
| ParsedOption o -> pr.GetValue<'V>(o :?> Option<'V>)
1515
| ParsedArgument a -> pr.GetValue<'V>(a :?> Argument<'V>)
1616
| Context -> { ParseResult = pr; CancellationToken = cancelToken } |> unbox<'V>
17+
| Injection value -> value |> unbox<'V>
1718

1819
type CommandSpec<'Inputs, 'Output> =
1920
{
@@ -265,14 +266,14 @@ type BaseCommandBuilder<'A, 'B, 'C, 'D, 'E, 'F, 'G, 'H, 'Output>() =
265266
match input.Source with
266267
| ParsedOption o -> cmd.Add o
267268
| ParsedArgument a -> cmd.Add a
268-
| Context -> ()
269+
| Context | Injection _ -> ()
269270
)
270271
spec.ExtraInputs
271272
|> Seq.iter (fun input ->
272273
match input.Source with
273274
| ParsedOption o -> cmd.Add o
274275
| ParsedArgument a -> cmd.Add a
275-
| Context -> ()
276+
| Context | Injection _ -> ()
276277
)
277278
spec.SubCommands |> List.iter cmd.Add
278279
spec.Aliases |> List.iter cmd.Aliases.Add

src/FSharp.SystemCommandLine/Inputs.fs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ type ActionContext =
2020
CancellationToken: System.Threading.CancellationToken
2121
}
2222

23-
type ActionInputSource =
23+
type ActionInputSource =
2424
| ParsedOption of Option
2525
| ParsedArgument of Argument
2626
| Context
27+
| Injection of obj
2728

2829
type ActionInput(source: ActionInputSource) =
2930
member this.Source = source
@@ -43,6 +44,7 @@ type ActionInput<'T>(inputType: ActionInputSource) =
4344
| ParsedOption o -> o :?> Option<'T> |> parseResult.GetValue
4445
| ParsedArgument a -> a :?> Argument<'T> |> parseResult.GetValue
4546
| Context -> parseResult |> unbox<'T>
47+
| Injection value -> value |> unbox<'T>
4648

4749
type Arity =
4850
| ArgumentArity of min: int * max: int
@@ -77,9 +79,13 @@ type Arity =
7779
module Input =
7880

7981
/// Injects an `ActionContext` into the action which contains the `ParseResult` and a cancellation token.
80-
let context =
82+
let context =
8183
ActionInput<ActionContext>(Context)
8284

85+
/// Injects a dependency value into the action inputs tuple.
86+
let inject<'T> (value: 'T) =
87+
ActionInput<'T>(Injection (box value))
88+
8389
/// Creates a named option. Example: `option "--file-name"`
8490
let option<'T> (name: string) =
8591
Option<'T>(name) |> ActionInput.OfOption

src/Tests/SimpleAppTest.fs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,3 +203,44 @@ let ``09 tryParser Directory Info`` () =
203203
)
204204
} =! 1 // Should fail
205205
actionCalled =! false
206+
207+
type AppSettings = { ConnectionString: string; MaxRetries: int }
208+
209+
[<Test>]
210+
let ``10 - Input_inject should pass a dependency into the action`` () =
211+
let settings = { ConnectionString = "Server=localhost"; MaxRetries = 3 }
212+
213+
testRootCommand "--name Jordan" {
214+
description "Inject a dependency"
215+
inputs (
216+
option<string> "--name" |> desc "Your name",
217+
inject settings
218+
)
219+
setAction (fun (name, injectedSettings) ->
220+
name =! "Jordan"
221+
injectedSettings =! settings
222+
actionCalled <- true
223+
)
224+
} =! 0
225+
actionCalled =! true
226+
227+
type Env = { GetRandom: int -> int -> int }
228+
229+
[<Test>]
230+
let ``11 - Input_inject should allow injecting behavior into a pure action`` () =
231+
let env = { GetRandom = fun min max -> 42 }
232+
233+
testRootCommand "--count 5" {
234+
description "Generate random numbers"
235+
inputs (
236+
inject env,
237+
option<int> "--count" |> desc "How many numbers to generate"
238+
)
239+
setAction (fun (env, count) ->
240+
let result = env.GetRandom 1 100
241+
count =! 5
242+
result =! 42
243+
actionCalled <- true
244+
)
245+
} =! 0
246+
actionCalled =! true

src/build.fsx

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
#r "nuget: Fun.Build, 1.0.5"
1+
#r "nuget: Fun.Build, 1.1.17"
22

33
open Fun.Build
44

55
let src = __SOURCE_DIRECTORY__
6+
let fsproj = $"{src}/FSharp.SystemCommandLine/FSharp.SystemCommandLine.fsproj"
67

7-
pipeline "CI Build" {
8+
pipeline "Build" {
89

910
stage "Build FSharp.SystemCommandLine.sln" {
10-
run $"dotnet restore {src}/FSharp.SystemCommandLine/FSharp.SystemCommandLine.fsproj"
11-
run $"dotnet build {src}/FSharp.SystemCommandLine/FSharp.SystemCommandLine.fsproj --configuration Release"
11+
run $"dotnet restore {fsproj}"
12+
run $"dotnet build {fsproj} --configuration Release"
1213
}
1314

1415
stage "Run Tests" {
@@ -18,4 +19,24 @@ pipeline "CI Build" {
1819
runIfOnlySpecified false
1920
}
2021

22+
open System.Xml.Linq
23+
24+
pipeline "Publish" {
25+
26+
stage "Pack" {
27+
run $"dotnet pack {fsproj} --configuration Release"
28+
}
29+
30+
stage "Push to NuGet" {
31+
run (fun _ ->
32+
let version = XDocument.Load(fsproj).Descendants(XName.Get "Version") |> Seq.head |> _.Value
33+
let nupkg = $"{src}/FSharp.SystemCommandLine/bin/Release/FSharp.SystemCommandLine.%s{version}.nupkg"
34+
let nugetKey = System.Environment.GetEnvironmentVariable("NUGET_KEY")
35+
$"dotnet nuget push %s{nupkg} --source https://api.nuget.org/v3/index.json --api-key %s{nugetKey} --skip-duplicate"
36+
)
37+
}
38+
39+
runIfOnlySpecified true
40+
}
41+
2142
tryPrintPipelineCommandHelp ()

src/publish.bat

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dotnet fsi build.fsx -- -p Build -p Publish

0 commit comments

Comments
 (0)