Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What's the best way to parse YAML in F# on .NET Core?

Tags:

parsing

yaml

f#

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...

like image 785
Tomas Aschan Avatar asked Oct 11 '17 20:10

Tomas Aschan


1 Answers

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
like image 137
scrwtp Avatar answered Sep 23 '22 06:09

scrwtp