use std::collections::BTreeMap;
use serde::Serialize;
use serde_json::Value as JsonValue;
use crate::expression::simple_rule_qualifier;
use crate::lint::PackageSemanticModel;
use crate::model::VariableResolutionSource;
use crate::sdk::{EvaluationContext, Package};
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SavedContextResolution {
pub name: String,
pub evaluation_context: String,
pub path: String,
pub ok: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<JsonValue>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<VariableResolutionSource>,
#[serde(skip_serializing_if = "Option::is_none")]
pub steps: Option<Vec<ResolutionStep>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub used_default: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ResolutionStep {
pub index: usize,
pub qualifier: String,
pub matched: bool,
pub evaluation: QualifierEvaluation,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct QualifierContextEvaluation {
pub name: String,
pub evaluation_context: String,
pub path: String,
pub evaluation: Option<QualifierEvaluation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct QualifierEvaluation {
pub id: String,
pub matched: Option<bool>,
pub when: Option<String>,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EditContextPreview {
pub name: String,
pub evaluation_context: String,
pub qualifier_truth: BTreeMap<String, bool>,
}
pub async fn evaluate_qualifier_with_context(
runtime: &Package,
model: &PackageSemanticModel,
qualifier_id: &str,
context: &JsonValue,
) -> QualifierEvaluation {
let matched = match EvaluationContext::from_json(context.clone()) {
Ok(evaluation_context) => runtime
.resolve_qualifier(qualifier_id, &evaluation_context)
.ok(),
Err(_) => None,
};
let when = model
.qualifiers
.iter()
.find(|candidate| candidate.id == qualifier_id)
.and_then(|qualifier| {
qualifier
.when
.as_ref()
.and_then(|field| field.value.clone())
});
QualifierEvaluation {
id: qualifier_id.to_owned(),
matched,
when,
}
}
#[derive(Clone)]
pub struct SavedContextInput {
pub name: String,
pub evaluation_context: String,
pub path: String,
pub text: String,
}
pub async fn resolve_saved_contexts(
runtime: &Package,
model: &PackageSemanticModel,
variable_id: &str,
contexts: &[SavedContextInput],
) -> Vec<SavedContextResolution> {
let rules = model
.variables
.iter()
.find(|variable| variable.id == variable_id)
.and_then(|variable| variable.resolve.as_ref())
.map(|resolve| resolve.rules.as_slice())
.unwrap_or_default();
let mut resolutions = Vec::new();
for context_input in contexts {
match resolve_one(runtime, model, variable_id, rules, context_input).await {
Ok(resolution) => resolutions.push(resolution),
Err(error) => resolutions.push(SavedContextResolution {
name: context_input.name.clone(),
evaluation_context: context_input.evaluation_context.clone(),
path: context_input.path.clone(),
ok: false,
value: None,
source: None,
steps: None,
used_default: None,
error: Some(error),
}),
}
}
resolutions
}
async fn resolve_one(
runtime: &Package,
model: &PackageSemanticModel,
variable_id: &str,
rules: &[crate::lint::RuleModel],
context_input: &SavedContextInput,
) -> std::result::Result<SavedContextResolution, String> {
let context: JsonValue =
serde_json::from_str(&context_input.text).map_err(|err| err.to_string())?;
let evaluation_context =
EvaluationContext::from_json(context.clone()).map_err(|err| err.to_string())?;
let resolution = runtime
.resolve_variable(variable_id, &evaluation_context)
.map_err(|err| err.to_string())?;
let mut steps = Vec::new();
let mut matched_rule = false;
for rule in rules {
let Some(qualifier) = rule
.when
.as_ref()
.and_then(|when| when.value.as_deref())
.and_then(simple_rule_qualifier)
else {
continue;
};
let evaluation =
evaluate_qualifier_with_context(runtime, model, &qualifier, &context).await;
let Some(matched) = evaluation.matched else {
return Err(format!("qualifier {qualifier} could not be evaluated"));
};
steps.push(ResolutionStep {
index: rule.index,
qualifier,
matched,
evaluation,
});
if matched {
matched_rule = true;
break;
}
}
Ok(SavedContextResolution {
name: context_input.name.clone(),
evaluation_context: context_input.evaluation_context.clone(),
path: context_input.path.clone(),
ok: true,
value: Some(resolution.value),
source: Some(resolution.source),
steps: Some(steps),
used_default: Some(!matched_rule),
error: None,
})
}
pub async fn edit_context_previews(
runtime: &Package,
qualifier_ids: &[String],
contexts: &[SavedContextInput],
) -> Vec<EditContextPreview> {
let mut previews = Vec::new();
for context_input in contexts {
let Ok(context) = serde_json::from_str::<JsonValue>(&context_input.text) else {
continue;
};
let Ok(evaluation_context) = EvaluationContext::from_json(context) else {
continue;
};
let mut qualifier_truth = BTreeMap::new();
for qualifier_id in qualifier_ids {
if let Ok(resolution) = runtime.resolve_qualifier(qualifier_id, &evaluation_context) {
qualifier_truth.insert(qualifier_id.clone(), resolution);
}
}
previews.push(EditContextPreview {
name: context_input.name.clone(),
evaluation_context: context_input.evaluation_context.clone(),
qualifier_truth,
});
}
previews
}
pub async fn qualifier_context_evaluations(
runtime: &Package,
model: &PackageSemanticModel,
qualifier_id: &str,
contexts: &[SavedContextInput],
) -> Vec<QualifierContextEvaluation> {
let mut evaluations = Vec::new();
for context_input in contexts {
match serde_json::from_str::<JsonValue>(&context_input.text) {
Ok(context) => {
let evaluation =
evaluate_qualifier_with_context(runtime, model, qualifier_id, &context).await;
evaluations.push(QualifierContextEvaluation {
name: context_input.name.clone(),
evaluation_context: context_input.evaluation_context.clone(),
path: context_input.path.clone(),
evaluation: Some(evaluation),
error: None,
});
}
Err(error) => evaluations.push(QualifierContextEvaluation {
name: context_input.name.clone(),
evaluation_context: context_input.evaluation_context.clone(),
path: context_input.path.clone(),
evaluation: None,
error: Some(error.to_string()),
}),
}
}
evaluations
}