///|
/// Aggregated result of semantic validation.
pub(all) struct ValidationReport {
  issues : Array[ValidationIssue]
  error_count : Int
  warning_count : Int
} derive(Eq)

///|
/// Return whether the document passed without semantic errors.
pub fn ValidationReport::is_valid(self : ValidationReport) -> Bool {
  self.error_count == 0
}

///|
fn is_valid_unit(unit : StringView) -> Bool {
  if unit.is_empty() {
    return false
  }
  for char in unit.iter() {
    if !(char.is_ascii_alphabetic() ||
      char.is_ascii_digit() ||
      char == '_' ||
      char == ':') {
      return false
    }
  }
  true
}

///|
fn labels_have_name(labels : Array[Label], name : String) -> Bool {
  for label in labels {
    if label.name == name {
      return true
    }
  }
  false
}

///|
fn same_label_set(left : Array[Label], right : Array[Label]) -> Bool {
  if left.length() != right.length() {
    return false
  }
  for label in left {
    let mut found = false
    for candidate in right {
      if candidate == label {
        found = true
        break
      }
    }
    if !found {
      return false
    }
  }
  true
}

///|
fn same_label_group_ignoring(
  left : Array[Label],
  right : Array[Label],
  ignored : String,
) -> Bool {
  let mut left_count = 0
  let mut right_count = 0
  for label in left {
    if label.name != ignored {
      left_count += 1
    }
  }
  for label in right {
    if label.name != ignored {
      right_count += 1
    }
  }
  if left_count != right_count {
    return false
  }
  for label in left {
    if label.name == ignored {
      continue
    }
    let mut found = false
    for candidate in right {
      if candidate.name != ignored && candidate == label {
        found = true
        break
      }
    }
    if !found {
      return false
    }
  }
  true
}

///|
fn metric_special_label(family : MetricFamily) -> String? {
  match family.metric_type {
    Histogram | GaugeHistogram => Some("le")
    Summary => Some("quantile")
    StateSet => Some(family.name)
    Unknown | Counter | Gauge | Info => None
  }
}

///|
fn same_metric_identity(
  family : MetricFamily,
  left : Sample,
  right : Sample,
) -> Bool {
  match metric_special_label(family) {
    Some(label) => same_label_group_ignoring(left.labels, right.labels, label)
    None => same_label_set(left.labels, right.labels)
  }
}

///|
fn label_number(sample : Sample, name : StringView) -> Double? {
  match sample.label(name) {
    None => None
    Some(value) =>
      match parse_number(value) {
        Ok(number) => Some(number)
        Err(_) => None
      }
  }
}

///|
fn is_finite_number(value : Double) -> Bool {
  !value.is_nan() && value != @double.infinity && value != -@double.infinity
}

///|
fn is_nonnegative_integer(value : Double) -> Bool {
  is_finite_number(value) && value >= 0.0 && value.trunc() == value
}

///|
fn same_point_timestamp(left : Sample, right : Sample) -> Bool {
  match (left.timestamp, right.timestamp) {
    (None, None) => true
    (Some(left), Some(right)) => left == right
    _ => false
  }
}

