use crate::models::template::ParameterSpec;
use serde_json::Value;
#[inline]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn validate_params(
specs: &[ParameterSpec],
provided: &serde_json::Map<String, Value>,
) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
for spec in specs {
if spec.required && !provided.contains_key(&spec.name) {
errors.push(format!("Missing required parameter: {}", spec.name));
}
}
for (key, value) in provided {
if let Some(spec) = specs.iter().find(|s| s.name == *key) {
if !validate_type(&spec.param_type, value) {
errors.push(format!(
"Invalid type for '{}': expected {:?}, got {}",
key,
spec.param_type,
value_type_name(value)
));
}
} else {
errors.push(format!("Unknown parameter: {key}"));
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
fn validate_type(expected: &crate::models::template::ParameterType, value: &Value) -> bool {
use crate::models::template::ParameterType;
match (expected, value) {
(_, Value::String(_)) => true,
(ParameterType::Boolean, Value::Bool(_)) => true,
_ => false,
}
}
fn value_type_name(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
#[must_use]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn expand_env_vars(template: &str) -> String {
let re = regex::Regex::new(r"\$\{([^}]+)\}").expect("internal error");
re.replace_all(template, |caps: ®ex::Captures| {
std::env::var(&caps[1]).unwrap_or_else(|_| caps[0].to_string())
})
.to_string()
}
#[inline]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn parse_key_val(s: &str) -> Result<(String, Value), String> {
let pos = s
.find('=')
.ok_or_else(|| format!("invalid KEY=value: no `=` found in `{s}`"))?;
let key = s.get(..pos).unwrap_or_default();
let val = s.get(pos + 1..).unwrap_or_default();
let value = if val.is_empty() {
Value::Bool(true) } else if val == "true" || val == "false" {
Value::Bool(val.parse().expect("internal error"))
} else if let Ok(n) = val.parse::<i64>() {
Value::Number(n.into())
} else if let Ok(f) = val.parse::<f64>() {
Value::Number(serde_json::Number::from_f64(f).expect("internal error"))
} else {
Value::String(val.to_string())
};
Ok((key.to_string(), value))
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
use crate::models::template::{ParameterSpec, ParameterType};
use serde_json::{Map, Value};
#[test]
fn test_validate_params_required_missing() {
let specs = vec![ParameterSpec {
name: "required_param".to_string(),
param_type: ParameterType::String,
required: true,
default_value: None,
validation_pattern: None,
description: "A required parameter".to_string(),
}];
let provided = Map::new();
let result = validate_params(&specs, &provided);
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors.len(), 1);
assert!(errors[0].contains("Missing required parameter: required_param"));
}
#[test]
fn test_validate_params_optional_missing() {
let specs = vec![ParameterSpec {
name: "optional_param".to_string(),
param_type: ParameterType::String,
required: false,
default_value: Some("default".to_string()),
validation_pattern: None,
description: "An optional parameter".to_string(),
}];
let provided = Map::new();
let result = validate_params(&specs, &provided);
assert!(result.is_ok());
}
#[test]
fn test_validate_params_unknown_parameter() {
let specs = vec![];
let mut provided = Map::new();
provided.insert("unknown".to_string(), Value::String("value".to_string()));
let result = validate_params(&specs, &provided);
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors.len(), 1);
assert!(errors[0].contains("Unknown parameter: unknown"));
}
#[test]
fn test_validate_params_type_validation() {
let specs = vec![ParameterSpec {
name: "bool_param".to_string(),
param_type: ParameterType::Boolean,
required: true,
default_value: None,
validation_pattern: None,
description: "A boolean parameter".to_string(),
}];
let mut provided = Map::new();
provided.insert("bool_param".to_string(), Value::Number(123.into()));
let result = validate_params(&specs, &provided);
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors.len(), 1);
assert!(errors[0].contains("Invalid type for 'bool_param'"));
}
#[test]
fn test_validate_params_success() {
let specs = vec![
ParameterSpec {
name: "string_param".to_string(),
param_type: ParameterType::String,
required: true,
default_value: None,
validation_pattern: None,
description: "A string parameter".to_string(),
},
ParameterSpec {
name: "bool_param".to_string(),
param_type: ParameterType::Boolean,
required: true,
default_value: None,
validation_pattern: None,
description: "A boolean parameter".to_string(),
},
];
let mut provided = Map::new();
provided.insert(
"string_param".to_string(),
Value::String("test".to_string()),
);
provided.insert("bool_param".to_string(), Value::Bool(true));
let result = validate_params(&specs, &provided);
assert!(result.is_ok());
}
#[test]
fn test_validate_type_string_accepts_all() {
assert!(validate_type(
&ParameterType::String,
&Value::String("test".to_string())
));
assert!(validate_type(
&ParameterType::Boolean,
&Value::String("true".to_string())
));
}
#[test]
fn test_validate_type_boolean() {
assert!(validate_type(&ParameterType::Boolean, &Value::Bool(true)));
assert!(validate_type(&ParameterType::Boolean, &Value::Bool(false)));
assert!(!validate_type(
&ParameterType::Boolean,
&Value::Number(123.into())
));
}
#[test]
fn test_value_type_name() {
assert_eq!(value_type_name(&Value::Null), "null");
assert_eq!(value_type_name(&Value::Bool(true)), "boolean");
assert_eq!(value_type_name(&Value::Number(123.into())), "number");
assert_eq!(
value_type_name(&Value::String("test".to_string())),
"string"
);
assert_eq!(value_type_name(&Value::Array(vec![])), "array");
assert_eq!(value_type_name(&Value::Object(Map::new())), "object");
}
#[test]
fn test_expand_env_vars_no_vars() {
let result = expand_env_vars("no variables here");
assert_eq!(result, "no variables here");
}
#[test]
fn test_expand_env_vars_with_existing_var() {
std::env::set_var("TEST_VAR", "expanded_value");
let result = expand_env_vars("prefix ${TEST_VAR} suffix");
assert_eq!(result, "prefix expanded_value suffix");
std::env::remove_var("TEST_VAR");
}
#[test]
fn test_expand_env_vars_with_missing_var() {
let result = expand_env_vars("prefix ${NONEXISTENT_VAR} suffix");
assert_eq!(result, "prefix ${NONEXISTENT_VAR} suffix");
}
#[test]
fn test_parse_key_val_string() {
let result = parse_key_val("key=value").expect("internal error");
assert_eq!(result.0, "key");
assert_eq!(result.1, Value::String("value".to_string()));
}
#[test]
fn test_parse_key_val_boolean_true() {
let result = parse_key_val("flag=true").expect("internal error");
assert_eq!(result.0, "flag");
assert_eq!(result.1, Value::Bool(true));
}
#[test]
fn test_parse_key_val_boolean_false() {
let result = parse_key_val("flag=false").expect("internal error");
assert_eq!(result.0, "flag");
assert_eq!(result.1, Value::Bool(false));
}
#[test]
fn test_parse_key_val_integer() {
let result = parse_key_val("count=42").expect("internal error");
assert_eq!(result.0, "count");
assert_eq!(result.1, Value::Number(42.into()));
}
#[test]
fn test_parse_key_val_float() {
let result = parse_key_val("ratio=1.234").expect("internal error");
assert_eq!(result.0, "ratio");
if let Value::Number(n) = result.1 {
assert_eq!(n.as_f64().expect("internal error"), 1.234);
} else {
panic!("Expected number");
}
}
#[test]
fn test_parse_key_val_empty_value() {
let result = parse_key_val("flag=").expect("internal error");
assert_eq!(result.0, "flag");
assert_eq!(result.1, Value::Bool(true));
}
#[test]
fn test_parse_key_val_no_equals() {
let result = parse_key_val("invalid");
assert!(result.is_err());
assert!(result.unwrap_err().contains("no `=` found"));
}
#[test]
fn test_parse_key_val_complex_string() {
let result = parse_key_val("path=/some/complex/path").expect("internal error");
assert_eq!(result.0, "path");
assert_eq!(result.1, Value::String("/some/complex/path".to_string()));
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}