///|
fn feature_line_number(target_index : Int) -> Int {
target_index + 1
}
///|
fn add_issue(
issues : Array[ValidationIssue],
severity : Severity,
code : String,
line : Int,
feature_type : String,
message : String,
) -> Unit {
issues.push({ severity, code, line, feature_type, message })
}
///|
fn has_feature_id(features : Array[Feature], id : String) -> Bool {
for feature in features {
if feature.attribute("ID") == Some(id) {
return true
}
}
false
}
///|
fn has_duplicate_id_before(
features : Array[Feature],
index : Int,
id : String,
) -> Bool {
for i in 0.. Unit {
let line = feature_line_number(index)
match feature.attribute("ID") {
Some(id) =>
if has_duplicate_id_before(annotation.features, index, id) {
add_issue(
issues,
Error,
"duplicate_id",
line,
feature.feature_type,
"ID \{id} appears more than once",
)
}
None =>
if feature.feature_type == "gene" ||
feature.feature_type == "mRNA" ||
feature.feature_type == "transcript" {
add_issue(
issues,
Warning,
"missing_id",
line,
feature.feature_type,
"\{feature.feature_type} has no ID attribute",
)
}
}
match feature.attribute("Parent") {
Some(parent) =>
if !has_feature_id(annotation.features, parent) {
add_issue(
issues,
Error,
"orphan_parent",
line,
feature.feature_type,
"parent \{parent} is not present",
)
}
None => ()
}
match feature.phase {
Some(phase) =>
if feature.feature_type == "CDS" &&
!(phase == "0" || phase == "1" || phase == "2") {
add_issue(
issues,
Warning,
"invalid_phase",
line,
feature.feature_type,
"CDS phase must be 0, 1, or 2",
)
}
None => ()
}
}
///|
fn validate_gtf_feature(
issues : Array[ValidationIssue],
feature : Feature,
index : Int,
) -> Unit {
let line = index + 1
if feature.attribute("gene_id") is None {
add_issue(
issues,
Error,
"missing_gene_id",
line,
feature.feature_type,
"GTF feature has no gene_id",
)
}
if feature.feature_type != "gene" &&
feature.attribute("transcript_id") is None {
add_issue(
issues,
Error,
"missing_transcript_id",
line,
feature.feature_type,
"GTF non-gene feature has no transcript_id",
)
}
if feature.feature_type == "CDS" {
match feature.phase {
Some(phase) =>
if !(phase == "0" || phase == "1" || phase == "2") {
add_issue(
issues,
Warning,
"invalid_phase",
line,
feature.feature_type,
"CDS phase must be 0, 1, or 2",
)
}
None => ()
}
}
}
///|
pub fn Annotation::validate(self : Annotation) -> ValidationReport {
let issues : Array[ValidationIssue] = []
for index, feature in self.features {
match self.format {
GFF3 => validate_gff3_feature(self, issues, feature, index)
GTF => validate_gtf_feature(issues, feature, index)
}
}
{ issues, }
}
///|
pub fn ValidationReport::issue_count(self : ValidationReport) -> Int {
self.issues.length()
}
///|
pub fn ValidationReport::count_by_severity(
self : ValidationReport,
severity : Severity,
) -> Int {
let mut count = 0
for issue in self.issues {
if issue.severity == severity {
count += 1
}
}
count
}
///|
pub fn ValidationReport::error_count(self : ValidationReport) -> Int {
self.count_by_severity(Error)
}
///|
pub fn ValidationReport::warning_count(self : ValidationReport) -> Int {
self.count_by_severity(Warning)
}
///|
pub fn ValidationReport::info_count(self : ValidationReport) -> Int {
self.count_by_severity(Info)
}
///|
pub fn ValidationReport::has_errors(self : ValidationReport) -> Bool {
self.error_count() > 0
}
///|
pub fn ValidationReport::to_text(self : ValidationReport) -> String {
let lines : Array[String] = []
for issue in self.issues {
lines.push(
"\{issue.severity.to_string()}\t\{issue.code}\tline \{issue.line}\t\{issue.message}",
)
}
if lines.length() == 0 {
""
} else {
lines.join("\n") + "\n"
}
}
///|
pub fn ValidationIssue::is_error(self : ValidationIssue) -> Bool {
self.severity == Error
}
///|
pub fn ValidationIssue::is_warning(self : ValidationIssue) -> Bool {
self.severity == Warning
}
///|
pub fn ValidationIssue::is_info(self : ValidationIssue) -> Bool {
self.severity == Info
}
///|
pub fn ValidationIssue::summary(self : ValidationIssue) -> String {
"\{self.severity.to_string()} \{self.code} at line \{self.line}"
}
///|
pub fn ValidationReport::filter_by_code(
self : ValidationReport,
code : String,
) -> Array[ValidationIssue] {
let result : Array[ValidationIssue] = []
for issue in self.issues {
if issue.code == code {
result.push(issue)
}
}
result
}
///|
pub fn ValidationReport::filter_by_feature_type(
self : ValidationReport,
feature_type : String,
) -> Array[ValidationIssue] {
let result : Array[ValidationIssue] = []
for issue in self.issues {
if issue.feature_type == feature_type {
result.push(issue)
}
}
result
}
///|
pub fn ValidationReport::codes(self : ValidationReport) -> Array[String] {
let codes : Array[String] = []
for issue in self.issues {
if !codes.contains(issue.code) {
codes.push(issue.code)
}
}
codes
}