Skip to content

Commit

Permalink
Improve the API to make it possible to reuse section binders. (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
Choc13 authored Jun 29, 2021
1 parent df5ef3e commit 284e817
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 105 deletions.
62 changes: 30 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
```

Expand Down
29 changes: 29 additions & 0 deletions src/Symbolica.Extensions.Configuration.FSharp/Api.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace Symbolica.Extensions.Configuration.FSharp

[<AutoOpen>]
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)
27 changes: 7 additions & 20 deletions src/Symbolica.Extensions.Configuration.FSharp/Binder.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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

[<AutoOpen>]
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
26 changes: 0 additions & 26 deletions src/Symbolica.Extensions.Configuration.FSharp/Builders.fs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@
<Compile Include="BindResult.fs" />
<Compile Include="Binder.fs" />
<Compile Include="Decode.fs" />
<Compile Include="Builders.fs" />
<Compile Include="Api.fs" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -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

[<Fact>]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@
<Compile Include="SectionStub.fs" />
<Compile Include="BinderTests.fs" />
<Compile Include="DecodeTests.fs" />
<Compile Include="BuildersTests.fs" />
<Compile Include="ApiTests.fs" />
</ItemGroup>
</Project>

0 comments on commit 284e817

Please sign in to comment.