Given that type providers is not supported yet, I need some other convenient way to parse a YAML file in F#. However, as type providers are so awesome, I'm finding it difficult to find anything else when I search the internet for an alternative solution.
What's the simplest way to parse a config file in F#, given that type providers are off the table?
Using a library is fine, but the more OO that the interface of that library is, the less convenient it will be for use in F#.
I don't need a full "deserialize any yaml into a given type/object graph" either; something like xpath querying but for YAML is perfectly fine; I just don't want to have to read the stream and parse it manually.
Here is my current attempt, which fails at runtime because the discriminated type union OneOfSeveral
does not have a default constructor. It doesn't really surprise me that it requires some special handling, but I have no idea how to go about doing it.
open System
open YamlDotNet.Serialization
[<CLIMutable>]
type SpecificThing = {
foo : string
bar : int
}
type OneOfSeveral = Thing of SpecificThing
[<CLIMutable>]
type Root = {
option : OneOfSeveral
}
[<EntryPoint>]
let main argv =
let yaml = @"---
option:
thing:
foo: foobar
bar: 17
"
let deserializer = DeserializerBuilder().Build()
let config = deserializer.Deserialize<Root>(yaml)
printfn "%A" config
0
I'm also not sure how I want to represent the choice in the type union in the YAML; I've considered several options:
# omit the 'option' level completely
thing:
foo: foobar
bar: 17
# have a specific field to discriminate on
option:
type: thing
foo: foobar
bar: 17
In the end, it's more important to me to have a flexible object graph for the config, than to have a nice-looking YAML file, so whatever works...
Yes, DU's lacking a parameterless constructor is a common pain point when getting F# types to work with C#-targeting libraries.
Looking at YamlDotNet, there seem to be two ways of customizing serialization/deserialization, IYamlTypeConverter
and IYamlConvertible
interface.
I've looked briefly at writing a general-purpose IYamlTypeConverter
for all union types, but ran into a wall with being unable to defer serialization of union's fields back to the original serializer. Having said that, you could implement IYamlTypeConverters
specifically for the types you care about.
Another more lightweight option would be to create a wrapper type for the union with a parameterless constructor that would implement IYamlConvertible
and expose that in your config type.
I've went for a different approach in the past - what I've used was the representation model part of YamlDotNet rather than the serializer/deserializer interface. This is how you'd open a stream from a yaml string:
open System.IO
open YamlDotNet.RepresentationModel
let read yaml =
use reader = new StringReader(yaml)
let stream = YamlStream()
stream.Load(reader)
stream.Documents
let doc = read yaml
doc.[0].RootNode
This gives you a generic tree representation of your document. Then I'd have an active pattern along these lines to simplify writing functions that traverse this tree:
let (|Mapping|Scalar|Sequence|) (yamlNode: YamlNode) =
match yamlNode.NodeType with
| YamlNodeType.Mapping ->
let node = yamlNode :?> YamlMappingNode
let mapping =
node.Children
|> Seq.map (fun kvp ->
let keyNode = kvp.Key :?> YamlScalarNode
keyNode.Value, kvp.Value)
|> Map.ofSeq
Mapping (node, mapping)
| YamlNodeType.Scalar ->
let node = yamlNode :?> YamlScalarNode
Scalar (node, node.Value)
| YamlNodeType.Sequence ->
let node = yamlNode :?> YamlSequenceNode
Sequence (node, List.ofSeq node.Children)
| YamlNodeType.Alias
| _ -> failwith "¯\_(ツ)_/¯"
Now you can write functions against that representation, like this here XPath wannabe:
let rec go (path: string list) (yamlNode: YamlNode) =
match path with
| [] -> Some yamlNode
| x::xs ->
match yamlNode with
| Mapping (n, mapping) ->
match mapping |> Map.tryFind x with
| Some nested ->
go xs nested
| None -> None
| Sequence _
| Scalar _ -> None
go ["option"; "thing"; "bar"] doc.[0].RootNode
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With