///|
fn document_object(
  value : Json,
  allowed : Array[String],
  required : Array[String],
  context : String,
) -> Map[String, Json] raise FormatError {
  guard value is Object(fields) else {
    raise Invalid(context + " must be an object")
  }
  for key in sorted_map_keys(fields) {
    guard allowed.contains(key) else {
      raise Invalid(context + ": unknown field " + key)
    }
  }
  for key in required {
    guard fields.contains(key) else {
      raise Invalid(context + ": missing field " + key)
    }
  }
  fields
}

///|
fn document_text(value : Json, context : String) -> String raise FormatError {
  match value {
    String(text) => text
    _ => raise Invalid(context + " must be a string")
  }
}

///|
fn document_array(
  value : Json,
  context : String,
) -> Array[Json] raise FormatError {
  match value {
    Array(items) => items
    _ => raise Invalid(context + " must be an array")
  }
}

///|
fn document_bool(value : Json, context : String) -> Bool raise FormatError {
  match value {
    True => true
    False => false
    _ => raise Invalid(context + " must be a boolean")
  }
}

///|
/// Decimal strings avoid JSON double rounding above 2^53.
fn document_count(value : Json, context : String) -> Int64 raise FormatError {
  let text = document_text(value, context)
  guard text != "" && uri_digits(text) && (text == "0" || !text.has_prefix("0")) else {
    raise Invalid(context + " must be a canonical nonnegative decimal string")
  }
  @string.parse_int64(text[:]) catch {
    _ => raise Invalid(context + " exceeds Int64 range")
  }
}

///|
fn document_strings(
  value : Json,
  context : String,
  unique~ : Bool = true,
) -> Array[String] raise FormatError {
  let result : Array[String] = []
  let seen : Map[String, Bool] = Map([])
  for item in document_array(value, context) {
    let text = document_text(item, context)
    guard !unique || !seen.contains(text) else {
      raise Invalid(context + ": duplicate value " + text)
    }
    seen[text] = true
    result.push(text)
  }
  result
}

///|
fn checked_add(
  a : Int64,
  b : Int64,
  context : String,
) -> Int64 raise FormatError {
  guard a >= 0L && b >= 0L && b <= 9223372036854775807L - a else {
    raise Invalid(context + ": nonnegative Int64 total overflow")
  }
  a + b
}

///|
fn normalized_digests(
  digests : Map[String, String],
  context : String,
) -> Map[String, String] raise FormatError {
  let result : Map[String, String] = Map([])
  for name in sorted_map_keys(digests) {
    let alg = algorithm(name)
    let digest = digests[name]
    guard valid_checksum(digest, alg) else {
      raise Invalid(context + ": invalid " + name + " digest")
    }
    result[name] = digest[:].to_lower().to_owned()
  }
  result
}

///|
fn document_record(value : Json) -> FileRecord raise FormatError {
  let fields = document_object(
    value,
    ["path", "size", "digests"],
    ["path", "size", "digests"],
    "file record",
  )
  let path = document_text(fields["path"], "file path")
  check_path(path)
  let size = document_count(fields["size"], "file size")
  guard fields["digests"] is Object(raw) else {
    raise Invalid("file digests must be an object")
  }
  let digests : Map[String, String] = Map([])
  for name in sorted_map_keys(raw) {
    digests[name] = document_text(raw[name], "digest")
  }
  { path, size, digests: normalized_digests(digests, path), }
}

///|
fn record_document(file : FileRecord) -> Json {
  {
    "path": file.path,
    "size": file.size.to_string(),
    "digests": Json(file.digests),
  }
}