///|
fn required_string(
  o : Map[String, Json],
  field : String,
) -> String raise GedError {
  match o.get(field) {
    Some(String(s)) => s
    _ => raise Query(field + " must be a string")
  }
}

///|
fn error_json(error : Error) -> Json {
  let (code, line, message) = match error {
    GedError::Syntax(line, message) => ("syntax", Some(line), message)
    GedError::Limit(message) => ("limit", None, message)
    GedError::InvalidState(message) => ("state", None, message)
    GedError::Query(message) => ("query", None, message)
    _ => ("invalid_request", None, "invalid JSON request")
  }
  Json::object(
    Map([
      ("ok", Json::boolean(false)),
      ("code", Json::string(code)),
      (
        "line",
        match line {
          Some(n) => Json::number(n.to_double())
          None => Json::null()
        },
      ),
      ("error", Json::string(message)),
    ]),
  )
}

///|
/// Pure host boundary shared by CLI and browser. GEDCOM is a string field parsed
/// independently, so malformed GEDCOM cannot inject fields into the envelope.
pub fn run_json(request : String) -> String {
  try {
    if request.length() > 34000000 {
      raise Limit("request is too large")
    }
    guard @json.parse(request) is Object(o) else {
      raise Query("request must be an object")
    }
    let doc = parse(required_string(o, "text"))
    let result = match required_string(o, "command") {
      "inspect" => inspect_json(doc)
      "nodes" => Json::array(doc.nodes.map(node_json))
      "normalize" => Json::string(serialize(doc))
      "csv" => Json::string(people_csv(doc))
      "people" => Json::array(people(doc).map(person_json))
      "path" => {
        let path = graph(doc).shortest_path(
          required_string(o, "from"),
          required_string(o, "to"),
        )
        match path {
          Some(edges) => Json::array(edges.map(edge_json))
          None => Json::null()
        }
      }
      "ancestors" | "descendants" as command => {
        let g = graph(doc)
        let id = required_string(o, "id")
        let rows = if command == "ancestors" {
          g.ancestors(id)
        } else {
          g.descendants(id)
        }
        Json::array(
          rows.map(fn(r) {
            Json::object(
              Map([
                ("person", Json::string(r.person)),
                ("generations", Json::number(r.generations.to_double())),
              ]),
            )
          }),
        )
      }
      other => raise Query("unknown command " + other)
    }
    Json::object(Map([("ok", Json::boolean(true)), ("result", result)])).stringify()
  } catch {
    error => error_json(error).stringify()
  }
}