diff --git a/README.md b/README.md index 6d6526e..ead900e 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ PM> Install-Package Symbolica.Extensions.Configuration.FSharp ## Usage -The primary means of using this library is through two computation expressions called `section` and `optSection`. These provide a declarative DSL for safely binding a type from an `IConfiguration` instance. +The primary means of using this library is through a computation expression called `bind` and a handful of combinator functions which make it easy to combine binders for sub sections into larger types. These provide a declarative DSL for safely binding a type from an `IConfiguration` instance. It's probably best to see an example, but you can find the full api [here](src/Symbolica.Extensions.Configuration.FSharp/Api.fs). ## Example @@ -60,37 +60,35 @@ type Options = We can bind this from an `IConfiguration` like this ```fsharp -let bindOptions (config: IConfiguration) = - section "Options" { - let! name = value "Name" - and! count = value "Count" - - and! subOptions = - section "Sub" { - let! optionalNumber = optValueOf Decode.float "MaybeDecimal" - and! bool = valueOf Decode.bool "bool" - - return - { OptionalNumber = optionalNumber - Bool = bool } - } - - and! optSubOptions = - optSection "OptSub" { - let! optionalNumber = optValueOf Decode.float "MaybeDecimal" - and! bool = valueOf Decode.bool "bool" - - return - { OptionalNumber = optionalNumber - Bool = bool } - } - - return - { Name = name - Count = count - SubOptions = subOptions - OptSubOptions = optSubOptions } - } +let bindOptions config = + + // We can easily define reusable binders for the child sections because the api is composable + let bindSubOptions = + bind { + let! optionalNumber = optValueOf Decode.float "MaybeDecimal" + and! bool = valueOf Decode.bool "bool" + + return + { OptionalNumber = optionalNumber + Bool = bool } + } + + section + "Options" + (bind { + let! name = value "Name" + + // We can use the `section` combinator with our bindSubOptions function to bind it from a section called "Sub" + and! subOptions = section "Sub" bindSubOptions + + // We can also use the `optSection` combinator with our bindSubOptions funtions if that whole section of config is optional + and! optSubOptions = optSection "OptSub" bindSubOptions + + return + { Name = name + SubOptions = subOptions + OptSubOptions = optSubOptions } + }) |> Binder.eval config ``` diff --git a/src/Symbolica.Extensions.Configuration.FSharp/Api.fs b/src/Symbolica.Extensions.Configuration.FSharp/Api.fs new file mode 100644 index 0000000..d2d8eda --- /dev/null +++ b/src/Symbolica.Extensions.Configuration.FSharp/Api.fs @@ -0,0 +1,29 @@ +namespace Symbolica.Extensions.Configuration.FSharp + +[] +module Api = + let bind = Binder.Builder() + + let section key sectionBinder = + key |> Binder.section |> Binder.nest sectionBinder + + let optSection key sectionBinder = + key + |> Binder.optSection + |> Binder.nestOpt sectionBinder + + let value = + Binder.section >> Binder.nest Binder.Section.value + + let valueOf decoder = value >> Binder.bind decoder + + let optValue = + Binder.optSection + >> Binder.nestOpt Binder.Section.value + + let optValueOf decoder = + optValue + >> Binder.bind + (function + | Some s -> s |> decoder |> Binder.map Some + | None -> None |> Success |> Binder.ofBind) diff --git a/src/Symbolica.Extensions.Configuration.FSharp/Binder.fs b/src/Symbolica.Extensions.Configuration.FSharp/Binder.fs index 760e39d..b6100e5 100644 --- a/src/Symbolica.Extensions.Configuration.FSharp/Binder.fs +++ b/src/Symbolica.Extensions.Configuration.FSharp/Binder.fs @@ -78,29 +78,16 @@ module Binder = if section.GetChildren() |> Seq.isEmpty then match section with | :? IConfigurationSection as s -> s.Value |> Success - | _ -> - failwith - $"Expected to bind from an IConfigurationSection, but was binding from an {section.GetType()}" + | _ -> failwith $"Expected to bind from an IConfigurationSection, but was binding from an {section.GetType()}" else [ $"Expected a simple value at '{section |> path}' but found an object." ] |> Failure) let valueOf decoder = value |> bind decoder -[] -module BinderExtensions = - let value = - Binder.section >> Binder.nest Binder.Section.value - - let valueOf decoder = value >> Binder.bind decoder - - let optValue = - Binder.optSection - >> Binder.nestOpt Binder.Section.value - - let optValueOf decoder = - optValue - >> Binder.bind - (function - | Some s -> s |> decoder |> Binder.map Some - | None -> None |> Success |> Binder.ofBind) + type Builder() = + member _.Bind(x: Binder<'a>, f) = x |> bind f + member _.BindReturn(x: Binder<'a>, f) = x |> map f + member _.MergeSources(x1, x2) = zip x1 x2 + member _.Return(x: 'a) : Binder<'a> = x |> Success |> ofBind + member _.ReturnFrom(x: Binder<'a>) = x diff --git a/src/Symbolica.Extensions.Configuration.FSharp/Builders.fs b/src/Symbolica.Extensions.Configuration.FSharp/Builders.fs deleted file mode 100644 index 54a5190..0000000 --- a/src/Symbolica.Extensions.Configuration.FSharp/Builders.fs +++ /dev/null @@ -1,26 +0,0 @@ -namespace Symbolica.Extensions.Configuration.FSharp - -type SectionBuilder(key) = - - let nest (b: Binder<'a>) = key |> Binder.section |> Binder.nest b - member _.Bind(x: Binder<'a>, f) = x |> Binder.bind f - member _.BindReturn(x: Binder<'a>, f) = x |> Binder.map f |> nest - member _.MergeSources(x1, x2) = Binder.zip x1 x2 - member _.Return(x: 'a) : Binder<'a> = Binder(fun _ -> x |> Success) |> nest - member _.ReturnFrom(x: Binder<'a>) = x |> nest - -type OptSectionBuilder(key) = - let nest (b: Binder<'a>) : Binder<'a option> = - key |> Binder.optSection |> Binder.nestOpt b - - member _.Bind(x: Binder<'a>, f) = x |> Binder.bind f - member _.BindReturn(x: Binder<'a>, f) = x |> Binder.map f |> nest - member _.MergeSources(x1, x2) = Binder.zip x1 x2 - member _.Return(x: 'a) : Binder<'a option> = Binder(fun _ -> x |> Success) |> nest - member _.ReturnFrom(x: Binder<'a>) = x |> nest - -[] -module Builders = - let section key = SectionBuilder(key) - - let optSection key = OptSectionBuilder(key) diff --git a/src/Symbolica.Extensions.Configuration.FSharp/Symbolica.Extensions.Configuration.FSharp.fsproj b/src/Symbolica.Extensions.Configuration.FSharp/Symbolica.Extensions.Configuration.FSharp.fsproj index 762c1f8..040db06 100644 --- a/src/Symbolica.Extensions.Configuration.FSharp/Symbolica.Extensions.Configuration.FSharp.fsproj +++ b/src/Symbolica.Extensions.Configuration.FSharp/Symbolica.Extensions.Configuration.FSharp.fsproj @@ -13,6 +13,6 @@ - + diff --git a/tests/Symbolica.Extensions.Configuration.FSharp.Tests/BuildersTests.fs b/tests/Symbolica.Extensions.Configuration.FSharp.Tests/ApiTests.fs similarity index 81% rename from tests/Symbolica.Extensions.Configuration.FSharp.Tests/BuildersTests.fs rename to tests/Symbolica.Extensions.Configuration.FSharp.Tests/ApiTests.fs index c2bdc0c..da6e160 100644 --- a/tests/Symbolica.Extensions.Configuration.FSharp.Tests/BuildersTests.fs +++ b/tests/Symbolica.Extensions.Configuration.FSharp.Tests/ApiTests.fs @@ -14,34 +14,28 @@ module Builders = OptSubOptions: SubOptions option } let mkOptions config = - section "Options" { - let! name = value "Name" + let bindSubOptions = + bind { + let! optionalNumber = optValueOf Decode.float "MaybeDecimal" + and! bool = valueOf Decode.bool "bool" - and! subOptions = - section "Sub" { - let! optionalNumber = optValueOf Decode.float "MaybeDecimal" - and! bool = valueOf Decode.bool "bool" + return + { OptionalNumber = optionalNumber + Bool = bool } + } - return - { OptionalNumber = optionalNumber - Bool = bool } - } + section + "Options" + (bind { + let! name = value "Name" + and! subOptions = section "Sub" bindSubOptions + and! optSubOptions = optSection "OptSub" bindSubOptions - and! optSubOptions = - optSection "OptSub" { - let! optionalNumber = optValueOf Decode.float "MaybeDecimal" - and! bool = valueOf Decode.bool "bool" - - return - { OptionalNumber = optionalNumber - Bool = bool } - } - - return - { Name = name - SubOptions = subOptions - OptSubOptions = optSubOptions } - } + return + { Name = name + SubOptions = subOptions + OptSubOptions = optSubOptions } + }) |> Binder.eval config [] diff --git a/tests/Symbolica.Extensions.Configuration.FSharp.Tests/Symbolica.Extensions.Configuration.FSharp.Tests.fsproj b/tests/Symbolica.Extensions.Configuration.FSharp.Tests/Symbolica.Extensions.Configuration.FSharp.Tests.fsproj index 48e8e0c..88531c6 100644 --- a/tests/Symbolica.Extensions.Configuration.FSharp.Tests/Symbolica.Extensions.Configuration.FSharp.Tests.fsproj +++ b/tests/Symbolica.Extensions.Configuration.FSharp.Tests/Symbolica.Extensions.Configuration.FSharp.Tests.fsproj @@ -20,6 +20,6 @@ - +