///|
fn validate_label_array(
  labels : Array[Label],
  family : String,
  sample : String,
  location : String,
  issues : Array[ValidationIssue],
) -> Unit {
  for index in 0.. Unit {
  validate_label_array(
    exemplar.labels,
    family.name,
    sample.name,
    "exemplar",
    issues,
  )
  let mut exemplar_size = 0
  for label in exemplar.labels {
    exemplar_size += label.name.char_length() + label.value.char_length()
  }
  if exemplar_size > 128 {
    issues.push(
      ValidationIssue::error(
        "exemplar_label_limit",
        family.name,
        "combined exemplar label names and values exceed 128 characters",
        sample=sample.name,
      ),
    )
  }
  let placement_ok = match family.metric_type {
    Counter => sample.name == "\{family.name}_total"
    Histogram | GaugeHistogram => sample.name == "\{family.name}_bucket"
    _ => false
  }
  if !placement_ok {
    issues.push(
      ValidationIssue::error(
        "exemplar_metric_type",
        family.name,
        "exemplars are only accepted on counter totals or histogram buckets",
        sample=sample.name,
      ),
    )
  }
  if sample.name == "\{family.name}_bucket" {
    match label_number(sample, "le") {
      Some(bound) =>
        if exemplar.value.is_nan() ||
          (!bound.is_nan() && exemplar.value > bound) {
          issues.push(
            ValidationIssue::error(
              "exemplar_outside_bucket",
              family.name,
              "exemplar value must be non-NaN and within its bucket upper bound",
              sample=sample.name,
            ),
          )
        }
      None => ()
    }
  }
  match exemplar.timestamp {
    Some(timestamp) =>
      if !is_finite_number(timestamp) {
        issues.push(
          ValidationIssue::error(
            "invalid_exemplar_timestamp",
            family.name,
            "exemplar timestamp must be a finite Unix-second value",
            sample=sample.name,
          ),
        )
      }
    None => ()
  }
}

///|
fn validate_histogram_order(
  family : MetricFamily,
  issues : Array[ValidationIssue],
) -> Unit {
  let bucket_name = "\{family.name}_bucket"
  let count_name = if family.metric_type == GaugeHistogram {
    "\{family.name}_gcount"
  } else {
    "\{family.name}_count"
  }
  for index in 0.. value
      None => continue
    }
    for other_index in (index + 1).. value
        None => continue
      }
      if bound > other_bound {
        issues.push(
          ValidationIssue::error(
            "unsorted_buckets",
            family.name,
            "histogram buckets must appear in increasing bound order",
            sample=bucket.name,
          ),
        )
      }
      if bound == other_bound {
        issues.push(
          ValidationIssue::error(
            "duplicate_bucket_bound",
            family.name,
            "one histogram label group repeats bucket bound \{format_number(bound)}",
            sample=bucket.name,
          ),
        )
      } else if (bound < other_bound && bucket.value > other.value) ||
        (other_bound < bound && other.value > bucket.value) {
        issues.push(
          ValidationIssue::error(
            "nonmonotonic_buckets",
            family.name,
            "cumulative histogram buckets decrease as their bounds increase",
            sample=bucket.name,
          ),
        )
      }
    }
    if bound == @double.infinity {
      let mut matching_count : Sample? = None
      for candidate in family.samples {
        if candidate.name == count_name &&
          same_point_timestamp(bucket, candidate) &&
          same_label_group_ignoring(bucket.labels, candidate.labels, "le") {
          matching_count = Some(candidate)
          break
        }
      }
      match matching_count {
        Some(count) =>
          if !bucket.value.is_nan() &&
            !count.value.is_nan() &&
            bucket.value != count.value {
            issues.push(
              ValidationIssue::error(
                "histogram_count_mismatch",
                family.name,
                "positive-infinity bucket must equal the family count",
                sample=bucket.name,
              ),
            )
          }
        None => ()
      }
    }
  }
}

///|
fn validate_histogram_components(
  family : MetricFamily,
  issues : Array[ValidationIssue],
) -> Unit {
  let bucket_name = "\{family.name}_bucket"
  let sum_name = if family.metric_type == GaugeHistogram {
    "\{family.name}_gsum"
  } else {
    "\{family.name}_sum"
  }
  let count_name = if family.metric_type == GaugeHistogram {
    "\{family.name}_gcount"
  } else {
    "\{family.name}_count"
  }
  for index in 0.. {
            if bound == @double.infinity {
              has_positive_infinity_bucket = true
            }
            if bound < 0.0 {
              has_negative_bucket = true
            }
          }
          None => ()
        }
      } else if candidate.name == sum_name {
        has_sum = true
      } else if candidate.name == count_name {
        has_count = true
      }
    }
    if !has_bucket {
      let code = if family.metric_type == GaugeHistogram {
        "incomplete_gaugehistogram"
      } else {
        "missing_histogram_bucket"
      }
      issues.push(
        ValidationIssue::error(
          code,
          family.name,
          "histogram metric point has no bucket samples",
          sample=anchor.name,
        ),
      )
    } else if !has_positive_infinity_bucket {
      issues.push(
        ValidationIssue::error(
          "missing_infinity_bucket",
          family.name,
          "histogram metric point requires an le=\"+Inf\" bucket",
          sample=anchor.name,
        ),
      )
    }
    if has_sum != has_count {
      let code = if family.metric_type == GaugeHistogram {
        "incomplete_gaugehistogram"
      } else {
        "incomplete_histogram"
      }
      let suffixes = if family.metric_type == GaugeHistogram {
        "_gsum and _gcount"
      } else {
        "_sum and _count"
      }
      issues.push(
        ValidationIssue::error(
          code,
          family.name,
          "\{suffixes} must either both be present or both be absent in one metric point",
          sample=anchor.name,
        ),
      )
    }
    if family.metric_type == Histogram && has_negative_bucket && has_sum {
      issues.push(
        ValidationIssue::error(
          "sum_with_negative_bucket",
          family.name,
          "histograms with negative bucket thresholds must not expose _sum",
          sample=anchor.name,
        ),
      )
    }
  }
}

