///|
/// Deterministic top-level serializer: RFC members first, extensions sorted.
pub fn serialize_problem(problem : ProblemDetails) -> String {
  let fields : Array[String] = []
  match problem.type_raw() {
    Some(value) => fields.push("\"type\":\{json_quote(value)}")
    None => ()
  }
  match problem.title() {
    Some(value) => fields.push("\"title\":\{json_quote(value)}")
    None => ()
  }
  match problem.status() {
    Some(value) => fields.push("\"status\":\{value}")
    None => ()
  }
  match problem.detail() {
    Some(value) => fields.push("\"detail\":\{json_quote(value)}")
    None => ()
  }
  match problem.instance() {
    Some(value) => fields.push("\"instance\":\{json_quote(value)}")
    None => ()
  }
  let extensions = problem.extensions()
  extensions.sort_by(fn(a, b) { lexical_compare(a.name(), b.name()) })
  for extension in extensions {
    fields.push(
      "\{json_quote(extension.name())}:\{json_value_string(extension.value())}",
    )
  }
  let body = join_strings(fields, ",")
  "{\{body}}"
}

///|
pub fn canonicalize_problem_json(
  input : String,
) -> Result[String, ProblemError] {
  match parse_problem(input) {
    Ok(problem) => Ok(serialize_problem(problem))
    Err(error) => Err(error)
  }
}

///|
fn join_strings(values : Array[String], separator : String) -> String {
  let builder = StringBuilder()
  for i = 0; i < values.length(); i = i + 1 {
    if i > 0 {
      builder.write_string(separator)
    }
    builder.write_string(values[i])
  }
  builder.to_string()
}