working on sensors and rules
This commit is contained in:
@@ -14,9 +14,11 @@ pub fn create_cors_layer(allowed_origins: Vec<String>) -> CorsLayer {
|
||||
// Default development origins
|
||||
vec![
|
||||
"http://localhost:3000".to_string(),
|
||||
"http://localhost:3001".to_string(),
|
||||
"http://localhost:5173".to_string(),
|
||||
"http://localhost:8080".to_string(),
|
||||
"http://127.0.0.1:3000".to_string(),
|
||||
"http://127.0.0.1:3001".to_string(),
|
||||
"http://127.0.0.1:5173".to_string(),
|
||||
"http://127.0.0.1:8080".to_string(),
|
||||
]
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
//! Parameter validation module
|
||||
//!
|
||||
//! Validates trigger and action parameters against their declared JSON schemas.
|
||||
//! Template-aware: values containing `{{ }}` template expressions are replaced
|
||||
//! with schema-appropriate placeholders before validation, so template expressions
|
||||
//! pass type checks while literal values are still validated normally.
|
||||
|
||||
use attune_common::models::{action::Action, trigger::Trigger};
|
||||
use jsonschema::Validator;
|
||||
@@ -8,15 +11,167 @@ use serde_json::Value;
|
||||
|
||||
use crate::middleware::ApiError;
|
||||
|
||||
/// Validate trigger parameters against the trigger's parameter schema
|
||||
/// Check if a JSON value is (or contains) a template expression.
|
||||
fn is_template_expression(value: &Value) -> bool {
|
||||
match value {
|
||||
Value::String(s) => s.contains("{{") && s.contains("}}"),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a JSON Schema property definition, produce a placeholder value that
|
||||
/// satisfies the schema's type constraint. This is used to replace template
|
||||
/// expressions so that JSON Schema validation passes for the remaining
|
||||
/// (non-template) parts of the parameters.
|
||||
fn placeholder_for_schema(property_schema: &Value) -> Value {
|
||||
// Handle anyOf / oneOf by picking the first variant
|
||||
if let Some(any_of) = property_schema.get("anyOf").and_then(|v| v.as_array()) {
|
||||
if let Some(first) = any_of.first() {
|
||||
return placeholder_for_schema(first);
|
||||
}
|
||||
}
|
||||
if let Some(one_of) = property_schema.get("oneOf").and_then(|v| v.as_array()) {
|
||||
if let Some(first) = one_of.first() {
|
||||
return placeholder_for_schema(first);
|
||||
}
|
||||
}
|
||||
|
||||
let type_value = property_schema.get("type").and_then(|t| t.as_str());
|
||||
|
||||
match type_value {
|
||||
Some("integer") => {
|
||||
// Use minimum if set, else default if set, else 0
|
||||
if let Some(default) = property_schema.get("default") {
|
||||
return default.clone();
|
||||
}
|
||||
if let Some(min) = property_schema.get("minimum").and_then(|v| v.as_i64()) {
|
||||
return Value::Number(min.into());
|
||||
}
|
||||
Value::Number(0.into())
|
||||
}
|
||||
Some("number") => {
|
||||
if let Some(default) = property_schema.get("default") {
|
||||
return default.clone();
|
||||
}
|
||||
if let Some(min) = property_schema.get("minimum").and_then(|v| v.as_f64()) {
|
||||
return serde_json::Number::from_f64(min)
|
||||
.map(Value::Number)
|
||||
.unwrap_or(Value::Number(0.into()));
|
||||
}
|
||||
serde_json::Number::from_f64(0.0)
|
||||
.map(Value::Number)
|
||||
.unwrap_or(Value::Number(0.into()))
|
||||
}
|
||||
Some("boolean") => {
|
||||
if let Some(default) = property_schema.get("default") {
|
||||
return default.clone();
|
||||
}
|
||||
Value::Bool(true)
|
||||
}
|
||||
Some("array") => {
|
||||
if let Some(default) = property_schema.get("default") {
|
||||
return default.clone();
|
||||
}
|
||||
Value::Array(vec![])
|
||||
}
|
||||
Some("object") => {
|
||||
if let Some(default) = property_schema.get("default") {
|
||||
return default.clone();
|
||||
}
|
||||
Value::Object(serde_json::Map::new())
|
||||
}
|
||||
Some("string") | None => {
|
||||
// For enum fields, use the first valid value so enum validation passes
|
||||
if let Some(enum_values) = property_schema.get("enum").and_then(|v| v.as_array()) {
|
||||
if let Some(first) = enum_values.first() {
|
||||
return first.clone();
|
||||
}
|
||||
}
|
||||
if let Some(default) = property_schema.get("default") {
|
||||
return default.clone();
|
||||
}
|
||||
Value::String("__template_placeholder__".to_string())
|
||||
}
|
||||
Some(_) => Value::Null,
|
||||
}
|
||||
}
|
||||
|
||||
/// Walk a parameters object and replace any template expression values with
|
||||
/// schema-appropriate placeholders. Only replaces leaf values that match
|
||||
/// `{{ ... }}`; non-template values are left untouched for normal validation.
|
||||
///
|
||||
/// `schema` should be the full JSON Schema object (with `properties`, `type`, etc).
|
||||
fn replace_templates_with_placeholders(params: &Value, schema: &Value) -> Value {
|
||||
match params {
|
||||
Value::Object(map) => {
|
||||
let properties = schema.get("properties").and_then(|p| p.as_object());
|
||||
|
||||
let mut result = serde_json::Map::new();
|
||||
for (key, value) in map {
|
||||
let prop_schema = properties.and_then(|p| p.get(key));
|
||||
|
||||
if is_template_expression(value) {
|
||||
// Replace with a type-appropriate placeholder
|
||||
if let Some(ps) = prop_schema {
|
||||
result.insert(key.clone(), placeholder_for_schema(ps));
|
||||
} else {
|
||||
// No schema for this property — keep as string placeholder
|
||||
result.insert(
|
||||
key.clone(),
|
||||
Value::String("__template_placeholder__".to_string()),
|
||||
);
|
||||
}
|
||||
} else if value.is_object() {
|
||||
// Recurse into nested objects
|
||||
let empty_schema = Value::Object(serde_json::Map::new());
|
||||
let nested_schema = prop_schema.unwrap_or(&empty_schema);
|
||||
result.insert(
|
||||
key.clone(),
|
||||
replace_templates_with_placeholders(value, nested_schema),
|
||||
);
|
||||
} else if value.is_array() {
|
||||
// Recurse into arrays — check each element
|
||||
if let Some(arr) = value.as_array() {
|
||||
let empty_items_schema = Value::Object(serde_json::Map::new());
|
||||
let item_schema = prop_schema
|
||||
.and_then(|ps| ps.get("items"))
|
||||
.unwrap_or(&empty_items_schema);
|
||||
let new_arr: Vec<Value> = arr
|
||||
.iter()
|
||||
.map(|item| {
|
||||
if is_template_expression(item) {
|
||||
placeholder_for_schema(item_schema)
|
||||
} else if item.is_object() || item.is_array() {
|
||||
replace_templates_with_placeholders(item, item_schema)
|
||||
} else {
|
||||
item.clone()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
result.insert(key.clone(), Value::Array(new_arr));
|
||||
} else {
|
||||
result.insert(key.clone(), value.clone());
|
||||
}
|
||||
} else {
|
||||
result.insert(key.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
Value::Object(result)
|
||||
}
|
||||
other => other.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate trigger parameters against the trigger's parameter schema.
|
||||
/// Template expressions (`{{ ... }}`) are accepted for any field type.
|
||||
pub fn validate_trigger_params(trigger: &Trigger, params: &Value) -> Result<(), ApiError> {
|
||||
// If no schema is defined, accept any parameters
|
||||
let Some(schema) = &trigger.param_schema else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// If parameters are empty object and schema exists, validate against schema
|
||||
// (schema might allow empty object or have defaults)
|
||||
// Replace template expressions with schema-appropriate placeholders
|
||||
let sanitized = replace_templates_with_placeholders(params, schema);
|
||||
|
||||
// Compile the JSON schema
|
||||
let compiled_schema = Validator::new(schema).map_err(|e| {
|
||||
@@ -26,9 +181,9 @@ pub fn validate_trigger_params(trigger: &Trigger, params: &Value) -> Result<(),
|
||||
))
|
||||
})?;
|
||||
|
||||
// Validate the parameters
|
||||
// Validate the sanitized parameters
|
||||
let errors: Vec<String> = compiled_schema
|
||||
.iter_errors(params)
|
||||
.iter_errors(&sanitized)
|
||||
.map(|e| {
|
||||
let path = e.instance_path().to_string();
|
||||
if path.is_empty() {
|
||||
@@ -50,13 +205,17 @@ pub fn validate_trigger_params(trigger: &Trigger, params: &Value) -> Result<(),
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate action parameters against the action's parameter schema
|
||||
/// Validate action parameters against the action's parameter schema.
|
||||
/// Template expressions (`{{ ... }}`) are accepted for any field type.
|
||||
pub fn validate_action_params(action: &Action, params: &Value) -> Result<(), ApiError> {
|
||||
// If no schema is defined, accept any parameters
|
||||
let Some(schema) = &action.param_schema else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// Replace template expressions with schema-appropriate placeholders
|
||||
let sanitized = replace_templates_with_placeholders(params, schema);
|
||||
|
||||
// Compile the JSON schema
|
||||
let compiled_schema = Validator::new(schema).map_err(|e| {
|
||||
ApiError::InternalServerError(format!(
|
||||
@@ -65,9 +224,9 @@ pub fn validate_action_params(action: &Action, params: &Value) -> Result<(), Api
|
||||
))
|
||||
})?;
|
||||
|
||||
// Validate the parameters
|
||||
// Validate the sanitized parameters
|
||||
let errors: Vec<String> = compiled_schema
|
||||
.iter_errors(params)
|
||||
.iter_errors(&sanitized)
|
||||
.map(|e| {
|
||||
let path = e.instance_path().to_string();
|
||||
if path.is_empty() {
|
||||
@@ -94,9 +253,10 @@ mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_validate_trigger_params_with_no_schema() {
|
||||
let trigger = Trigger {
|
||||
// ── Helper builders ──────────────────────────────────────────────
|
||||
|
||||
fn make_trigger(schema: Option<Value>) -> Trigger {
|
||||
Trigger {
|
||||
id: 1,
|
||||
r#ref: "test.trigger".to_string(),
|
||||
pack: Some(1),
|
||||
@@ -104,7 +264,7 @@ mod tests {
|
||||
label: "Test Trigger".to_string(),
|
||||
description: None,
|
||||
enabled: true,
|
||||
param_schema: None,
|
||||
param_schema: schema,
|
||||
out_schema: None,
|
||||
webhook_enabled: false,
|
||||
webhook_key: None,
|
||||
@@ -112,12 +272,43 @@ mod tests {
|
||||
is_adhoc: false,
|
||||
created: chrono::Utc::now(),
|
||||
updated: chrono::Utc::now(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn make_action(schema: Option<Value>) -> Action {
|
||||
Action {
|
||||
id: 1,
|
||||
r#ref: "test.action".to_string(),
|
||||
pack: 1,
|
||||
pack_ref: "test".to_string(),
|
||||
label: "Test Action".to_string(),
|
||||
description: "Test action".to_string(),
|
||||
entrypoint: "test.sh".to_string(),
|
||||
runtime: Some(1),
|
||||
param_schema: schema,
|
||||
out_schema: None,
|
||||
is_workflow: false,
|
||||
workflow_def: None,
|
||||
is_adhoc: false,
|
||||
parameter_delivery: attune_common::models::ParameterDelivery::default(),
|
||||
parameter_format: attune_common::models::ParameterFormat::default(),
|
||||
output_format: attune_common::models::OutputFormat::default(),
|
||||
created: chrono::Utc::now(),
|
||||
updated: chrono::Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
// ── No schema ────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_validate_trigger_params_with_no_schema() {
|
||||
let trigger = make_trigger(None);
|
||||
let params = json!({ "any": "value" });
|
||||
assert!(validate_trigger_params(&trigger, ¶ms).is_ok());
|
||||
}
|
||||
|
||||
// ── Basic trigger validation (no templates) ──────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_validate_trigger_params_with_valid_params() {
|
||||
let schema = json!({
|
||||
@@ -129,24 +320,7 @@ mod tests {
|
||||
"required": ["unit", "delta"]
|
||||
});
|
||||
|
||||
let trigger = Trigger {
|
||||
id: 1,
|
||||
r#ref: "test.trigger".to_string(),
|
||||
pack: Some(1),
|
||||
pack_ref: Some("test".to_string()),
|
||||
label: "Test Trigger".to_string(),
|
||||
description: None,
|
||||
enabled: true,
|
||||
param_schema: Some(schema),
|
||||
out_schema: None,
|
||||
webhook_enabled: false,
|
||||
webhook_key: None,
|
||||
webhook_config: None,
|
||||
is_adhoc: false,
|
||||
created: chrono::Utc::now(),
|
||||
updated: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let trigger = make_trigger(Some(schema));
|
||||
let params = json!({ "unit": "seconds", "delta": 10 });
|
||||
assert!(validate_trigger_params(&trigger, ¶ms).is_ok());
|
||||
}
|
||||
@@ -162,23 +336,7 @@ mod tests {
|
||||
"required": ["unit", "delta"]
|
||||
});
|
||||
|
||||
let trigger = Trigger {
|
||||
id: 1,
|
||||
r#ref: "test.trigger".to_string(),
|
||||
pack: Some(1),
|
||||
pack_ref: Some("test".to_string()),
|
||||
label: "Test Trigger".to_string(),
|
||||
description: None,
|
||||
enabled: true,
|
||||
param_schema: Some(schema),
|
||||
out_schema: None,
|
||||
webhook_enabled: false,
|
||||
webhook_key: None,
|
||||
webhook_config: None,
|
||||
is_adhoc: false,
|
||||
created: chrono::Utc::now(),
|
||||
updated: chrono::Utc::now(),
|
||||
};
|
||||
let trigger = make_trigger(Some(schema));
|
||||
|
||||
// Missing required field 'delta'
|
||||
let params = json!({ "unit": "seconds" });
|
||||
@@ -193,6 +351,8 @@ mod tests {
|
||||
assert!(validate_trigger_params(&trigger, ¶ms).is_err());
|
||||
}
|
||||
|
||||
// ── Basic action validation (no templates) ───────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_validate_action_params_with_valid_params() {
|
||||
let schema = json!({
|
||||
@@ -203,27 +363,7 @@ mod tests {
|
||||
"required": ["message"]
|
||||
});
|
||||
|
||||
let action = Action {
|
||||
id: 1,
|
||||
r#ref: "test.action".to_string(),
|
||||
pack: 1,
|
||||
pack_ref: "test".to_string(),
|
||||
label: "Test Action".to_string(),
|
||||
description: "Test action".to_string(),
|
||||
entrypoint: "test.sh".to_string(),
|
||||
runtime: Some(1),
|
||||
param_schema: Some(schema),
|
||||
out_schema: None,
|
||||
is_workflow: false,
|
||||
workflow_def: None,
|
||||
is_adhoc: false,
|
||||
parameter_delivery: attune_common::models::ParameterDelivery::default(),
|
||||
parameter_format: attune_common::models::ParameterFormat::default(),
|
||||
output_format: attune_common::models::OutputFormat::default(),
|
||||
created: chrono::Utc::now(),
|
||||
updated: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let action = make_action(Some(schema));
|
||||
let params = json!({ "message": "Hello, world!" });
|
||||
assert!(validate_action_params(&action, ¶ms).is_ok());
|
||||
}
|
||||
@@ -238,28 +378,327 @@ mod tests {
|
||||
"required": ["message"]
|
||||
});
|
||||
|
||||
let action = Action {
|
||||
id: 2,
|
||||
r#ref: "test.action".to_string(),
|
||||
pack: 1,
|
||||
pack_ref: "test".to_string(),
|
||||
label: "Test Action".to_string(),
|
||||
description: "Test action".to_string(),
|
||||
entrypoint: "test.sh".to_string(),
|
||||
runtime: Some(1),
|
||||
param_schema: Some(schema),
|
||||
out_schema: None,
|
||||
is_workflow: false,
|
||||
workflow_def: None,
|
||||
is_adhoc: false,
|
||||
parameter_delivery: attune_common::models::ParameterDelivery::default(),
|
||||
parameter_format: attune_common::models::ParameterFormat::default(),
|
||||
output_format: attune_common::models::OutputFormat::default(),
|
||||
created: chrono::Utc::now(),
|
||||
updated: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let action = make_action(Some(schema));
|
||||
let params = json!({});
|
||||
assert!(validate_action_params(&action, ¶ms).is_err());
|
||||
}
|
||||
|
||||
// ── Template-aware validation ────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_template_in_integer_field_passes() {
|
||||
let schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"counter": { "type": "integer" }
|
||||
},
|
||||
"required": ["counter"]
|
||||
});
|
||||
|
||||
let action = make_action(Some(schema));
|
||||
let params = json!({ "counter": "{{ event.payload.counter }}" });
|
||||
assert!(validate_action_params(&action, ¶ms).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_template_in_boolean_field_passes() {
|
||||
let schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"verbose": { "type": "boolean" }
|
||||
},
|
||||
"required": ["verbose"]
|
||||
});
|
||||
|
||||
let action = make_action(Some(schema));
|
||||
let params = json!({ "verbose": "{{ event.payload.debug }}" });
|
||||
assert!(validate_action_params(&action, ¶ms).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_template_in_number_field_passes() {
|
||||
let schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"threshold": { "type": "number", "minimum": 0.0 }
|
||||
},
|
||||
"required": ["threshold"]
|
||||
});
|
||||
|
||||
let action = make_action(Some(schema));
|
||||
let params = json!({ "threshold": "{{ event.payload.threshold }}" });
|
||||
assert!(validate_action_params(&action, ¶ms).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_template_in_enum_field_passes() {
|
||||
let schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level": { "type": "string", "enum": ["info", "warn", "error"] }
|
||||
},
|
||||
"required": ["level"]
|
||||
});
|
||||
|
||||
let action = make_action(Some(schema));
|
||||
let params = json!({ "level": "{{ event.payload.severity }}" });
|
||||
assert!(validate_action_params(&action, ¶ms).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_template_in_array_field_passes() {
|
||||
let schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"recipients": { "type": "array", "items": { "type": "string" } }
|
||||
},
|
||||
"required": ["recipients"]
|
||||
});
|
||||
|
||||
let action = make_action(Some(schema));
|
||||
let params = json!({ "recipients": "{{ event.payload.emails }}" });
|
||||
assert!(validate_action_params(&action, ¶ms).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_template_in_object_field_passes() {
|
||||
let schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"metadata": { "type": "object" }
|
||||
},
|
||||
"required": ["metadata"]
|
||||
});
|
||||
|
||||
let action = make_action(Some(schema));
|
||||
let params = json!({ "metadata": "{{ event.payload.meta }}" });
|
||||
assert!(validate_action_params(&action, ¶ms).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mixed_template_and_literal_values() {
|
||||
let schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": { "type": "string" },
|
||||
"count": { "type": "integer" },
|
||||
"verbose": { "type": "boolean" }
|
||||
},
|
||||
"required": ["message", "count", "verbose"]
|
||||
});
|
||||
|
||||
let action = make_action(Some(schema));
|
||||
|
||||
// Mix of literal and template values
|
||||
let params = json!({
|
||||
"message": "Hello",
|
||||
"count": "{{ event.payload.count }}",
|
||||
"verbose": true
|
||||
});
|
||||
assert!(validate_action_params(&action, ¶ms).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_literal_values_still_validated() {
|
||||
let schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": { "type": "string" },
|
||||
"count": { "type": "integer" }
|
||||
},
|
||||
"required": ["message", "count"]
|
||||
});
|
||||
|
||||
let action = make_action(Some(schema));
|
||||
|
||||
// Template for message is fine, but literal "not_a_number" for integer is not
|
||||
let params = json!({
|
||||
"message": "{{ event.payload.msg }}",
|
||||
"count": "not_a_number"
|
||||
});
|
||||
assert!(validate_action_params(&action, ¶ms).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_field_still_enforced_with_templates() {
|
||||
let schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": { "type": "string" },
|
||||
"count": { "type": "integer" }
|
||||
},
|
||||
"required": ["message", "count"]
|
||||
});
|
||||
|
||||
let action = make_action(Some(schema));
|
||||
|
||||
// Only message provided (even as template), count is missing
|
||||
let params = json!({ "message": "{{ event.payload.msg }}" });
|
||||
assert!(validate_action_params(&action, ¶ms).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pack_config_template_passes() {
|
||||
let schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"api_key": { "type": "string" },
|
||||
"timeout": { "type": "integer" }
|
||||
},
|
||||
"required": ["api_key", "timeout"]
|
||||
});
|
||||
|
||||
let action = make_action(Some(schema));
|
||||
let params = json!({
|
||||
"api_key": "{{ pack.config.api_key }}",
|
||||
"timeout": "{{ pack.config.default_timeout }}"
|
||||
});
|
||||
assert!(validate_action_params(&action, ¶ms).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_template_passes() {
|
||||
let schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"timestamp": { "type": "string" },
|
||||
"rule_id": { "type": "integer" }
|
||||
},
|
||||
"required": ["timestamp", "rule_id"]
|
||||
});
|
||||
|
||||
let action = make_action(Some(schema));
|
||||
let params = json!({
|
||||
"timestamp": "{{ system.timestamp }}",
|
||||
"rule_id": "{{ system.rule.id }}"
|
||||
});
|
||||
assert!(validate_action_params(&action, ¶ms).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trigger_params_template_aware() {
|
||||
let schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"unit": { "type": "string", "enum": ["seconds", "minutes", "hours"] },
|
||||
"delta": { "type": "integer", "minimum": 1 }
|
||||
},
|
||||
"required": ["unit", "delta"]
|
||||
});
|
||||
|
||||
let trigger = make_trigger(Some(schema));
|
||||
|
||||
// Both fields as templates
|
||||
let params = json!({
|
||||
"unit": "{{ pack.config.timer_unit }}",
|
||||
"delta": "{{ pack.config.timer_delta }}"
|
||||
});
|
||||
assert!(validate_trigger_params(&trigger, ¶ms).is_ok());
|
||||
}
|
||||
|
||||
// ── Placeholder generation ───────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_is_template_expression() {
|
||||
assert!(is_template_expression(&json!("{{ event.payload.x }}")));
|
||||
assert!(is_template_expression(&json!("{{ pack.config.key }}")));
|
||||
assert!(is_template_expression(&json!(
|
||||
"prefix {{ system.ts }} suffix"
|
||||
)));
|
||||
assert!(!is_template_expression(&json!("no braces here")));
|
||||
assert!(!is_template_expression(&json!(42)));
|
||||
assert!(!is_template_expression(&json!(true)));
|
||||
assert!(!is_template_expression(&json!("{ single braces }")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_placeholder_for_schema_types() {
|
||||
assert_eq!(
|
||||
placeholder_for_schema(&json!({"type": "integer"})),
|
||||
json!(0)
|
||||
);
|
||||
assert_eq!(
|
||||
placeholder_for_schema(&json!({"type": "number"})),
|
||||
json!(0.0)
|
||||
);
|
||||
assert_eq!(
|
||||
placeholder_for_schema(&json!({"type": "boolean"})),
|
||||
json!(true)
|
||||
);
|
||||
assert_eq!(placeholder_for_schema(&json!({"type": "array"})), json!([]));
|
||||
assert_eq!(
|
||||
placeholder_for_schema(&json!({"type": "object"})),
|
||||
json!({})
|
||||
);
|
||||
assert_eq!(
|
||||
placeholder_for_schema(&json!({"type": "string"})),
|
||||
json!("__template_placeholder__")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_placeholder_respects_enum() {
|
||||
let schema = json!({"type": "string", "enum": ["a", "b", "c"]});
|
||||
assert_eq!(placeholder_for_schema(&schema), json!("a"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_placeholder_respects_default() {
|
||||
let schema = json!({"type": "integer", "default": 42});
|
||||
assert_eq!(placeholder_for_schema(&schema), json!(42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_placeholder_respects_minimum() {
|
||||
let schema = json!({"type": "integer", "minimum": 5});
|
||||
assert_eq!(placeholder_for_schema(&schema), json!(5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nested_object_template_replacement() {
|
||||
let schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"outer": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"inner_count": { "type": "integer" }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let params = json!({
|
||||
"outer": {
|
||||
"inner_count": "{{ event.payload.count }}"
|
||||
}
|
||||
});
|
||||
|
||||
let sanitized = replace_templates_with_placeholders(¶ms, &schema);
|
||||
// The inner template should be replaced with an integer placeholder
|
||||
assert!(sanitized["outer"]["inner_count"].is_number());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_array_element_template_replacement() {
|
||||
let schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let params = json!({
|
||||
"tags": ["literal", "{{ event.payload.tag }}"]
|
||||
});
|
||||
|
||||
let sanitized = replace_templates_with_placeholders(¶ms, &schema);
|
||||
let tags = sanitized["tags"].as_array().unwrap();
|
||||
assert_eq!(tags[0], "literal");
|
||||
assert!(tags[1].is_string());
|
||||
assert_ne!(tags[1], "{{ event.payload.tag }}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user