///|
fn validate_summary_order(
  family : MetricFamily,
  issues : Array[ValidationIssue],
) -> Unit {
  for index in 0.. value
      None => continue
    }
    for other_index in (index + 1).. number
        None => continue
      }
      if value == other_value {
        issues.push(
          ValidationIssue::error(
            "duplicate_quantile",
            family.name,
            "one summary label group repeats quantile \{format_number(value)}",
            sample=quantile.name,
          ),
        )
      } else if (value < other_value && quantile.value > other.value) ||
        (other_value < value && other.value > quantile.value) {
        issues.push(
          ValidationIssue::error(
            "nonmonotonic_quantiles",
            family.name,
            "summary values decrease as quantiles increase",
            sample=quantile.name,
          ),
        )
      }
    }
  }
}

///|
fn validate_summary_points(
  family : MetricFamily,
  issues : Array[ValidationIssue],
) -> Unit {
  let count_name = "\{family.name}_count"
  for count in family.samples {
    if count.name != count_name || count.value != 0.0 {
      continue
    }
    for quantile in family.samples {
      if quantile.name == family.name &&
        same_point_timestamp(count, quantile) &&
        same_metric_identity(family, count, quantile) &&
        !quantile.value.is_nan() {
        issues.push(
          ValidationIssue::error(
            "quantile_without_observations",
            family.name,
            "summary quantiles must be NaN when the matching count is zero",
            sample=quantile.name,
          ),
        )
      }
    }
  }
}

///|
fn validate_counter_points(
  family : MetricFamily,
  issues : Array[ValidationIssue],
) -> Unit {
  let total_name = "\{family.name}_total"
  let created_name = "\{family.name}_created"
  for index in 0..
          match previous_time {
            None => {
              previous_time = Some(candidate_time)
              previous_total = Some(candidate)
            }
            Some(latest) =>
              if candidate_time > latest {
                previous_time = Some(candidate_time)
                previous_total = Some(candidate)
              }
          }
        None => ()
      }
    }
    match previous_total {
      Some(previous) =>
        if sample.value < previous.value {
          let mut reset_ok = sample.value == 0.0
          if reset_ok {
            reset_ok = false
            let previous_point_time = match previous.timestamp {
              Some(value) => value
              None => current_time
            }
            for candidate in family.samples {
              if candidate.name == created_name &&
                same_label_set(candidate.labels, sample.labels) &&
                same_point_timestamp(candidate, sample) &&
                candidate.value > previous_point_time &&
                candidate.value <= current_time {
                reset_ok = true
                break
              }
            }
          }
          if !reset_ok {
            issues.push(
              ValidationIssue::error(
                "counter_decrease",
                family.name,
                "counter totals must not decrease without a zero reset and a created time within the sampling interval",
                sample=sample.name,
              ),
            )
          }
        }
      None => ()
    }
  }
}

///|
fn validate_created_values(
  family : MetricFamily,
  issues : Array[ValidationIssue],
) -> Unit {
  match family.metric_type {
    Counter | Histogram | Summary => {
      let created_name = "\{family.name}_created"
      for sample in family.samples {
        if sample.name == created_name && !is_finite_number(sample.value) {
          issues.push(
            ValidationIssue::error(
              "invalid_created",
              family.name,
              "created values must be finite Unix-second timestamps",
              sample=sample.name,
            ),
          )
        }
      }
    }
    Unknown | Gauge | GaugeHistogram | Info | StateSet => ()
  }
}

