Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Argu.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,6 @@
<Project Path="samples/Argu.Samples.Introspect/Argu.Samples.Introspect.fsproj" />
</Folder>
<Project Path="src/Argu/Argu.fsproj" />
<Project Path="src/Argu.MSConfig/Argu.MSConfig.fsproj" />
<Project Path="tests/Argu.Tests/Argu.Tests.fsproj" />
</Solution>
3 changes: 3 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
<PackageVersion Include="FSharp.Core" Version="6.0.0"/>
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="4.4.0"/>

<!-- Companion adapter deps -->
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0"/>

<!-- Test deps -->
<PackageVersion Include="xunit.v3.mtp-v2" Version="3.2.2"/>
<PackageVersion Include="Unquote" Version="7.0.1"/>
Expand Down
19 changes: 19 additions & 0 deletions src/Argu.MSConfig/Argu.MSConfig.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
<Description>Microsoft.Extensions.Configuration adapter for Argu — exposes any IConfiguration source as an Argu IConfigurationReader.</Description>
<PackageTags>F#, argument, commandline, parser, configuration</PackageTags>
</PropertyGroup>
<ItemGroup>
<Compile Include="ConfigurationReader.fs"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Argu\Argu.fsproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="FSharp.Core"/>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions"/>
</ItemGroup>
</Project>
27 changes: 27 additions & 0 deletions src/Argu.MSConfig/ConfigurationReader.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace Argu.MSConfig

open Microsoft.Extensions.Configuration

open Argu

/// <summary>
/// Argu <see cref="IConfigurationReader"/> backed by a
/// <see cref="Microsoft.Extensions.Configuration.IConfiguration"/>.
/// The reader returns <c>null</c> when a key is missing, matching the
/// contract of every other Argu configuration reader.
/// </summary>
type MicrosoftExtensionsConfigurationReader(configuration : IConfiguration, ?name : string) =
let name = defaultArg name "Microsoft.Extensions.Configuration reader"
interface IConfigurationReader with
member _.Name = name
member _.GetValue(key : string) =
// IConfiguration.[key] returns null for missing keys, which matches Argu's
// expectation.
configuration[key]

/// Factory helpers mirroring the shape of <see cref="ConfigurationReader"/>.
[<AbstractClass; Sealed>]
type ConfigurationReader =
/// Create an Argu <see cref="IConfigurationReader"/> over an <see cref="IConfiguration"/>.
static member FromMicrosoftConfiguration(configuration : IConfiguration, ?name : string) : IConfigurationReader =
MicrosoftExtensionsConfigurationReader(configuration, ?name = name) :> _
18 changes: 18 additions & 0 deletions src/Argu/ConfigReaders.fs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,24 @@ type ConfigurationReader =
static member FromEnvironmentVariables() =
EnvironmentVariableConfigurationReader() :> IConfigurationReader

/// <summary>
/// Create a configuration reader that reads environment variables
/// with a fixed prefix prepended to the requested key. Useful for
/// mapping <c>--foo-bar</c> arguments onto <c>MYAPP_FOO_BAR</c>
/// environment variables without renaming the schema. Reads
/// Process, then User, then Machine targets (same order as
/// <see cref="FromEnvironmentVariables()"/>).
/// </summary>
/// <param name="prefix">String prepended to each requested key (e.g. <c>"MYAPP_"</c>).</param>
static member FromEnvironmentVariables(prefix : string) =
let inner = EnvironmentVariableConfigurationReader() :> IConfigurationReader
let read (key : string) =
match inner.GetValue(prefix + key) with
| null -> None
| v -> Some v
FunctionConfigurationReader(read, name = sprintf "Environment Variables (prefix=%s)" prefix)
:> IConfigurationReader
Comment thread
bartelink marked this conversation as resolved.
Outdated

/// Create a configuration reader instance using the application's resident AppSettings configuration
static member FromAppSettings() = AppSettingsConfigurationReader() :> IConfigurationReader
/// Create a configuration reader instance using a local xml App.Config file
Expand Down
1 change: 1 addition & 0 deletions tests/Argu.Tests/Argu.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<Compile Include="CoverageTests.fs"/>
<Compile Include="ParseConfigTests.fs"/>
<Compile Include="UsageStringsTests.fs"/>
<Compile Include="EnvVarPrefixTests.fs"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Argu\Argu.fsproj"/>
Expand Down
53 changes: 53 additions & 0 deletions tests/Argu.Tests/EnvVarPrefixTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
namespace Argu.Tests

open System
open Xunit
open Swensen.Unquote
Comment thread
bartelink marked this conversation as resolved.
Outdated

open Argu

/// Tests for the env-var prefix configuration reader (PR 20).
module ``Argu Tests EnvVarPrefix`` =

type Args =
| [<CustomAppSettings("HOST")>] HostName of string
| [<CustomAppSettings("PORT")>] Port of int
interface IArgParserTemplate with member this.Usage = "x"

let private withEnv (key : string) (value : string) (body : unit -> 'T) : 'T =

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably worth making this return an IDisposable such that you write use _ = envOverride "key" "value that does the finally bit in the Dispose()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in d3d9098withEnv is now an IDisposable-returning envOverride, so each test reads use _ = envOverride key value and the prior value is restored in Dispose(). Also flattens the nested-lambda indentation in the two-var round-trip test. 147 tests green.

let prior = Environment.GetEnvironmentVariable key
try
Environment.SetEnvironmentVariable(key, value)
body ()
finally
Environment.SetEnvironmentVariable(key, prior)

[<Fact>]
let ``FromEnvironmentVariables(prefix) prepends prefix to key`` () =
let reader = ConfigurationReader.FromEnvironmentVariables(prefix = "MYAPP_")
withEnv "MYAPP_HOST" "example.com" (fun () ->
test <@ reader.GetValue("HOST") = "example.com" @>)

[<Fact>]
let ``FromEnvironmentVariables(prefix) returns null for missing keys`` () =
let reader = ConfigurationReader.FromEnvironmentVariables(prefix = "ARGU_TEST_MISSING_")
// Ensure no stray env var; SetEnvironmentVariable(null) clears it.
Environment.SetEnvironmentVariable("ARGU_TEST_MISSING_NOPE", null)
test <@ reader.GetValue("NOPE") = null @>

[<Fact>]
let ``FromEnvironmentVariables(prefix) parses round-trip through ArgumentParser`` () =
withEnv "DEMO_HOST" "host.local" (fun () ->
withEnv "DEMO_PORT" "443" (fun () ->
let parser = ArgumentParser.Create<Args>(programName = "demo")
let reader = ConfigurationReader.FromEnvironmentVariables(prefix = "DEMO_")
let results = parser.ParseConfiguration(reader, ignoreMissing = true)
test <@ results.GetResult(HostName) = "host.local" @>
test <@ results.GetResult(Port) = 443 @>))

[<Fact>]
let ``No-arg FromEnvironmentVariables still works without prefix`` () =
// Sanity check: the legacy zero-arg overload is preserved.
withEnv "ARGU_TEST_PLAIN" "value" (fun () ->
let reader = ConfigurationReader.FromEnvironmentVariables()
test <@ reader.GetValue("ARGU_TEST_PLAIN") = "value" @>)
Loading