YAML.jl Custom Tag Constructor Example: !include


#1

YAML.jl has recently gained support for custom tag constructors (YAML.jl/pull/33) and to understand how they work, I thought I’d try it out to implement an !include tag that can be used to include/merge the contents of arbitrary files into YAML files at parse time.

e.g. if you have files foo.yaml and bar.yaml

# foo.yaml
x: !include bar.yaml
y: [1, 2, 3]
# bar.yaml
a: 1
b: 2

through some process, these should effectively become:

x:
  a: 1
  b: 2
y: [1, 2, 3]

Ideally, this would work for relative and absolute paths :slight_smile:

Here is a solution using the new tag constructor features in YAML.jl:

module yaml

using YAML

const _constructors = Dict{AbstractString, Function}()

"""Loads YAML files relative to the current directory"""
function load_file_relative(filename::AbstractString;
    constructors::Dict{AbstractString, Function}=_constructors
)
    filename = abspath(filename)
    cd(dirname(filename)) do
        YAML.load_file(filename, constructors)
    end
end

"""Processes an !include constructor"""
function construct_include(constructor::YAML.Constructor, node::YAML.ScalarNode)
    filename = node.value
    if !isfile(filename)
        throw(YAML.ConstructorError(nothing, nothing, "file $(abspath(filename)) does not exist", node.start_mark))
    end
    if !(last(splitext(filename)) in (".yaml", ".yml", ".json"))
        return readstring(filename)
    end
    # pass forward custom constructors
    constructors = Dict{AbstractString, Function}(
        tag => f for (tag, f) in constructor.yaml_constructors
        if tag !== nothing && ismatch(r"^!\w+$", tag)
    )
    load_file_relative(filename; constructors=constructors)
end
construct_include(::YAML.Constructor, node::YAML.Node) = throw(
    YAML.ConstructorError(nothing, nothing, "expected a scalar node, but found $(typeof(node))", node.start_mark)
)
_constructors["!include"] = construct_include

end

Then, foo.yaml can be loaded as follows:

julia> yaml.load_file_relative("/tmp/foo.yaml")
Dict{Any,Any} with 2 entries:
  "x" => Dict{Any,Any}(Pair{Any,Any}("b",2),Pair{Any,Any}("a",1))
  "y" => [1,2,3]

Hopefully this is useful to someone!