///|
fn validate_metric_sequence(
  family : MetricFamily,
  issues : Array[ValidationIssue],
) -> Unit {
  for index in 0.. {
            issues.push(
              ValidationIssue::error(
                "mixed_metric_point_timestamps",
                family.name,
                "multiple points for one metric must all have explicit timestamps",
                sample=sample.name,
              ),
            )
            break
          }
          _ => ()
        }
      }
    }
    if index > 0 {
      let previous = family.samples[index - 1]
      if !same_metric_identity(family, previous, sample) {
        for earlier in 0..<(index - 1) {
          if same_metric_identity(family, family.samples[earlier], sample) {
            issues.push(
              ValidationIssue::error(
                "interleaved_metrics",
                family.name,
                "samples from one metric must remain contiguous",
                sample=sample.name,
              ),
            )
            break
          }
        }
      } else if !same_point_timestamp(previous, sample) {
        for earlier in 0..<(index - 1) {
          if same_metric_identity(family, family.samples[earlier], sample) &&
            same_point_timestamp(family.samples[earlier], sample) {
            issues.push(
              ValidationIssue::error(
                "interleaved_metric_points",
                family.name,
                "samples from one metric point must remain contiguous",
                sample=sample.name,
              ),
            )
            break
          }
        }
      }
    }
    match sample.timestamp {
      Some(current_time) =>
        for previous in 0..
              if current_time < previous_time {
                issues.push(
                  ValidationIssue::error(
                    "nonmonotonic_metric_points",
                    family.name,
                    "metric points require monotonically increasing timestamps",
                    sample=sample.name,
                  ),
                )
                break
              }
            None => ()
          }
        }
      None => ()
    }
  }
}

