///|
/// An optional string as the wire carries it: the value, or `null`.
///
/// Not the field's own `ToJson` — MoonBit encodes `Some(v)` as the one-element
/// array `[v]`, which a string lookup rejects, silently turning a present
/// field into an absent one.
fn or_null(value : String?) -> Json {
  match value {
    Some(value) => Json::string(value)
    None => Json::null()
  }
}

///|
/// An array of strings as a JSON array.
fn strings_to_json(values : Array[String]) -> Json {
  Json::array(values.map(Json::string))
}

///|
/// A required string field. `None` when absent or not a string.
fn text(fields : Map[String, Json], key : String) -> String? {
  match fields.get(key) {
    Some(String(value)) => Some(value)
    _ => None
  }
}

///|
/// An optional string field: present-and-string, or absent/null.
///
/// Distinct from `text` in that a `null` is a successful read of "no value"
/// rather than a malformed line, so callers cannot conflate the two.
fn opt_text(fields : Map[String, Json], key : String) -> String? {
  match fields.get(key) {
    Some(String(value)) => Some(value)
    _ => None
  }
}

///|
/// A required integer field. JSON has one number type, so a whole-valued
/// double is accepted and anything fractional is not.
fn int(fields : Map[String, Json], key : String) -> Int32? {
  match fields.get(key) {
    Some(Number(value, ..)) => {
      let truncated = value.to_int()
      if truncated.to_double() == value {
        Some(truncated)
      } else {
        None
      }
    }
    _ => None
  }
}

///|
/// A required array-of-strings field. Absent reads as empty: every producer of
/// these lists writes them, and an empty list and a missing one mean the same
/// thing to every renderer.
fn strings(fields : Map[String, Json], key : String) -> Array[String] {
  match fields.get(key) {
    Some(Array(items)) => {
      let out : Array[String] = []
      for item in items {
        if item is String(value) {
          out.push(value)
        }
      }
      out
    }
    _ => []
  }
}

///|
/// A required nested object field.
fn object(fields : Map[String, Json], key : String) -> Map[String, Json]? {
  match fields.get(key) {
    Some(Object(nested)) => Some(nested)
    _ => None
  }
}