///|
fn parse_document(text : String) -> Json raise PricingError {
  if text.length() > 1048576 {
    invalid("size", "$", "JSON input exceeds 1048576 UTF-16 code units")
  }
  let value = @json.parse(text, max_nesting_depth=32) catch {
    @json.DepthLimitExceeded =>
      raise Invalid(
        code="depth",
        path="$",
        message="maximum JSON nesting depth is 32",
      )
    e => raise Invalid(code="json", path="$", message="invalid JSON: \{e}")
  }
  // Core's Json object model is a map. Inspect original string tokens as well,
  // so duplicate names (including escaped aliases) cannot be silently replaced.
  let objects : Array[Map[String, Unit]] = []
  let mut i = 0
  let mut depth = 0
  while i < text.length() {
    match text[i] {
      '{' => {
        objects.push(Map([]))
        depth += 1
        i += 1
      }
      '}' => {
        ignore(objects.pop())
        depth -= 1
        i += 1
      }
      '[' => {
        depth += 1
        i += 1
      }
      ']' => {
        depth -= 1
        i += 1
      }
      '"' => {
        let start = i
        i += 1
        while i < text.length() && text[i] != '"' {
          i += if text[i] == '\\' { 2 } else { 1 }
        }
        i += 1
        let end = i
        while i < text.length() &&
              (
                text[i] == ' ' ||
                text[i] == '\n' ||
                text[i] == '\r' ||
                text[i] == '\t'
              ) {
          i += 1
        }
        if i < text.length() && text[i] == ':' {
          let decoded = @json.parse(text[start:end]) catch {
            _ =>
              raise Invalid(
                code="json",
                path="$",
                message="invalid object name",
              )
          }
          if decoded is String(key) {
            let names = objects[objects.length() - 1]
            if names.contains(key) {
              invalid(
                "duplicate",
                "$",
                "duplicate key '\{key}' at UTF-16 offset \{start}",
              )
            }
            names[key] = ()
          }
        }
      }
      _ => i += 1
    }
    if depth > 32 {
      invalid("depth", "$", "maximum JSON nesting depth is 32")
    }
  }
  value
}

///|
fn fields(
  value : Json,
  names : ArrayView[String],
  path : String,
) -> Map[String, Json] raise PricingError {
  guard value is Object(object) else {
    invalid("type", path, "expected an object")
    return Map([])
  }
  for key, _ in object {
    if !names.contains(key) {
      invalid(
        "unknown",
        path + "." + key,
        "unsupported field; input is rejected rather than discarded",
      )
    }
  }
  for name in names {
    if !object.contains(name) {
      invalid("missing", path + "." + name, "required field is missing")
    }
  }
  object
}

///|
fn text_value(value : Json, path : String) -> String raise PricingError {
  guard value is String(text) else {
    invalid(
      "type", path, "expected a string; quantities and prices must not be JSON numbers",
    )
    return ""
  }
  text
}

///|
fn expect_value(
  object : Map[String, Json],
  key : String,
  expected : String,
  path : String,
) -> Unit raise PricingError {
  if text_value(object[key], path + "." + key) != expected {
    invalid("unsupported", path + "." + key, "only '\{expected}' is supported")
  }
}