///|
fn validate_family_shape(
  family : MetricFamily,
  issues : Array[ValidationIssue],
) -> Unit {
  let mut has_counter_total = false
  for sample in family.samples {
    if (family.metric_type == Histogram || family.metric_type == GaugeHistogram) &&
      sample.name != "\{family.name}_bucket" &&
      labels_have_name(sample.labels, "le") {
      issues.push(
        ValidationIssue::error(
          "reserved_component_label",
          family.name,
          "histogram metric labels must not contain the reserved 'le' label",
          sample=sample.name,
        ),
      )
    }
    if family.metric_type == Summary &&
      sample.name != family.name &&
      labels_have_name(sample.labels, "quantile") {
      issues.push(
        ValidationIssue::error(
          "reserved_component_label",
          family.name,
          "summary metric labels must not contain the reserved 'quantile' label",
          sample=sample.name,
        ),
      )
    }
    match family.metric_type {
      Counter =>
        if sample.name == "\{family.name}_total" {
          has_counter_total = true
          if sample.value.is_nan() || sample.value < 0.0 {
            issues.push(
              ValidationIssue::error(
                "negative_counter",
                family.name,
                "counter totals must be non-NaN and non-negative",
                sample=sample.name,
              ),
            )
          }
        }
      Histogram | GaugeHistogram => {
        if sample.name == "\{family.name}_bucket" {
          match sample.label("le") {
            None =>
              issues.push(
                ValidationIssue::error(
                  "missing_bucket_bound",
                  family.name,
                  "histogram buckets require an 'le' label",
                  sample=sample.name,
                ),
              )
            Some(bound) =>
              match parse_number(bound) {
                Err(_) =>
                  issues.push(
                    ValidationIssue::error(
                      "invalid_bucket_bound",
                      family.name,
                      "bucket bound '\{bound}' is not numeric",
                      sample=sample.name,
                    ),
                  )
                Ok(value) =>
                  if value.is_nan() {
                    issues.push(
                      ValidationIssue::error(
                        "invalid_bucket_bound",
                        family.name,
                        "bucket bounds must not be NaN",
                        sample=sample.name,
                      ),
                    )
                  }
              }
          }
          if !is_nonnegative_integer(sample.value) {
            issues.push(
              ValidationIssue::error(
                "invalid_bucket_count",
                family.name,
                "bucket counts must be non-negative integers",
                sample=sample.name,
              ),
            )
          }
        }
        if sample.name == "\{family.name}_sum" {
          if sample.value.is_nan() || sample.value < 0.0 {
            issues.push(
              ValidationIssue::error(
                "invalid_histogram_sum",
                family.name,
                "histogram sum must be non-NaN and non-negative",
                sample=sample.name,
              ),
            )
          }
        }
        if sample.name == "\{family.name}_count" {
          if !is_nonnegative_integer(sample.value) {
            issues.push(
              ValidationIssue::error(
                "invalid_histogram_count",
                family.name,
                "histogram count must be a non-negative integer",
                sample=sample.name,
              ),
            )
          }
        }
        if sample.name == "\{family.name}_gsum" {
          if sample.value.is_nan() {
            issues.push(
              ValidationIssue::error(
                "invalid_gaugehistogram_sum",
                family.name,
                "gauge-histogram sum must not be NaN",
                sample=sample.name,
              ),
            )
          }
        }
        if sample.name == "\{family.name}_gcount" {
          if !is_nonnegative_integer(sample.value) {
            issues.push(
              ValidationIssue::error(
                "invalid_gaugehistogram_count",
                family.name,
                "gauge-histogram count must be a non-negative integer",
                sample=sample.name,
              ),
            )
          }
        }
      }
      Summary =>
        if sample.name == "\{family.name}_sum" {
          if sample.value.is_nan() || sample.value < 0.0 {
            issues.push(
              ValidationIssue::error(
                "invalid_summary_sum",
                family.name,
                "summary sum must be non-NaN and non-negative",
                sample=sample.name,
              ),
            )
          }
        } else if sample.name == "\{family.name}_count" {
          if !is_nonnegative_integer(sample.value) {
            issues.push(
              ValidationIssue::error(
                "invalid_summary_count",
                family.name,
                "summary count must be a non-negative integer",
                sample=sample.name,
              ),
            )
          }
        } else if sample.name == family.name {
          match sample.label("quantile") {
            None =>
              issues.push(
                ValidationIssue::error(
                  "missing_quantile",
                  family.name,
                  "summary observations require a 'quantile' label",
                  sample=sample.name,
                ),
              )
            Some(quantile) =>
              match parse_number(quantile) {
                Ok(value) =>
                  if value.is_nan() || value < 0.0 || value > 1.0 {
                    issues.push(
                      ValidationIssue::error(
                        "invalid_quantile",
                        family.name,
                        "quantile must be between 0 and 1",
                        sample=sample.name,
                      ),
                    )
                  }
                Err(_) =>
                  issues.push(
                    ValidationIssue::error(
                      "invalid_quantile",
                      family.name,
                      "quantile '\{quantile}' is not numeric",
                      sample=sample.name,
                    ),
                  )
              }
          }
          if sample.value < 0.0 {
            issues.push(
              ValidationIssue::error(
                "negative_quantile_value",
                family.name,
                "summary quantile values must not be negative",
                sample=sample.name,
              ),
            )
          }
        }
      Info =>
        if sample.name == "\{family.name}_info" && sample.value != 1.0 {
          issues.push(
            ValidationIssue::error(
              "invalid_info_value",
              family.name,
              "info samples must have value 1",
              sample=sample.name,
            ),
          )
        }
      StateSet => {
        if !labels_have_name(sample.labels, family.name) {
          issues.push(
            ValidationIssue::error(
              "missing_state_label",
              family.name,
              "stateset samples require a label named after the metric family",
              sample=sample.name,
            ),
          )
        }
        if sample.value != 0.0 && sample.value != 1.0 {
          issues.push(
            ValidationIssue::error(
              "invalid_state_value",
              family.name,
              "stateset sample values must be 0 or 1",
              sample=sample.name,
            ),
          )
        }
      }
      Unknown | Gauge => ()
    }
  }
  match family.metric_type {
    Counter =>
      if !family.samples.is_empty() && !has_counter_total {
        issues.push(
          ValidationIssue::error(
            "missing_counter_total",
            family.name,
            "counter family has no _total sample",
          ),
        )
      }
    Unknown | Gauge | Histogram | GaugeHistogram | Summary | Info | StateSet =>
      ()
  }
  match family.metric_type {
    Histogram | GaugeHistogram => {
      validate_histogram_components(family, issues)
      validate_histogram_order(family, issues)
    }
    Summary => {
      validate_summary_order(family, issues)
      validate_summary_points(family, issues)
    }
    Counter => validate_counter_points(family, issues)
    Unknown | Gauge | Info | StateSet => ()
  }
  validate_created_values(family, issues)
  validate_metric_sequence(family, issues)
}

