///|
/// Validate GeoJSON coordinate nesting and minimum geometry cardinalities.
/// It does not test ring self-intersections or winding order.
fn validate_geojson_geometry(v : Json, depth : Int) -> Unit raise StacError {
  if depth > 8 {
    raise Invalid("geometry too deeply nested")
  }
  let kind = string_field(v, "type")
  if kind == "GeometryCollection" {
    let geometries = array_field(v, "geometries")
    if geometries.length() == 0 {
      raise Invalid("empty GeometryCollection")
    }
    for geometry in geometries {
      validate_geojson_geometry(geometry, depth + 1)
    }
    return
  }
  let coordinates = match field(v, "coordinates") {
    Some(value) => value
    None => raise Invalid("missing coordinates")
  }
  match kind {
    "Point" => ignore(position_coords(coordinates))
    "MultiPoint" => {
      let points = required_array(coordinates, "MultiPoint")
      if points.length() == 0 {
        raise Invalid("empty MultiPoint")
      }
      for point in points {
        ignore(position_coords(point))
      }
    }
    "LineString" => validate_line(coordinates)
    "MultiLineString" => {
      let lines = required_array(coordinates, "MultiLineString")
      if lines.length() == 0 {
        raise Invalid("empty MultiLineString")
      }
      for line in lines {
        validate_line(line)
      }
    }
    "Polygon" => validate_polygon(coordinates)
    "MultiPolygon" => {
      let polygons = required_array(coordinates, "MultiPolygon")
      if polygons.length() == 0 {
        raise Invalid("empty MultiPolygon")
      }
      for polygon in polygons {
        validate_polygon(polygon)
      }
    }
    _ => raise Invalid("invalid geometry type")
  }
}

///|
fn required_array(v : Json, label : String) -> Array[Json] raise StacError {
  match v {
    Array(xs) => xs
    _ => raise Invalid("invalid " + label + " coordinates")
  }
}

///|
fn array_field(v : Json, name : String) -> Array[Json] raise StacError {
  match field(v, name) {
    Some(Array(xs)) => xs
    _ => raise Invalid("invalid " + name)
  }
}

///|
fn position_coords(v : Json) -> Array[Double] raise StacError {
  let values = required_array(v, "position")
  if values.length() != 2 && values.length() != 3 {
    raise Invalid("position must have two or three numbers")
  }
  let coords : Array[Double] = []
  for value in values {
    match value {
      Number(n, ..) => coords.push(n)
      _ => raise Invalid("non-numeric position coordinate")
    }
  }
  coords
}

///|
fn validate_line(v : Json) -> Unit raise StacError {
  let points = required_array(v, "LineString")
  if points.length() < 2 {
    raise Invalid("LineString requires two positions")
  }
  for point in points {
    ignore(position_coords(point))
  }
}

///|
fn validate_polygon(v : Json) -> Unit raise StacError {
  let rings = required_array(v, "Polygon")
  if rings.length() == 0 {
    raise Invalid("Polygon requires a ring")
  }
  for ring in rings {
    validate_ring(ring)
  }
}

///|
fn validate_ring(v : Json) -> Unit raise StacError {
  let points = required_array(v, "linear ring")
  if points.length() < 4 {
    raise Invalid("linear ring requires four positions")
  }
  let first = position_coords(points[0])
  for point in points {
    ignore(position_coords(point))
  }
  let last = position_coords(points[points.length() - 1])
  if first.length() != last.length() {
    raise Invalid("linear ring is not closed")
  }
  for i in 0..