///|
priv struct ProgramConfigFile {
schema_version : Int
assay_id : String
unit : String
precision : Int
levels : Array[ControlLevelConfigFile]
rules : Array[RulePolicyConfigFile]
} derive(FromJson)
///|
priv struct ControlLevelConfigFile {
id : String
mean : String
standard_deviation : String
required : Bool
} derive(FromJson)
///|
priv struct RulePolicyConfigFile {
rule : String
disposition : String
} derive(FromJson)
///|
pub(all) struct ProgramConfigResult {
program : @assay.AssayProgram?
issues : Array[String]
} derive(Debug)
///|
fn config_contains(values : Array[String], value : String) -> Bool {
for item in values {
if item == value {
return true
}
}
false
}
///|
fn config_unknown_fields(
value : Json,
allowed : Array[String],
path : String,
issues : Array[String],
) -> Unit {
match value {
Object(fields) =>
for key, _ in fields {
if !config_contains(allowed, key) {
issues.push("unsupported field " + path + "." + key)
}
}
_ => ()
}
}
///|
fn validate_config_fields(value : Json, issues : Array[String]) -> Unit {
match value {
Object(fields) => {
config_unknown_fields(
value,
["schema_version", "assay_id", "unit", "precision", "levels", "rules"],
"$",
issues,
)
match fields.get("levels") {
Some(Array(levels)) =>
for index, level in levels {
config_unknown_fields(
level,
["id", "mean", "standard_deviation", "required"],
"$.levels[" + index.to_string() + "]",
issues,
)
}
_ => ()
}
match fields.get("rules") {
Some(Array(rules)) =>
for index, rule in rules {
config_unknown_fields(
rule,
["rule", "disposition"],
"$.rules[" + index.to_string() + "]",
issues,
)
}
_ => ()
}
}
_ => ()
}
}
///|
fn config_rule(name : String) -> @assay.Rule? {
match name {
"1_2s" => Some(@assay.Rule12s)
"1_3s" => Some(@assay.Rule13s)
"2_2s" => Some(@assay.Rule22s)
"R_4s" => Some(@assay.RuleR4s)
"4_1s" => Some(@assay.Rule41s)
"10x" => Some(@assay.Rule10x)
_ => None
}
}
///|
fn config_disposition(name : String) -> @assay.RuleDisposition? {
match name {
"disabled" => Some(@assay.Disabled)
"warning" => Some(@assay.Warning)
"requires_review" => Some(@assay.RequiresReview)
_ => None
}
}
///|
/// Parse a versioned JSON assay configuration and validate the resulting program.
/// Decimal values are strings so conversion to the program's scaled Int64 values
/// never passes through floating-point arithmetic.
pub fn parse_assay_program_json(text : String) -> ProgramConfigResult {
let json = @json.parse(text) catch {
error =>
return { program: None, issues: ["invalid JSON: " + error.to_string()] }
}
let shape_issues : Array[String] = []
validate_config_fields(json, shape_issues)
if shape_issues.length() > 0 {
return { program: None, issues: shape_issues }
}
let config : ProgramConfigFile = @json.from_json(json) catch {
error =>
return {
program: None,
issues: ["invalid program configuration: " + error.to_string()],
}
}
if config.schema_version != 1 {
return {
program: None,
issues: [
"unsupported schema_version " +
config.schema_version.to_string() +
"; supported version is 1",
],
}
}
let issues : Array[String] = []
if config.precision < 0 || config.precision > 18 {
issues.push("precision must be between 0 and 18")
}
let levels : Array[@assay.ControlLevel] = []
for index, level in config.levels {
let mean = match parse_scaled(level.mean, config.precision) {
Parsed(value) => Some(value)
Missing => {
issues.push("levels[" + index.to_string() + "].mean must not be empty")
None
}
Invalid(message) => {
issues.push("levels[" + index.to_string() + "].mean: " + message)
None
}
}
let standard_deviation = match
parse_scaled(level.standard_deviation, config.precision) {
Parsed(value) => Some(value)
Missing => {
issues.push(
"levels[" +
index.to_string() +
"].standard_deviation must not be empty",
)
None
}
Invalid(message) => {
issues.push(
"levels[" + index.to_string() + "].standard_deviation: " + message,
)
None
}
}
if mean is Some(mean_value) && standard_deviation is Some(sd_value) {
levels.push({
id: level.id,
mean: mean_value,
standard_deviation: sd_value,
required: level.required,
})
}
}
let rule_policies : Array[@assay.RulePolicy] = []
for index, policy in config.rules {
let rule = config_rule(policy.rule)
let disposition = config_disposition(policy.disposition)
if rule is None {
issues.push(
"rules[" + index.to_string() + "].rule is unknown: " + policy.rule,
)
}
if disposition is None {
issues.push(
"rules[" +
index.to_string() +
"].disposition is unknown: " +
policy.disposition,
)
}
match (rule, disposition) {
(Some(rule_value), Some(disposition_value)) =>
rule_policies.push({ rule: rule_value, disposition: disposition_value })
_ => ()
}
}
if issues.length() > 0 {
return { program: None, issues }
}
let program : @assay.AssayProgram = {
assay_id: config.assay_id,
unit: config.unit,
precision: config.precision,
levels,
rule_policies,
}
let validation = @assay.validate_program(program)
if validation.issues.length() > 0 {
for issue in validation.issues {
issues.push(issue)
}
return { program: None, issues }
}
{ program: Some(program), issues }
}