///|
fn validate_family(
  family : MetricFamily,
  issues : Array[ValidationIssue],
) -> Unit {
  if !is_valid_metric_name(family.name) {
    issues.push(
      ValidationIssue::error(
        "invalid_family_name",
        family.name,
        "family name is not a valid metric identifier",
      ),
    )
  }
  if family.name.has_prefix("_") {
    issues.push(
      ValidationIssue::error(
        "reserved_family_name",
        family.name,
        "metric family names beginning with '_' are reserved",
      ),
    )
  }
  if family.help is None || family.help == Some("") {
    issues.push(
      ValidationIssue::warning(
        "missing_help",
        family.name,
        "metric family has no HELP directive",
      ),
    )
  }
  if family.metric_type == Unknown {
    issues.push(
      ValidationIssue::warning(
        "unknown_type",
        family.name,
        "metric family has no concrete TYPE",
      ),
    )
  }
  match family.unit {
    Some(unit) =>
      if !unit.is_empty() {
        if !is_valid_unit(unit) {
          issues.push(
            ValidationIssue::error(
              "invalid_unit",
              family.name,
              "unit '\{unit}' is not a valid unit identifier",
            ),
          )
        } else if !family.name.has_suffix("_\{unit}") {
          issues.push(
            ValidationIssue::error(
              "unit_suffix",
              family.name,
              "family name must end with '_\{unit}'",
            ),
          )
        }
        match family.metric_type {
          Info | StateSet =>
            issues.push(
              ValidationIssue::error(
                "unit_for_unitless_type",
                family.name,
                "info and stateset families must not declare a unit",
              ),
            )
          _ => ()
        }
      }
    None => ()
  }
  for index in 0.. validate_exemplar(family, sample, exemplar, issues)
      None => ()
    }
    match sample.timestamp {
      Some(timestamp) =>
        if !is_finite_number(timestamp) {
          issues.push(
            ValidationIssue::error(
              "invalid_timestamp",
              family.name,
              "sample timestamp must be a finite Unix-second value",
              sample=sample.name,
            ),
          )
        }
      None => ()
    }
    let mut saw_same_series = false
    let mut saw_untimestamped_point = false
    let mut latest_timestamp : Double? = None
    for previous in 0.. saw_untimestamped_point = true
          Some(previous_time) =>
            match latest_timestamp {
              None => latest_timestamp = Some(previous_time)
              Some(latest) =>
                if previous_time > latest {
                  latest_timestamp = Some(previous_time)
                }
            }
        }
      }
    }
    if saw_same_series {
      match sample.timestamp {
        None =>
          issues.push(
            ValidationIssue::error(
              "duplicate_series",
              family.name,
              "sample repeats an existing name and label set without distinct timestamps",
              sample=sample.name,
            ),
          )
        Some(current_time) =>
          if saw_untimestamped_point {
            issues.push(
              ValidationIssue::error(
                "duplicate_series",
                family.name,
                "sample repeats an untimestamped name and label set",
                sample=sample.name,
              ),
            )
          } else {
            match latest_timestamp {
              Some(latest) =>
                if current_time <= latest {
                  issues.push(
                    ValidationIssue::error(
                      "nonmonotonic_timestamps",
                      family.name,
                      "repeated metric points require increasing timestamps",
                      sample=sample.name,
                    ),
                  )
                }
              None => ()
            }
          }
      }
    }
  }
  validate_family_shape(family, issues)
}

///|
fn family_wire_names(family : MetricFamily) -> Array[String] {
  match family.metric_type {
    Counter => ["\{family.name}_total", "\{family.name}_created"]
    Histogram =>
      [
        "\{family.name}_bucket",
        "\{family.name}_sum",
        "\{family.name}_count",
        "\{family.name}_created",
      ]
    GaugeHistogram =>
      ["\{family.name}_bucket", "\{family.name}_gsum", "\{family.name}_gcount"]
    Summary =>
      [
        family.name,
        "\{family.name}_sum",
        "\{family.name}_count",
        "\{family.name}_created",
      ]
    Info => ["\{family.name}_info"]
    Unknown | Gauge | StateSet => [family.name]
  }
}

///|
fn families_have_wire_collision(
  left : MetricFamily,
  right : MetricFamily,
) -> Bool {
  for left_name in family_wire_names(left) {
    for right_name in family_wire_names(right) {
      if left_name == right_name {
        return true
      }
    }
  }
  false
}

///|
/// Validate document-level and metric-family semantics.
pub fn validate(document : Document) -> ValidationReport {
  let issues : Array[ValidationIssue] = []
  if !document.has_eof {
    issues.push(
      ValidationIssue::error(
        "missing_eof", "", "OpenMetrics 1.0 documents must end with # EOF",
      ),
    )
  }
  for index in 0.. error_count += 1
      Warning => warning_count += 1
    }
  }
  { issues, error_count, warning_count }
}

///|
/// Return true when semantic validation finds no errors.
pub fn is_valid(document : Document) -> Bool {
  validate(document).is_valid()
}