///|
fn validate_keys(ctx : Context, value : Node, allowed : Array[String]) -> Unit {
  for key in keys(value.raw) {
    if !allowed.contains(key) && !key.has_prefix("x-") {
      ctx.diagnose(
        "unsupported-field",
        "unsupported",
        value.child(key, field(value.raw, key).unwrap()),
        "Field is outside the OpenAPI V1 profile: " + key,
      )
    }
  }
}

// Expand reachable response references for change detection, without claiming response compatibility analysis.

///|
fn resolved_response(
  ctx : Context,
  raw : Node,
  name_map : Bool,
  depth : Int,
) -> Json {
  guard ctx.visit(raw) else { return Json::null() }
  if depth > 64 {
    ctx.diagnose(
      "response-depth-limit", "unsupported", raw, "Response reference traversal exceeds 64 levels.",
    )
    return Json::null()
  }
  let value = if name_map {
    raw
  } else {
    match ctx.resolve(raw) {
      Some(n) => n
      None => return Json::null()
    }
  }
  match value.raw {
    Object(fields) => {
      let result : Map[String, Json] = Map([])
      for key in keys(value.raw) {
        if ctx.budget.exhausted {
          break
        }
        let item = fields[key]
        if name_map || !ignored_key(key) {
          let child_map = !name_map &&
            ["properties", "headers", "content", "links", "responses"].contains(
              key,
            )
          // Link parameters and requestBody contain arbitrary literal values or
          // runtime expressions: their business keys and $ref strings are data.
          result[key] = if !name_map &&
            ["default", "enum", "parameters", "requestBody"].contains(key) {
            item
          } else {
            resolved_response(ctx, value.child(key, item), child_map, depth + 1)
          }
        }
      }
      Json::object(result)
    }
    Array(items) => {
      let result : Array[Json] = []
      for index, item in items {
        if ctx.budget.exhausted {
          break
        }
        result.push(
          resolved_response(
            ctx,
            value.child(index.to_string(), item),
            false,
            depth + 1,
          ),
        )
      }
      Json::array(result)
    }
    _ => value.raw
  }
}

///|
fn numeric_constraints(ctx : Context, value : Node) -> Unit {
  for
    key in [
      "minimum", "maximum", "multipleOf", "minLength", "maxLength", "minProperties",
      "maxProperties",
    ] {
    match field(value.raw, key) {
      None => ()
      Some(Number(number, ..)) => {
        if ["minLength", "maxLength", "minProperties", "maxProperties"].contains(
            key,
          ) &&
          (number < 0 || number != number.floor()) {
          ctx.diagnose(
            "invalid-schema",
            "error",
            value,
            key + " must be a nonnegative integer.",
          )
        }
        if key == "multipleOf" && number <= 0 {
          ctx.diagnose(
            "invalid-schema", "error", value, "multipleOf must be positive.",
          )
        }
      }
      _ =>
        ctx.diagnose(
          "invalid-schema",
          "error",
          value,
          key + " must be numeric.",
        )
    }
  }
  for key in ["format", "pattern"] {
    match field(value.raw, key) {
      None | Some(String(_)) => ()
      _ =>
        ctx.diagnose(
          "invalid-schema",
          "error",
          value,
          key + " must be a string.",
        )
    }
  }
}