///|
fn rule_kind_from_string(value : StringView) -> SymbolKind? {
  match value {
    "function" => Some(Function)
    "type" | "type_alias" => Some(TypeAlias)
    "struct" => Some(Struct)
    "enum" => Some(Enum)
    "trait" => Some(Trait)
    "value" => Some(Value)
    _ => None
  }
}

///|
fn required_rule_string(
  fields : Map[String, Json],
  key : String,
) -> String raise RuleError {
  match fields.get(key) {
    Some(String(value)) => value
    Some(_) => raise RuleError("Rule field `\{key}` must be a string")
    None => raise RuleError("Rule is missing required field `\{key}`")
  }
}

///|
fn rule_from_json(value : Json) -> MigrationRule? raise RuleError {
  guard value is Object(fields) else {
    raise RuleError("Each rename rule must be a JSON object")
  }
  let old_name = required_rule_string(fields, "from")
  let new_name = required_rule_string(fields, "to")
  let kind_text = match fields.get("kind") {
    Some(String(kind)) => kind
    Some(_) => raise RuleError("Rule field `kind` must be a string")
    None => "function"
  }
  let symbol_kind = match rule_kind_from_string(kind_text) {
    Some(kind) => kind
    None => raise RuleError("Unsupported rule kind `\{kind_text}`")
  }
  Some(MigrationRule::{ symbol_kind, old_name, new_name, })
}

///|
/// Loads migration rules from the documented JSON format.
///
/// Example:
///
/// ```json
/// {
///   "version": 1,
///   "renames": [
///     { "kind": "function", "from": "parse_amount", "to": "parse_money" }
///   ]
/// }
/// ```
pub fn rules_from_json(
  source : StringView,
) -> Array[MigrationRule] raise RuleError {
  let document = @json.parse(source) catch {
    error => raise RuleError("Invalid rules JSON: \{error.to_string()}")
  }
  let entries = match document {
    Object(fields) =>
      match fields.get("renames") {
        Some(Array(values)) => values
        Some(_) => raise RuleError("`renames` must be an array")
        None => raise RuleError("Rules JSON must contain a `renames` array")
      }
    Array(values) => values
    _ => raise RuleError("Rules JSON must be an object or an array")
  }
  let result = []
  for entry in entries {
    if rule_from_json(entry) is Some(rule) {
      result.push(rule)
    }
  }
  result
}

///|
pub fn rules_to_json(rules : Array[MigrationRule]) -> String {
  let builder = StringBuilder()
  builder.write_string("{\n  \"version\": 1,\n  \"renames\": [")
  for index, rule in rules {
    if index > 0 {
      builder.write_char(',')
    }
    builder.write_string(
      "\n    {\"kind\": \"\{symbol_kind_name(rule.symbol_kind)}\", \"from\": \"\{json_escape(rule.old_name)}\", \"to\": \"\{json_escape(rule.new_name)}\"}",
    )
  }
  if !rules.is_empty() {
    builder.write_string("\n  ")
  }
  builder.write_string("]\n}\n")
  builder.to_string()
}