// JSON plumbing shared by every endpoint.
//
// Two jobs live here: building request bodies whose keys are Exa's camelCase
// (while the MoonBit API stays snake_case) and reading response bodies
// leniently, so a field Exa adds or stops sending does not break decoding.

///|
/// A JSON object under construction. `set_opt` drops `None`, so unset optional
/// arguments never reach the wire.
priv struct JsonObject {
  fields : Map[String, Json]
}

///|
fn JsonObject::new() -> JsonObject {
  { fields: {}, }
}

///|
fn JsonObject::set(self : JsonObject, key : String, value : Json) -> Unit {
  self.fields[key] = value
}

///|
fn[T : ToJson] JsonObject::set_opt(
  self : JsonObject,
  key : String,
  value : T?,
) -> Unit {
  match value {
    Some(v) => self.fields[key] = v.to_json()
    None => ()
  }
}

///|
fn JsonObject::build(self : JsonObject) -> Json {
  Json::object(self.fields)
}

// -- decoding ---------------------------------------------------------------

///|
fn field(j : Json, key : String) -> Json? {
  if j is Object(obj) {
    obj.get(key)
  } else {
    None
  }
}

///|
fn get_str(j : Json, key : String) -> String? {
  if field(j, key) is Some(String(s)) {
    Some(s)
  } else {
    None
  }
}

///|
fn get_double(j : Json, key : String) -> Double? {
  if field(j, key) is Some(Number(n, ..)) {
    Some(n)
  } else {
    None
  }
}

///|
fn get_int(j : Json, key : String) -> Int? {
  get_double(j, key).map(fn(d) { d.to_int() })
}

///|
/// An array field, or `[]` when absent, null, or not an array. Exa omits these
/// entirely unless the corresponding option was requested.
fn get_array(j : Json, key : String) -> Array[Json] {
  if field(j, key) is Some(Array(a)) {
    a
  } else {
    []
  }
}

///|
fn get_str_array(j : Json, key : String) -> Array[String] {
  let out = []
  for item in get_array(j, key) {
    if item is String(s) {
      out.push(s)
    }
  }
  out
}

///|
fn get_double_array(j : Json, key : String) -> Array[Double] {
  let out = []
  for item in get_array(j, key) {
    if item is Number(n, ..) {
      out.push(n)
    }
  }
  out
}

///|
/// A required string field. Only a response shape we cannot work with at all is
/// an error; everything else decodes to `None`.
fn get_str_req(j : Json, key : String, ctx : String) -> String raise ExaError {
  match get_str(j, key) {
    Some(s) => s
    None => raise Decode("\{ctx}: missing or non-string field `\{key}`")
  }
}