///|
/// Normalize user-authored contract metadata without changing its intent.
pub fn normalize_contract(contract : Contract) -> Contract {
  {
    name: contract.name.trim().to_owned(),
    version: contract.version.trim().to_owned(),
    fields: contract.fields.map(normalize_field),
    rules: contract.rules.map(normalize_rule),
  }
}

///|
fn normalize_field(field : FieldSpec) -> FieldSpec {
  {
    name: field.name.trim().to_owned(),
    kind: field.kind,
    required: field.required,
    nullable: field.nullable,
    allowed_values: unique_strings(
      field.allowed_values.map(fn(value) { value.trim().to_owned() }),
    ),
    min_int: field.min_int,
    max_int: field.max_int,
    pattern: field.pattern.map(fn(value) { value.trim().to_owned() }),
    description: field.description.trim().to_owned(),
  }
}

///|
fn normalize_rule(rule : Rule) -> Rule {
  match rule {
    Unique(fields~, severity~, message~) =>
      Unique(
        fields=fields.map(fn(field) { field.trim().to_owned() }),
        severity~,
        message=message.trim().to_owned(),
      )
    Completeness(field~, min_percent~, severity~, message~) =>
      Completeness(
        field=field.trim().to_owned(),
        min_percent~,
        severity~,
        message=message.trim().to_owned(),
      )
    Enum(field~, allowed_values~, severity~, message~) =>
      Enum(
        field=field.trim().to_owned(),
        allowed_values=unique_strings(
          allowed_values.map(fn(value) { value.trim().to_owned() }),
        ),
        severity~,
        message=message.trim().to_owned(),
      )
    IntRange(field~, min~, max~, severity~, message~) =>
      IntRange(
        field=field.trim().to_owned(),
        min~,
        max~,
        severity~,
        message=message.trim().to_owned(),
      )
    CompareInts(left~, op~, right~, severity~, message~) =>
      CompareInts(
        left=left.trim().to_owned(),
        op~,
        right=right.trim().to_owned(),
        severity~,
        message=message.trim().to_owned(),
      )
    PatternMatch(field~, pattern~, severity~, message~) =>
      PatternMatch(
        field=field.trim().to_owned(),
        pattern=pattern.trim().to_owned(),
        severity~,
        message=message.trim().to_owned(),
      )
    StringLength(field~, min~, max~, severity~, message~) =>
      StringLength(
        field=field.trim().to_owned(),
        min~,
        max~,
        severity~,
        message=message.trim().to_owned(),
      )
    DistinctCount(field~, min~, max~, severity~, message~) =>
      DistinctCount(
        field=field.trim().to_owned(),
        min~,
        max~,
        severity~,
        message=message.trim().to_owned(),
      )
    RequiredIf(condition_field~, condition_value~, field~, severity~, message~) =>
      RequiredIf(
        condition_field=condition_field.trim().to_owned(),
        condition_value=condition_value.trim().to_owned(),
        field=field.trim().to_owned(),
        severity~,
        message=message.trim().to_owned(),
      )
    RowCount(min~, max~, severity~, message~) =>
      RowCount(min~, max~, severity~, message=message.trim().to_owned())
  }
}