///|
/// Whether a `$schema` value names the draft-07 dialect.
pub fn draft07_dialect(text : String) -> Bool {
  text == "http://json-schema.org/draft-07/schema" ||
  text == "http://json-schema.org/draft-07/schema#" ||
  text == "https://json-schema.org/draft-07/schema" ||
  text == "https://json-schema.org/draft-07/schema#"
}

///|
fn schema_shaped(value : @exact.Value) -> Bool {
  match value.kind() {
    @exact.Object(_) | @exact.Boolean(_) => true
    _ => false
  }
}

///|
/// The draft-07 subschema positions held by one keyword's value, as path
/// suffixes below that keyword. Only object and boolean values are positions;
/// literal keywords such as `const`, `enum` and `default` have none.
/// Shared by schema indexing and validation so both agree on what is a schema.
pub fn draft07_subschemas(
  keyword : String,
  value : @exact.Value,
) -> Array[(Array[String], @exact.Value)] {
  let result = []
  let push = fn(suffix, value) {
    if schema_shaped(value) {
      result.push((suffix, value))
    }
  }
  match keyword {
    "properties" | "definitions" | "patternProperties" | "dependencies" =>
      if value.kind() is @exact.Object(entries) {
        entries.sort_by((a, b) => a.0.lexical_compare(b.0))
        for (key, value) in entries {
          push([key], value)
        }
      }
    "allOf" | "anyOf" | "oneOf" | "items" =>
      if value.kind() is @exact.Array(items) {
        for i, value in items {
          push([i.to_string()], value)
        }
      } else if keyword == "items" {
        push([], value)
      }
    "additionalProperties"
    | "additionalItems"
    | "contains"
    | "propertyNames"
    | "if"
    | "then"
    | "else"
    | "not" => push([], value)
    _ => ()
  }
  result
}