///|
/// Entries are sorted by group name so exports are reproducible.
pub fn GroupScores::to_snapshot(
  self : GroupScores,
) -> String raise ConformalError {
  let entries = []
  for name in self.names {
    entries.push(
      Json::object(
        Map([
          ("name", Json::string(name)),
          (
            "scores",
            Json::array(self.get(name).sorted.map(x => Json::number(x))),
          ),
        ]),
      ),
    )
  }
  Json::object(
    Map([
      ("schema", Json::number(1.0)),
      ("kind", Json::string("groups")),
      ("groups", Json::array(entries)),
    ]),
  ).stringify()
}

///|
/// Applies one global score budget, in addition to per-group validation.
pub fn GroupScores::from_snapshot(
  text : String,
) -> GroupScores raise ConformalError {
  let fields = snapshot_object(snapshot_parse(text), [
    "schema", "kind", "groups",
  ])
  snapshot_version(fields, "groups")
  let values = []
  let labels = []
  let seen : Map[String, Bool] = Map([])
  match snapshot_field(fields, "groups") {
    Array(entries) => {
      if entries.is_empty() || entries.length() > 10000 {
        raise Invalid("snapshot group count outside range")
      }
      for entry in entries {
        let fields = snapshot_object(entry, ["name", "scores"])
        let name = match snapshot_field(fields, "name") {
          String(name) => name
          _ => raise Invalid("snapshot group name must be a string")
        }
        if seen.contains(name) {
          raise Invalid("duplicate snapshot group name")
        }
        seen.set(name, true)
        let scores = snapshot_scores(snapshot_field(fields, "scores"))
        if values.length() + scores.count() > 100000 {
          raise ResourceLimit("total snapshot scores exceed 100000")
        }
        for value in scores.sorted {
          values.push(value)
          labels.push(name)
        }
      }
    }
    _ => raise Invalid("snapshot groups must be an array")
  }
  grouped_scores(values, labels)
}