#![expect(
clippy::disallowed_types,
reason = "canonical JSON / Simplified Formats operate on the wire value by definition \
(ITS-JSON; ITS-REST Simplified Formats) — serde_json::Value IS the subject matter \
(#1694)"
)]
use std::collections::BTreeSet;
use openehr_base::v1_3::base_types::definitions::definitions_impl::LOCAL_TERMINOLOGY_ID;
use serde_json::Value;
use super::{ValidationKind, Validator};
use crate::flat::webtemplate::model::{
WebTemplateCodedValue, WebTemplateInput, WebTemplateInputType, WebTemplateNode,
WebTemplateRange,
};
pub(super) fn check_inputs(v: &mut Validator, instance: &Value, wt: &WebTemplateNode) {
let rm = wt.rm_type.split('<').next().unwrap_or(&wt.rm_type);
match rm {
"DV_CODED_TEXT" => check_coded_text(v, instance, wt),
"DV_STATE" => {
if let Some(value) = instance.get("value") {
check_coded_text(v, value, wt);
}
}
"CODE_PHRASE" => check_code_phrase(v, instance, wt),
"DV_ORDINAL" | "DV_SCALE" => check_ordinal(v, instance, wt),
"DV_QUANTITY" => check_quantity(v, instance, wt),
"DV_COUNT" => check_count(v, instance, wt),
"DV_PROPORTION" => check_proportion(v, instance, wt),
"DV_TEXT" | "DV_URI" | "DV_EHR_URI" => check_text(v, instance, wt),
"DV_BOOLEAN" => check_boolean(v, instance, wt),
"DV_IDENTIFIER" => check_identifier(v, instance, wt),
"DV_PARSABLE" => check_parsable(v, instance, wt),
"DV_DATE" | "DV_TIME" | "DV_DATE_TIME" => check_temporal(v, instance, wt),
"DV_DURATION" => check_duration(v, instance, wt),
_ => {}
}
check_numeric_lists(v, instance, wt);
check_numeric_ranges(v, instance, wt);
check_code_lists(v, instance, wt);
}
fn check_numeric_ranges(v: &mut Validator, instance: &Value, wt: &WebTemplateNode) {
let attrs: BTreeSet<&str> = wt.numeric_ranges.iter().map(|(a, _)| a.as_str()).collect();
for attr in attrs {
let Some(value) = instance.get(attr).and_then(as_f64) else {
continue;
};
let mut ranges = wt.numeric_ranges.iter().filter(|(a, _)| a == attr);
if !ranges.any(|(_, range)| in_range(value, range)) {
v.push(
&wt.aql_path,
format!("{attr} {value} is outside the constrained range"),
ValidationKind::RangeError,
);
}
}
}
fn check_numeric_lists(v: &mut Validator, instance: &Value, wt: &WebTemplateNode) {
for (attr, list) in &wt.numeric_lists {
let Some(value) = instance.get(attr).and_then(as_f64) else {
continue;
};
if !list.iter().any(|allowed| (allowed - value).abs() < 1e-9) {
v.push(
&wt.aql_path,
format!("{attr} {value} is not in the constrained value list"),
ValidationKind::CodedValue,
);
}
}
}
fn check_code_lists(v: &mut Validator, instance: &Value, wt: &WebTemplateNode) {
for cl in &wt.code_lists {
let Some(cp) = instance.get(&cl.attr) else {
continue;
};
let code = cp.get("code_string").and_then(Value::as_str);
let term = cp
.get("terminology_id")
.and_then(|t| t.get("value"))
.and_then(Value::as_str);
let Some(code) = code else { continue };
if !terminology_matches(cl.terminology.as_deref(), term) {
continue;
}
if !cl.codes.iter().any(|c| c == code) {
v.push(
&wt.aql_path,
format!(
"{} code '{code}' is not in the constrained code list",
cl.attr
),
ValidationKind::CodedValue,
);
}
}
}
pub(super) fn terminology_matches(constraint: Option<&str>, instance: Option<&str>) -> bool {
match constraint {
None | Some(LOCAL_TERMINOLOGY_ID | "") => {
matches!(instance, None | Some(LOCAL_TERMINOLOGY_ID | ""))
}
Some(t) => instance == Some(t),
}
}
fn check_coded_text(v: &mut Validator, instance: &Value, wt: &WebTemplateNode) {
let Some(code_input) = input_with_suffix(wt, "code") else {
return;
};
let code = instance
.get("defining_code")
.and_then(|c| c.get("code_string"))
.and_then(Value::as_str);
let terminology = instance
.get("defining_code")
.and_then(|c| c.get("terminology_id"))
.and_then(|t| t.get("value"))
.and_then(Value::as_str);
check_code_membership(v, wt, code_input, code, terminology);
}
fn check_code_phrase(v: &mut Validator, instance: &Value, wt: &WebTemplateNode) {
let Some(code_input) = input_with_suffix(wt, "code").or_else(|| wt.inputs.first()) else {
return;
};
let code = instance.get("code_string").and_then(Value::as_str);
let terminology = instance
.get("terminology_id")
.and_then(|t| t.get("value"))
.and_then(Value::as_str);
check_code_membership(v, wt, code_input, code, terminology);
}
fn check_ordinal(v: &mut Validator, instance: &Value, wt: &WebTemplateNode) {
let Some(input) = wt.inputs.first() else {
return;
};
if input.list.is_empty() || input.list_open == Some(true) {
return;
}
let Some(code) = checkable_symbol_code(instance, input) else {
return;
};
let same_symbol: Vec<&WebTemplateCodedValue> =
input.list.iter().filter(|cv| cv.value == code).collect();
if same_symbol.is_empty() {
v.push(
&wt.aql_path,
format!("ordinal symbol '{code}' is not in the constrained value set"),
ValidationKind::CodedValue,
);
return;
}
if same_symbol
.iter()
.all(|cv| cv.scale.is_none() && cv.ordinal.is_none())
{
return;
}
let is_scale = wt.rm_type.starts_with("DV_SCALE");
let Some(inst_value) = instance.get("value") else {
return; };
let pair_ok = same_symbol
.iter()
.any(|cv| paired_value_matches(cv, is_scale, inst_value));
if !pair_ok {
v.push(
&wt.aql_path,
format!(
"ordinal (symbol '{code}', value {inst_value}) does not match a \
constrained (symbol, value) pair"
),
ValidationKind::CodedValue,
);
}
}
fn checkable_symbol_code<'a>(instance: &'a Value, input: &WebTemplateInput) -> Option<&'a str> {
let defining_code = instance.get("symbol").and_then(|s| s.get("defining_code"));
let terminology = defining_code
.and_then(|c| c.get("terminology_id"))
.and_then(|t| t.get("value"))
.and_then(Value::as_str);
if !terminology_matches(input.terminology.as_deref(), terminology) {
return None;
}
defining_code
.and_then(|c| c.get("code_string"))
.and_then(Value::as_str)
}
fn paired_value_matches(entry: &WebTemplateCodedValue, is_scale: bool, inst_value: &Value) -> bool {
if is_scale {
return matches!(
(entry.scale, inst_value.as_f64()),
(Some(s), Some(iv)) if (s - iv).abs() < 1e-9
);
}
matches!(
(entry.ordinal, inst_value.as_i64()),
(Some(o), Some(iv)) if i64::from(o) == iv
)
}
fn check_code_membership(
v: &mut Validator,
wt: &WebTemplateNode,
input: &WebTemplateInput,
code: Option<&str>,
terminology: Option<&str>,
) {
if input.list.is_empty() || input.list_open == Some(true) {
return;
}
let Some(code) = code else { return };
if terminology_matches(input.terminology.as_deref(), terminology) {
if !input.list.iter().any(|cv| cv.value == code) {
v.push(
&wt.aql_path,
format!("coded value '{code}' is not in the constrained value set"),
ValidationKind::CodedValue,
);
}
return;
}
if wt.coded_terminology_local {
v.push(
&wt.aql_path,
format!(
"coded value '{code}' (terminology '{}') is not in the constrained local value set",
terminology.unwrap_or("")
),
ValidationKind::CodedValue,
);
}
}
fn check_quantity(v: &mut Validator, instance: &Value, wt: &WebTemplateNode) {
let unit = instance.get("units").and_then(Value::as_str);
let unit_input = input_with_suffix(wt, "unit");
let has_unit_list =
unit_input.is_some_and(|ui| !ui.list.is_empty() && ui.list_open != Some(true));
if let (Some(ui), Some(u)) = (unit_input, unit)
&& has_unit_list
&& !ui.list.iter().any(|cv| cv.value == u)
{
v.push(
&wt.aql_path,
format!("unit '{u}' is not among the constrained units"),
ValidationKind::CodedValue,
);
}
if let (Some(property), Some(u)) = (&wt.quantity_property, unit)
&& !has_unit_list
{
let bundle = openehr_term::bundle::openehr();
let unit_matches =
|pu: &&openehr_term::bundle::Unit| pu.text == u || pu.ucum.as_deref() == Some(u);
let allowed = bundle.units_for_property(property);
let in_property = allowed.iter().any(unit_matches);
let known_elsewhere = bundle
.property_units()
.units
.iter()
.any(|pu| unit_matches(&pu));
if !allowed.is_empty() && !in_property && known_elsewhere {
v.push(
&wt.aql_path,
format!("unit '{u}' is not a valid unit for the constrained property '{property}'"),
ValidationKind::CodedValue,
);
}
}
if let Some(mag) = instance.get("magnitude").and_then(as_f64) {
let range = input_with_suffix(wt, "magnitude")
.and_then(|i| i.validation.as_ref())
.and_then(|val| val.range.as_ref())
.or_else(|| unit_scoped_range(unit_input, unit));
if let Some(range) = range
&& !in_range(mag, range)
{
v.push(
&wt.aql_path,
format!("magnitude {mag} is outside the constrained range"),
ValidationKind::RangeError,
);
}
}
if let Some(prec) = instance.get("precision").and_then(as_f64) {
let range = input_with_suffix(wt, "magnitude")
.and_then(|i| i.validation.as_ref())
.and_then(|val| val.precision.as_ref())
.or_else(|| unit_scoped_precision(unit_input, unit));
if let Some(range) = range
&& !in_range(prec, range)
{
v.push(
&wt.aql_path,
format!("precision {prec} is outside the constrained precision range"),
ValidationKind::RangeError,
);
}
}
}
fn unit_scoped_range<'a>(
unit_input: Option<&'a WebTemplateInput>,
unit: Option<&str>,
) -> Option<&'a WebTemplateRange> {
let (ui, u) = (unit_input?, unit?);
ui.list
.iter()
.find(|cv| cv.value == u)
.and_then(|cv| cv.validation.as_ref())
.and_then(|val| val.range.as_ref())
}
fn unit_scoped_precision<'a>(
unit_input: Option<&'a WebTemplateInput>,
unit: Option<&str>,
) -> Option<&'a WebTemplateRange> {
let (ui, u) = (unit_input?, unit?);
ui.list
.iter()
.find(|cv| cv.value == u)
.and_then(|cv| cv.validation.as_ref())
.and_then(|val| val.precision.as_ref())
}
fn check_count(v: &mut Validator, instance: &Value, wt: &WebTemplateNode) {
let Some(mag) = instance.get("magnitude").and_then(as_f64) else {
return;
};
if let Some(range) = wt
.inputs
.first()
.and_then(|i| i.validation.as_ref())
.and_then(|val| val.range.as_ref())
&& !in_range(mag, range)
{
v.push(
&wt.aql_path,
format!("count {mag} is outside the constrained range"),
ValidationKind::RangeError,
);
}
}
fn check_proportion(v: &mut Validator, instance: &Value, wt: &WebTemplateNode) {
if !wt.proportion_types.is_empty()
&& let Some(kind) = instance.get("type").and_then(Value::as_i64)
{
let name = usize::try_from(kind)
.ok()
.and_then(|k| crate::flat::webtemplate::PROPORTION_KINDS.get(k));
if name.is_none_or(|n| !wt.proportion_types.iter().any(|p| p == n)) {
v.push(
&wt.aql_path,
format!(
"proportion type {kind} is not among the permitted kinds [{}]",
wt.proportion_types.join(", ")
),
ValidationKind::CodedValue,
);
}
}
if let Some(prec) = instance.get("precision").and_then(as_f64)
&& let Some(range) = wt
.inputs
.iter()
.find_map(|i| i.validation.as_ref().and_then(|val| val.precision.as_ref()))
&& !in_range(prec, range)
{
v.push(
&wt.aql_path,
format!("precision {prec} is outside the constrained precision range"),
ValidationKind::RangeError,
);
}
for part in ["numerator", "denominator"] {
let Some(value) = instance.get(part).and_then(as_f64) else {
continue;
};
if let Some(range) = input_with_suffix(wt, part)
.and_then(|i| i.validation.as_ref())
.and_then(|val| val.range.as_ref())
&& !in_range(value, range)
{
v.push(
&wt.aql_path,
format!("{part} {value} is outside the constrained range"),
ValidationKind::RangeError,
);
}
}
}
fn check_text(v: &mut Validator, instance: &Value, wt: &WebTemplateNode) {
let Some(input) = wt.inputs.first() else {
return;
};
let Some(value) = instance.get("value").and_then(Value::as_str) else {
return;
};
check_string_constraints(v, wt, input, value);
}
fn check_boolean(v: &mut Validator, instance: &Value, wt: &WebTemplateNode) {
let Some(input) = wt.inputs.first() else {
return;
};
let Some(value) = instance.get("value").and_then(Value::as_bool) else {
return;
};
if !input.list.is_empty() && !input.list.iter().any(|cv| cv.value == value.to_string()) {
v.push(
&wt.aql_path,
format!("boolean value {value} is not permitted"),
ValidationKind::CodedValue,
);
}
}
fn check_identifier(v: &mut Validator, instance: &Value, wt: &WebTemplateNode) {
for part in ["id", "type", "issuer", "assigner"] {
let Some(input) = input_with_suffix(wt, part) else {
continue;
};
let Some(value) = instance.get(part).and_then(Value::as_str) else {
continue;
};
check_string_constraints(v, wt, input, value);
}
}
fn check_parsable(v: &mut Validator, instance: &Value, wt: &WebTemplateNode) {
for part in ["value", "formalism"] {
let Some(input) = input_with_suffix(wt, part) else {
continue;
};
let Some(value) = instance.get(part).and_then(Value::as_str) else {
continue;
};
check_string_constraints(v, wt, input, value);
}
}
fn check_temporal(v: &mut Validator, instance: &Value, wt: &WebTemplateNode) {
let Some(value) = instance.get("value").and_then(Value::as_str) else {
return;
};
if let Some(validation) = wt.inputs.first().and_then(|i| i.validation.as_ref()) {
if let Some(pattern) = &validation.pattern
&& let Some(breach) = temporal_pattern_breach(pattern, value)
{
v.push(
&wt.aql_path,
format!(
"temporal value '{value}' does not satisfy the pattern '{pattern}': {}. \
{PATTERN_KEY}",
breach.explain(pattern)
),
ValidationKind::PatternError,
);
}
if let Some(range) = &validation.range
&& !temporal_in_range(value, range)
{
v.push(
&wt.aql_path,
format!("temporal value '{value}' is outside the constrained range"),
ValidationKind::RangeError,
);
}
}
check_timezone_validity(v, wt, value);
}
fn check_timezone_validity(v: &mut Validator, wt: &WebTemplateNode, value: &str) {
let Some(tzv) = wt.tz_validity else { return };
let has_tz = has_timezone(value);
match tzv {
1001 if !has_tz => v.push(
&wt.aql_path,
format!("temporal value '{value}' is missing a mandatory timezone"),
ValidationKind::PatternError,
),
1003 if has_tz => v.push(
&wt.aql_path,
format!("temporal value '{value}' carries a timezone the constraint disallows"),
ValidationKind::PatternError,
),
_ => {}
}
}
fn has_timezone(value: &str) -> bool {
let time = match value.split_once('T') {
Some((_, t)) => t,
None if value.contains(':') => value, None => return false,
};
time.ends_with('Z') || time.ends_with('z') || time.rfind(['+', '-']).is_some()
}
fn check_duration(v: &mut Validator, instance: &Value, wt: &WebTemplateNode) {
let Some(value) = instance.get("value").and_then(Value::as_str) else {
return;
};
let Some(fields) = duration_fields(value) else {
return; };
check_duration_fields_allowed(v, value, &fields, wt);
check_duration_range(v, value, &fields, wt);
}
fn check_duration_fields_allowed(
v: &mut Validator,
value: &str,
fields: &[(&'static str, f64)],
wt: &WebTemplateNode,
) {
let allowed: Vec<&str> = wt
.inputs
.iter()
.filter_map(|i| i.suffix.as_deref())
.collect();
if allowed.is_empty() || allowed.len() >= 7 {
return;
}
for (name, _) in fields.iter().filter(|(_, n)| *n != 0.0) {
if !allowed.contains(name) {
v.push(
&wt.aql_path,
format!(
"duration '{value}' uses the '{name}' field the C_DURATION pattern \
does not allow"
),
ValidationKind::PatternError,
);
}
}
}
fn check_duration_range(
v: &mut Validator,
value: &str,
fields: &[(&'static str, f64)],
wt: &WebTemplateNode,
) {
let Some(range) = &wt.duration_range else {
return;
};
let secs = duration_seconds(fields);
let bound = |b: &Option<Value>| -> Option<f64> {
b.as_ref()
.and_then(Value::as_str)
.and_then(|s| duration_fields(s).map(|f| duration_seconds(&f)))
};
let min_ok = bound(&range.min).is_none_or(|b| {
if range.min_op.as_deref() == Some(">") {
secs > b
} else {
secs >= b
}
});
let max_ok = bound(&range.max).is_none_or(|b| {
if range.max_op.as_deref() == Some("<") {
secs < b
} else {
secs <= b
}
});
if !min_ok || !max_ok {
v.push(
&wt.aql_path,
format!("duration '{value}' is outside the constrained range"),
ValidationKind::RangeError,
);
}
}
fn duration_fields(value: &str) -> Option<Vec<(&'static str, f64)>> {
let rest = value.strip_prefix('-').unwrap_or(value);
let rest = rest.strip_prefix('P')?;
let (date_part, time_part) = match rest.split_once('T') {
Some((d, t)) => (d, t),
None => (rest, ""),
};
let mut out = Vec::new();
push_duration_part(date_part, false, &mut out)?;
push_duration_part(time_part, true, &mut out)?;
Some(out)
}
fn push_duration_part(part: &str, in_time: bool, out: &mut Vec<(&'static str, f64)>) -> Option<()> {
let mut num = String::new();
for ch in part.chars() {
if ch.is_ascii_digit() || ch == '.' || ch == ',' {
num.push(if ch == ',' { '.' } else { ch });
continue;
}
let n: f64 = num.parse().ok()?;
num.clear();
out.push((duration_field_name(ch, in_time)?, n));
}
if num.is_empty() { Some(()) } else { None }
}
fn duration_field_name(designator: char, in_time: bool) -> Option<&'static str> {
match (designator, in_time) {
('Y', false) => Some("year"),
('M', false) => Some("month"),
('W', false) => Some("week"),
('D', false) => Some("day"),
('H', true) => Some("hour"),
('M', true) => Some("minute"),
('S', true) => Some("second"),
_ => None,
}
}
fn duration_seconds(fields: &[(&'static str, f64)]) -> f64 {
fields
.iter()
.map(|(name, n)| {
n * match *name {
"year" => 31_557_600.0,
"month" => 2_629_800.0,
"week" => 604_800.0,
"day" => 86_400.0,
"hour" => 3_600.0,
"minute" => 60.0,
_ => 1.0,
}
})
.sum()
}
const PATTERN_KEY: &str = "Pattern key: a letter field (yyyy, mm, dd, HH, MM, SS) is required, \
?? is optional, XX is not allowed.";
const DATE_FIELDS: [&str; 3] = ["year", "month", "day"];
const TIME_FIELDS: [&str; 3] = ["hour", "minute", "second"];
#[derive(Debug, Default, PartialEq, Eq)]
struct PatternBreach {
not_allowed: Vec<&'static str>,
missing: Vec<&'static str>,
}
impl PatternBreach {
fn explain(&self, pattern: &str) -> String {
let mut parts = Vec::new();
let (pat_date, pat_time) = split_date_time(pattern);
let time_disallowed = !pat_time.is_empty() && pat_time.split(':').all(|seg| seg == "XX");
if time_disallowed && self.not_allowed.iter().any(|f| TIME_FIELDS.contains(f)) {
if pat_date.is_empty() {
parts.push("the template allows no time here".to_owned());
} else {
parts
.push("the template allows a date only; a time is not allowed here".to_owned());
}
let date_extra: Vec<&str> = self
.not_allowed
.iter()
.copied()
.filter(|f| DATE_FIELDS.contains(f))
.collect();
if !date_extra.is_empty() {
parts.push(format!("{} not allowed here", fields_phrase(&date_extra)));
}
} else if !self.not_allowed.is_empty() {
parts.push(format!(
"the value carries {}, which the template does not allow here",
fields_phrase(&self.not_allowed)
));
}
if !self.missing.is_empty() {
parts.push(format!(
"the template requires {}, which the value lacks",
fields_phrase(&self.missing)
));
}
parts.join("; ")
}
}
fn fields_phrase(fields: &[&str]) -> String {
match fields {
[] => String::new(),
[one] => format!("the {one}"),
[init @ .., last] => format!("the {} and {last}", init.join(", ")),
}
}
fn temporal_pattern_breach(pattern: &str, value: &str) -> Option<PatternBreach> {
let (pat_date, pat_time) = split_date_time(pattern);
let (val_date, val_time) = split_date_time(value);
let (val_date, val_time) = if pat_date.is_empty()
&& !pat_time.is_empty()
&& !val_date.is_empty()
&& val_time.is_empty()
{
("", val_date)
} else {
(val_date, val_time)
};
let mut breach = PatternBreach::default();
judge_segments(pat_date, val_date, '-', 1, &DATE_FIELDS, &mut breach);
judge_segments(pat_time, val_time, ':', 0, &TIME_FIELDS, &mut breach);
(breach != PatternBreach::default()).then_some(breach)
}
fn judge_segments(
pattern: &str,
value: &str,
sep: char,
skip: usize,
fields: &[&'static str; 3],
breach: &mut PatternBreach,
) {
if pattern.is_empty() {
return;
}
let val_count = if value.is_empty() {
0
} else {
value.splitn(3, sep).count()
};
for (i, (seg, field)) in pattern
.splitn(3, sep)
.zip(fields.iter().copied())
.enumerate()
.skip(skip)
{
let present = val_count > i;
match (seg, present) {
("XX", true) => breach.not_allowed.push(field),
("??" | "XX", _) | (_, true) => {}
(_, false) => breach.missing.push(field),
}
}
}
fn split_date_time(s: &str) -> (&str, &str) {
if let Some((d, t)) = s.split_once('T') {
(d, trim_timezone(t))
} else if s.contains(':') {
("", trim_timezone(s))
} else {
(s, "")
}
}
fn trim_timezone(t: &str) -> &str {
let t = t.strip_suffix('Z').unwrap_or(t);
match t.rfind(['+', '-']).and_then(|i| t.get(..i)) {
Some(core) => core,
None => t,
}
}
fn temporal_in_range(value: &str, range: &WebTemplateRange) -> bool {
let norm = |s: &str| -> String {
let (d, t) = split_date_time(s);
if t.is_empty() {
d.to_owned()
} else if d.is_empty() {
t.to_owned()
} else {
format!("{d}T{t}")
}
};
let val = norm(value);
let cmp = |bound: &str| -> std::cmp::Ordering {
let b = norm(bound);
let n = val.len().min(b.len());
val.get(..n).unwrap_or(&val).cmp(b.get(..n).unwrap_or(&b))
};
if let Some(min) = range.min.as_ref().and_then(Value::as_str) {
let ok = match range.min_op.as_deref() {
Some(">") => cmp(min) == std::cmp::Ordering::Greater,
_ => cmp(min) != std::cmp::Ordering::Less,
};
if !ok {
return false;
}
}
if let Some(max) = range.max.as_ref().and_then(Value::as_str) {
let ok = match range.max_op.as_deref() {
Some("<") => cmp(max) == std::cmp::Ordering::Less,
_ => cmp(max) != std::cmp::Ordering::Greater,
};
if !ok {
return false;
}
}
true
}
fn check_string_constraints(
v: &mut Validator,
wt: &WebTemplateNode,
input: &WebTemplateInput,
value: &str,
) {
if input.input_type == WebTemplateInputType::Text
&& !input.list.is_empty()
&& input.list_open != Some(true)
&& !input.list.iter().any(|cv| cv.value == value)
{
v.push(
&wt.aql_path,
format!("value '{value}' is not in the constrained value set"),
ValidationKind::CodedValue,
);
}
if let Some(pattern) = input
.validation
.as_ref()
.and_then(|val| val.pattern.as_ref())
&& !matches_pattern(pattern, value)
{
v.push(
&wt.aql_path,
format!("value '{value}' does not match pattern /{pattern}/"),
ValidationKind::PatternError,
);
}
}
pub(super) fn matches_pattern(pattern: &str, value: &str) -> bool {
let body = pattern
.strip_prefix('/')
.and_then(|p| p.strip_suffix('/'))
.unwrap_or(pattern);
let anchored = format!("^(?:{body})$");
if let Ok(re) = regex::Regex::new(&anchored) {
return re.is_match(value);
}
match fancy_regex::Regex::new(&anchored) {
Ok(re) => re.is_match(value).unwrap_or(true),
Err(_) => true,
}
}
fn input_with_suffix<'a>(wt: &'a WebTemplateNode, suffix: &str) -> Option<&'a WebTemplateInput> {
wt.inputs
.iter()
.find(|i| i.suffix.as_deref() == Some(suffix))
}
fn as_f64(v: &Value) -> Option<f64> {
v.as_f64()
}
fn in_range(value: f64, range: &WebTemplateRange) -> bool {
let min = range.min.as_ref().and_then(as_f64);
let max = range.max.as_ref().and_then(as_f64);
let above = min.is_none_or(|m| match range.min_op.as_deref() {
Some(">") => value > m,
_ => value >= m,
});
above
&& max.is_none_or(|m| match range.max_op.as_deref() {
Some("<") => value < m,
_ => value <= m,
})
}