///|
/// Helpers over the dynamic YAML tree.
///
/// A Compose file is dynamic by nature: almost every field is optional, and
/// many accept two or three different shapes (`environment` is a map *or* a
/// list, `ports` is a list *or* a map, `build` is a string *or* a map). Rather
/// than mirror the whole spec in a typed tree, the model is kept as `Yaml` and
/// every access goes through one of these coercions.
///|
/// Lower-case ASCII only. MoonBit's core has no `String::to_lowercase`, and
/// env-var names and boolean literals are ASCII anyway.
pub fn ascii_lower(s : String) -> String {
let out = StringBuilder(size_hint=s.length())
for c in s {
out.write_char(c.to_ascii_lowercase())
}
out.to_string()
}
///|
/// A scalar rendered as text. `None` for collections and nulls.
pub fn y_str(v : @yaml.Yaml) -> String? {
match v {
@yaml.Yaml::String(s) => Some(s)
@yaml.Yaml::Boolean(b) => Some(if b { "true" } else { "false" })
@yaml.Yaml::Integer(i) => Some(i.to_string())
@yaml.Yaml::Real(_, repr~) => Some(repr)
_ => None
}
}
///|
/// A scalar rendered as text, or `fallback`.
pub fn y_str_or(v : @yaml.Yaml, fallback : String) -> String {
match y_str(v) {
Some(s) => s
None => fallback
}
}
///|
/// A boolean, accepting the spellings Compose and `podman` accept.
pub fn y_bool(v : @yaml.Yaml) -> Bool? {
match v {
@yaml.Yaml::Boolean(b) => Some(b)
@yaml.Yaml::Integer(i) => Some(i != 0)
@yaml.Yaml::String(s) =>
match ascii_lower(s) {
"true" | "yes" | "on" | "1" => Some(true)
"false" | "no" | "off" | "0" => Some(false)
_ => None
}
_ => None
}
}
///|
/// Parse an integer, or `None` when the text is not one.
pub fn try_int_str(s : String) -> Int32? {
Some(@string.parse_int(s.trim())) catch {
_ => None
}
}
///|
/// An integer, accepting ints, floats and numeric strings.
pub fn y_int(v : @yaml.Yaml) -> Int32? {
match v {
@yaml.Yaml::Integer(i) => Some(i.to_int())
@yaml.Yaml::Real(d, ..) => Some(d.to_int())
@yaml.Yaml::String(s) => try_int_str(s)
_ => None
}
}
///|
/// The map behind a `Yaml::Map`, if any.
pub fn y_map(v : @yaml.Yaml) -> Map[String, @yaml.Yaml]? {
match v {
@yaml.Yaml::Map(m) => Some(m)
_ => None
}
}
///|
/// The array behind a `Yaml::Array`, if any.
pub fn y_arr(v : @yaml.Yaml) -> Array[@yaml.Yaml]? {
match v {
@yaml.Yaml::Array(a) => Some(a)
_ => None
}
}
///|
/// Look a key up in a mapping value. `None` for a non-mapping or a missing key.
pub fn y_get(v : @yaml.Yaml, key : String) -> @yaml.Yaml? {
match v {
@yaml.Yaml::Map(m) => m.get(key)
_ => None
}
}
///|
/// The `norm_as_list` of the reference implementation: a scalar becomes a
/// one-element list, a map becomes `["k=v", "k2"]`, null becomes empty.
pub fn y_list(v : @yaml.Yaml) -> Array[@yaml.Yaml] {
match v {
@yaml.Yaml::Null | @yaml.Yaml::BadValue => []
@yaml.Yaml::Array(a) => a
@yaml.Yaml::Map(m) => {
let out : Array[@yaml.Yaml] = []
for entry in m {
let (k, value) = entry
match value {
@yaml.Yaml::Null | @yaml.Yaml::BadValue => out.push(y_str_v(k))
_ => out.push(y_str_v(k + "=" + y_str_or(value, "")))
}
}
out
}
_ => [v]
}
}
///|
/// `y_list`, keeping only the scalars.
pub fn y_list_str(v : @yaml.Yaml) -> Array[String] {
let out : Array[String] = []
for item in y_list(v) {
match y_str(item) {
Some(s) => out.push(s)
None => ()
}
}
out
}
///|
/// The `norm_as_dict` of the reference implementation: `["k=v", "k2"]` becomes
/// `{k: v, k2: null}`. Bare keys keep a `Yaml::Null` value so the original
/// spelling survives a round-trip.
pub fn y_dict(v : @yaml.Yaml) -> Map[String, @yaml.Yaml] {
match v {
@yaml.Yaml::Null | @yaml.Yaml::BadValue => Map([])
@yaml.Yaml::Map(m) => m
@yaml.Yaml::Array(a) => {
let out : Map[String, @yaml.Yaml] = Map([])
for item in a {
match y_str(item) {
Some(s) =>
match s.split_once("=") {
Some(pair) => out[pair.0.to_owned()] = y_str_v(pair.1.to_owned())
None => out[s] = @yaml.Yaml::Null
}
None => ()
}
}
out
}
@yaml.Yaml::String(s) => {
let out : Map[String, @yaml.Yaml] = Map([])
match s.split_once("=") {
Some(pair) => out[pair.0.to_owned()] = y_str_v(pair.1.to_owned())
None => out[s] = @yaml.Yaml::Null
}
out
}
_ => Map([])
}
}
///|
/// Turn a mapping into a list of `k=v` (or bare `k`) strings. This is the
/// `norm_as_list(norm_as_dict(x))` shape `container_to_args` wants.
pub fn y_env_list(v : @yaml.Yaml) -> Array[String] {
let out : Array[String] = []
for entry in y_dict(v) {
let (k, value) = entry
match value {
@yaml.Yaml::Null | @yaml.Yaml::BadValue => out.push(k)
_ => out.push(k + "=" + y_str_or(value, ""))
}
}
out
}
///|
pub fn y_str_v(s : String) -> @yaml.Yaml {
@yaml.Yaml::String(s)
}
///|
pub fn y_int_v(i : Int32) -> @yaml.Yaml {
@yaml.Yaml::Integer(i.to_int64())
}
///|
pub fn y_bool_v(b : Bool) -> @yaml.Yaml {
@yaml.Yaml::Boolean(b)
}
///|
pub fn y_null() -> @yaml.Yaml {
@yaml.Yaml::Null
}
///|
pub fn y_arr_v(a : Array[@yaml.Yaml]) -> @yaml.Yaml {
@yaml.Yaml::Array(a)
}
///|
pub fn y_map_v(m : Map[String, @yaml.Yaml]) -> @yaml.Yaml {
@yaml.Yaml::Map(m)
}
///|
/// Build a mapping value from key/value pairs.
pub fn y_obj(pairs : Array[(String, @yaml.Yaml)]) -> @yaml.Yaml {
let m : Map[String, @yaml.Yaml] = Map([])
for p in pairs {
m[p.0] = p.1
}
@yaml.Yaml::Map(m)
}
///|
/// Deep copy. Merge mutates its target, so every value copied in must be owned.
pub fn y_clone(v : @yaml.Yaml) -> @yaml.Yaml {
match v {
@yaml.Yaml::Map(m) => {
let out : Map[String, @yaml.Yaml] = Map([])
for entry in m {
let (k, value) = entry
out[k] = y_clone(value)
}
@yaml.Yaml::Map(out)
}
@yaml.Yaml::Array(a) => {
let out : Array[@yaml.Yaml] = []
for item in a {
out.push(y_clone(item))
}
@yaml.Yaml::Array(out)
}
_ => v
}
}
///|
/// Render a value as YAML text, without the leading `---` document marker.
pub fn y_dump(v : @yaml.Yaml) -> String {
let text = v.dump()
if text.has_prefix("---\n") {
text[4:].to_owned()
} else {
text
}
}