///|
pub fn validate_document(
document : VcfDocument,
options~ : ValidationOptions,
) -> ValidationReport {
let issues : Array[Diagnostic] = []
validate_header(document.header, options~, issues)
for index, record in document.records {
validate_record(record, document.header, options~, issues, line=index + 1)
}
{ issues, }
}
///|
fn validate_header(
header : VcfHeader,
options~ : ValidationOptions,
issues : Array[Diagnostic],
) -> Unit {
if header.fileformat == "" {
append_issue(
issues,
options~,
code="MISSING_FILEFORMAT",
line=0,
column=1,
field="fileformat",
message="VCF header has no fileformat declaration",
)
}
for kind in ["INFO", "FORMAT", "FILTER", "CONTIG"] {
let seen : Array[String] = []
let names = definition_names(header, kind)
for name in names {
if has_name(seen, name) {
append_issue(
issues,
options~,
code="DUPLICATE_\{kind}",
line=0,
column=1,
field=name,
message="duplicate {kind} definition",
)
} else {
seen.push(name)
}
}
}
if header.columns.length() == 0 {
append_issue(
issues,
options~,
code="MISSING_COLUMNS",
line=0,
column=1,
field="#CHROM",
message="VCF header has no column definition",
)
}
}
///|
fn validate_record(
record : VcfRecord,
header : VcfHeader,
options~ : ValidationOptions,
issues : Array[Diagnostic],
line~ : Int,
) -> Unit {
if record.chrom == "" {
append_issue(
issues,
options~,
code="EMPTY_CHROM",
line~,
column=1,
field="CHROM",
message="CHROM must not be empty",
)
}
if record.pos <= 0 {
append_issue(
issues,
options~,
code="INVALID_POS",
line~,
column=2,
field="POS",
message="POS must be positive",
)
}
if record.ref_allele == "" || record.ref_allele == "." {
append_issue(
issues,
options~,
code="EMPTY_REF",
line~,
column=4,
field="REF",
message="REF must contain an allele",
)
}
if record.alt_alleles.length() == 0 {
append_issue(
issues,
options~,
code="EMPTY_ALT",
line~,
column=5,
field="ALT",
message="ALT must contain at least one allele",
)
}
for alt in record.alt_alleles {
if alt == "" || alt == "." {
append_issue(
issues,
options~,
code="INVALID_ALT",
line~,
column=5,
field="ALT",
message="ALT contains an empty allele",
)
}
}
for info_pair in record.info {
let (key, value) = info_pair
match find_info_definition(header, key) {
None =>
append_issue(
issues,
options~,
code="UNKNOWN_INFO",
line~,
column=8,
field=key,
message="INFO key is not declared in the header",
)
Some(def) =>
match expected_value_count(def.number, record.alt_alleles.length()) {
Some(expected) =>
if value_count(value) != expected {
append_issue(
issues,
options~,
code="INFO_CARDINALITY",
line~,
column=8,
field=key,
message="INFO value count does not match header cardinality",
)
}
None => ()
}
}
}
if record.samples.length() != header.samples.length() {
append_issue(
issues,
options~,
code="SAMPLE_COUNT_MISMATCH",
line~,
column=10,
field="samples",
message="sample count does not match header",
)
}
for sample_index, sample in record.samples {
if sample_index < header.samples.length() &&
sample.name != header.samples[sample_index] {
append_issue(
issues,
options~,
code="SAMPLE_NAME_MISMATCH",
line~,
column=10 + sample_index,
field=sample.name,
message="sample name does not match header",
)
}
if sample.values.length() != record.format_keys.length() {
append_issue(
issues,
options~,
code="FORMAT_VALUE_COUNT",
line~,
column=9,
field=sample.name,
message="sample FORMAT value count does not match FORMAT keys",
)
}
}
}
///|
fn find_info_definition(header : VcfHeader, key : String) -> InfoDef? {
for item in header.meta {
match item {
MetaLine::Info(def) if def.id == key => return Some(def)
_ => ()
}
}
None
}
///|
fn expected_value_count(number : Cardinality, alt_count : Int) -> Int? {
match number {
Cardinality::Fixed(n) => Some(n)
Cardinality::PerAlt => Some(alt_count)
Cardinality::PerAllele => Some(alt_count + 1)
_ => None
}
}
///|
fn value_count(value : VcfValue) -> Int {
match value {
VcfValue::List(values) => values.length()
Missing | Integer(_) | Float(_) | String(_) => 1
Flag => 0
}
}