///|
/// Parse one bounded document. Duplicate keys are rejected before Map construction.
fn codec_parse(text : String, max_depth~ : Int32 = 32) -> Json raise CepError {
  if text.length() > 1048576 {
    raise Resource(resource="JSON UTF-16 code units", limit=1048576)
  }
  @json.parse(text[:], max_nesting_depth=max_depth, reject_duplicate_keys=true) catch {
    err => raise Invalid(path="json", reason=err.to_string())
  }
}

///|
fn codec_object(
  value : Json,
  path : String,
) -> Map[String, Json] raise CepError {
  match value {
    Object(object) => object
    _ => raise Invalid(path~, reason="expected object")
  }
}

///|
fn codec_keys(
  object : Map[String, Json],
  allowed : Array[String],
  path : String,
) -> Unit raise CepError {
  for key, _ in object {
    if !allowed.contains(key) {
      raise Invalid(path="\{path}.\{key}", reason="unknown property")
    }
  }
}

///|
fn codec_required(
  object : Map[String, Json],
  key : String,
  path : String,
) -> Json raise CepError {
  match object.get(key) {
    Some(value) => value
    None =>
      raise Invalid(
        path="\{path}.\{key}",
        reason="required property is missing",
      )
  }
}

///|
fn codec_string(value : Json, path : String) -> String raise CepError {
  match value {
    String(text) => text
    _ => raise Invalid(path~, reason="expected string")
  }
}

///|
fn codec_boolean(value : Json, path : String) -> Bool raise CepError {
  match value {
    True => true
    False => false
    _ => raise Invalid(path~, reason="expected boolean")
  }
}

///|
/// Accumulate negatively so Int64::MIN_VALUE never needs to be negated.
/// Every multiplication and subtraction is checked before it executes.
fn codec_int64(value : Json, path : String) -> Int64 raise CepError {
  let text = codec_string(value, path)
  let chars = text.to_array()
  if chars.length() < 1 || chars.length() > 20 {
    raise Invalid(path~, reason="expected canonical Int64 string")
  }
  let negative = chars[0] == '-'
  let start = if negative { 1 } else { 0 }
  if start == chars.length() {
    raise Invalid(path~, reason="expected digits after minus")
  }
  if chars[start] == '0' && (negative || chars.length() - start > 1) {
    raise Invalid(path~, reason="leading zeros and negative zero are forbidden")
  }
  let limit = if negative {
    -9223372036854775808L
  } else {
    -9223372036854775807L
  }
  let mut total = 0L
  for i in start.. 57 {
      raise Invalid(path~, reason="expected canonical decimal integer digits")
    }
    let digit = (code - 48).to_int64()
    if total < limit / 10L {
      raise Invalid(path~, reason="integer is outside Int64 range")
    }
    let multiplied = total * 10L
    if multiplied < limit + digit {
      raise Invalid(path~, reason="integer is outside Int64 range")
    }
    total = multiplied - digit
  }
  if negative {
    total
  } else {
    -total
  }
}

///|
fn codec_field_path(path : String, key : String) -> String {
  path + "[" + Json::string(key).stringify() + "]"
}