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