use crate::enums::AdvanceReason;
use crate::error::{Diagnostic, DiagnosticSeverity, ParseError, ParseErrorKind};
use crate::types::*;
use regex::Regex;
use serde_json::Value;
use std::collections::HashMap;
use std::time::Duration;
pub(crate) fn compile_user_regex(pattern: &str) -> Result<Regex, regex::Error> {
regex::RegexBuilder::new(pattern)
.size_limit(1 << 20) .dfa_size_limit(1 << 20) .build()
}
pub use crate::event_registry::extract_protocol;
pub fn resolve_simple_path(path: &str, value: &Value) -> Option<Value> {
if path.is_empty() {
return Some(value.clone());
}
let mut current = value;
for segment in path.split('.') {
match current.as_object() {
Some(obj) => match obj.get(segment) {
Some(v) => current = v,
None => return None,
},
None => return None,
}
}
Some(current.clone())
}
pub fn resolve_wildcard_path(path: &str, value: &Value) -> Vec<Value> {
if path.is_empty() {
return vec![value.clone()];
}
let segments = match split_wildcard_segments(path) {
Some(s) => s,
None => return vec![],
};
let mut current = vec![value.clone()];
for seg in &segments {
if current.is_empty() {
break;
}
let mut next = Vec::new();
for val in ¤t {
if seg.wildcard {
let target = if seg.name.is_empty() {
val.clone()
} else {
match val.as_object().and_then(|o| o.get(&seg.name)) {
Some(v) => v.clone(),
None => continue,
}
};
if let Some(arr) = target.as_array() {
next.extend(arr.iter().cloned());
}
} else if let Some(v) = val.as_object().and_then(|o| o.get(&seg.name)) {
next.push(v.clone());
}
}
current = next;
}
current
}
struct WildcardSegment {
name: String,
wildcard: bool,
}
fn is_path_segment_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_' || c == '-'
}
fn split_wildcard_segments(path: &str) -> Option<Vec<WildcardSegment>> {
let mut segments = Vec::new();
let mut current = String::new();
let chars: Vec<char> = path.chars().collect();
let mut i = 0;
let mut expect_segment = true;
while i < chars.len() {
match chars[i] {
'.' => {
if !current.is_empty() {
segments.push(WildcardSegment {
name: current.clone(),
wildcard: false,
});
current.clear();
} else if !expect_segment {
} else {
return None; }
expect_segment = true;
i += 1;
}
'[' => {
if i + 2 < chars.len() && chars[i + 1] == '*' && chars[i + 2] == ']' {
if current.is_empty() && segments.is_empty() {
return None;
}
segments.push(WildcardSegment {
name: current.clone(),
wildcard: true,
});
current.clear();
i += 3;
expect_segment = false;
if i < chars.len() {
if chars[i] == '.' {
expect_segment = true;
i += 1;
} else {
return None;
}
}
} else if i + 1 < chars.len() && chars[i + 1] == '-' {
return None; } else {
return None; }
}
c if is_path_segment_char(c) => {
current.push(c);
i += 1;
}
_ => {
return None; }
}
}
if !current.is_empty() {
segments.push(WildcardSegment {
name: current,
wildcard: false,
});
} else if expect_segment && !segments.is_empty() {
return None;
}
Some(segments)
}
pub fn is_valid_wildcard_dot_path(path: &str) -> bool {
if path.is_empty() {
return true;
}
split_wildcard_segments(path).is_some()
}
pub fn is_valid_simple_dot_path(path: &str) -> bool {
if path.is_empty() {
return true;
}
for seg in path.split('.') {
if seg.is_empty() {
return false;
}
if !seg.chars().all(is_path_segment_char) {
return false;
}
}
true
}
pub fn parse_duration(input: &str) -> Result<Duration, ParseError> {
if input.is_empty() {
return Err(duration_error("empty duration string"));
}
if input.starts_with('P') {
parse_iso_duration(input)
} else {
parse_shorthand_duration(input)
}
}
fn parse_shorthand_duration(input: &str) -> Result<Duration, ParseError> {
if input.len() < 2 {
return Err(duration_error(&format!(
"invalid shorthand duration: '{}'",
input
)));
}
let split_pos = input
.char_indices()
.next_back()
.map(|(i, _)| i)
.unwrap_or(0);
let (num_str, unit) = input.split_at(split_pos);
let n: u64 = num_str
.parse()
.map_err(|_| duration_error(&format!("invalid shorthand duration: '{}'", input)))?;
let secs = match unit {
"s" => Some(n),
"m" => n.checked_mul(60),
"h" => n.checked_mul(3600),
"d" => n.checked_mul(86400),
_ => {
return Err(duration_error(&format!(
"unknown duration unit: '{}'",
unit
)));
}
};
let secs =
secs.ok_or_else(|| duration_error(&format!("duration value too large: '{}'", input)))?;
Ok(Duration::from_secs(secs))
}
fn parse_iso_duration(input: &str) -> Result<Duration, ParseError> {
let rest = &input[1..]; let mut total_secs: u64 = 0;
let (date_part, time_part) = if let Some(t_pos) = rest.find('T') {
(&rest[..t_pos], Some(&rest[t_pos + 1..]))
} else {
(rest, None)
};
if !date_part.is_empty() {
if let Some(num_str) = date_part.strip_suffix('D') {
let n: u64 = num_str
.parse()
.map_err(|_| duration_error(&format!("invalid ISO duration: '{}'", input)))?;
total_secs = n
.checked_mul(86400)
.and_then(|v| total_secs.checked_add(v))
.ok_or_else(|| duration_error(&format!("duration value too large: '{}'", input)))?;
} else {
return Err(duration_error(&format!(
"invalid ISO duration: '{}'",
input
)));
}
}
if let Some(time) = time_part {
if time.is_empty() {
return Err(duration_error(&format!(
"invalid ISO duration: '{}'",
input
)));
}
let mut remaining = time;
if let Some(h_pos) = remaining.find('H') {
let n: u64 = remaining[..h_pos]
.parse()
.map_err(|_| duration_error(&format!("invalid ISO duration: '{}'", input)))?;
total_secs = n
.checked_mul(3600)
.and_then(|v| total_secs.checked_add(v))
.ok_or_else(|| duration_error(&format!("duration value too large: '{}'", input)))?;
remaining = &remaining[h_pos + 1..];
}
if let Some(m_pos) = remaining.find('M') {
let n: u64 = remaining[..m_pos]
.parse()
.map_err(|_| duration_error(&format!("invalid ISO duration: '{}'", input)))?;
total_secs = n
.checked_mul(60)
.and_then(|v| total_secs.checked_add(v))
.ok_or_else(|| duration_error(&format!("duration value too large: '{}'", input)))?;
remaining = &remaining[m_pos + 1..];
}
if let Some(s_pos) = remaining.find('S') {
let n: u64 = remaining[..s_pos]
.parse()
.map_err(|_| duration_error(&format!("invalid ISO duration: '{}'", input)))?;
total_secs = total_secs
.checked_add(n)
.ok_or_else(|| duration_error(&format!("duration value too large: '{}'", input)))?;
remaining = &remaining[s_pos + 1..];
}
if !remaining.is_empty() {
return Err(duration_error(&format!(
"invalid ISO duration: '{}'",
input
)));
}
}
if date_part.is_empty() && time_part.is_none() {
return Err(duration_error(&format!(
"invalid ISO duration: '{}'",
input
)));
}
Ok(Duration::from_secs(total_secs))
}
fn duration_error(message: &str) -> ParseError {
ParseError {
kind: ParseErrorKind::Syntax,
message: message.to_string(),
path: None,
line: None,
column: None,
}
}
pub fn evaluate_condition(condition: &Condition, value: &Value) -> bool {
match condition {
Condition::Equality(expected) => values_deep_equal(value, expected),
Condition::Operators(cond) => evaluate_match_condition(cond, value),
}
}
pub fn evaluate_match_condition(cond: &MatchCondition, value: &Value) -> bool {
let need_string_op = cond.contains.is_some()
|| cond.starts_with.is_some()
|| cond.ends_with.is_some()
|| cond.regex.is_some();
if need_string_op {
let text = value_to_string(value);
if let Some(ref s) = cond.contains
&& !text.contains(s.as_str())
{
return false;
}
if let Some(ref s) = cond.starts_with
&& !text.starts_with(s.as_str())
{
return false;
}
if let Some(ref s) = cond.ends_with
&& !text.ends_with(s.as_str())
{
return false;
}
if let Some(ref pattern) = cond.regex {
if let Ok(re) = compile_user_regex(pattern) {
if !re.is_match(&text) {
return false;
}
} else {
return false; }
}
}
if let Some(ref items) = cond.any_of
&& !items.iter().any(|item| values_deep_equal(value, item))
{
return false;
}
if let Some(threshold) = cond.gt {
match value.as_f64() {
Some(v) if v > threshold => {}
_ => return false,
}
}
if let Some(threshold) = cond.lt {
match value.as_f64() {
Some(v) if v < threshold => {}
_ => return false,
}
}
if let Some(threshold) = cond.gte {
match value.as_f64() {
Some(v) if v >= threshold => {}
_ => return false,
}
}
if let Some(threshold) = cond.lte {
match value.as_f64() {
Some(v) if v <= threshold => {}
_ => return false,
}
}
true
}
fn values_deep_equal(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Null, Value::Null) => true,
(Value::Bool(a), Value::Bool(b)) => a == b,
(Value::Number(a), Value::Number(b)) => match (a.as_f64(), b.as_f64()) {
(Some(fa), Some(fb)) => fa == fb,
_ => a == b,
},
(Value::String(a), Value::String(b)) => a == b,
(Value::Array(a), Value::Array(b)) => {
a.len() == b.len() && a.iter().zip(b.iter()).all(|(a, b)| values_deep_equal(a, b))
}
(Value::Object(a), Value::Object(b)) => {
if a.len() != b.len() {
return false;
}
a.iter()
.all(|(k, v)| b.get(k).is_some_and(|bv| values_deep_equal(v, bv)))
}
_ => false,
}
}
pub fn evaluate_predicate(predicate: &MatchPredicate, value: &Value) -> bool {
for (path, entry) in predicate {
let resolved = resolve_simple_path(path, value);
match entry {
MatchEntry::Scalar(expected) => match &resolved {
Some(val) => {
if !values_deep_equal(val, expected) {
return false;
}
}
None => return false,
},
MatchEntry::Condition(cond) => {
match cond {
MatchCondition {
exists: Some(false),
..
} => {
if resolved.is_some() {
return false;
}
let has_other_ops = cond.contains.is_some()
|| cond.starts_with.is_some()
|| cond.ends_with.is_some()
|| cond.regex.is_some()
|| cond.any_of.is_some()
|| cond.gt.is_some()
|| cond.lt.is_some()
|| cond.gte.is_some()
|| cond.lte.is_some();
if has_other_ops {
return false;
}
}
MatchCondition {
exists: Some(true), ..
} => {
let Some(val) = &resolved else {
return false;
};
if !evaluate_match_condition_excluding_exists(cond, val) {
return false;
}
}
_ => {
match &resolved {
Some(val) => {
if !evaluate_match_condition(cond, val) {
return false;
}
}
None => return false,
}
}
}
}
}
}
true
}
fn evaluate_match_condition_excluding_exists(cond: &MatchCondition, value: &Value) -> bool {
let without_exists = MatchCondition {
contains: cond.contains.clone(),
starts_with: cond.starts_with.clone(),
ends_with: cond.ends_with.clone(),
regex: cond.regex.clone(),
any_of: cond.any_of.clone(),
gt: cond.gt,
lt: cond.lt,
gte: cond.gte,
lte: cond.lte,
exists: None,
};
evaluate_match_condition(&without_exists, value)
}
pub fn interpolate_template(
template: &str,
extractors: &HashMap<String, String>,
request: Option<&Value>,
response: Option<&Value>,
) -> (String, Vec<Diagnostic>) {
let mut diagnostics = Vec::new();
const PLACEHOLDER: &str = "\x00ESCAPED_OPEN_BRACE\x00";
let working = template.replace("\\{{", PLACEHOLDER);
let mut result = String::new();
let mut remaining = working.as_str();
while let Some(start) = remaining.find("{{") {
result.push_str(&remaining[..start]);
let after_open = &remaining[start + 2..];
if let Some(end) = after_open.find("}}") {
let expr = &after_open[..end];
if let Some(val) = extractors.get(expr) {
result.push_str(val);
}
else if let Some(rest) = expr.strip_prefix("request.") {
if let Some(req) = request {
match resolve_simple_path(rest, req) {
Some(v) => result.push_str(&value_to_string(&v)),
None => {
diagnostics.push(w004_diagnostic(expr));
}
}
} else {
diagnostics.push(w004_diagnostic(expr));
}
}
else if let Some(rest) = expr.strip_prefix("response.") {
if let Some(resp) = response {
match resolve_simple_path(rest, resp) {
Some(v) => result.push_str(&value_to_string(&v)),
None => {
diagnostics.push(w004_diagnostic(expr));
}
}
} else {
diagnostics.push(w004_diagnostic(expr));
}
}
else {
diagnostics.push(w004_diagnostic(expr));
}
remaining = &after_open[end + 2..];
} else {
result.push_str("{{");
remaining = after_open;
}
}
result.push_str(remaining);
let final_result = result.replace(PLACEHOLDER, "{{");
(final_result, diagnostics)
}
fn value_to_string(v: &Value) -> String {
match v {
Value::String(s) => s.clone(),
Value::Null => "null".to_string(),
Value::Bool(b) => b.to_string(),
Value::Number(n) => n.to_string(),
_ => {
serde_json::to_string(&sort_keys(v)).unwrap_or_else(|_| "<unserializable>".to_string())
}
}
}
const MAX_VALUE_DEPTH: usize = 128;
fn sort_keys(v: &Value) -> Value {
sort_keys_inner(v, 0)
}
fn sort_keys_inner(v: &Value, depth: usize) -> Value {
if depth > MAX_VALUE_DEPTH {
return v.clone();
}
match v {
Value::Object(map) => {
let mut sorted: serde_json::Map<String, Value> = serde_json::Map::new();
let mut keys: Vec<&String> = map.keys().collect();
keys.sort();
for k in keys {
sorted.insert(k.clone(), sort_keys_inner(&map[k], depth + 1));
}
Value::Object(sorted)
}
Value::Array(arr) => {
Value::Array(arr.iter().map(|v| sort_keys_inner(v, depth + 1)).collect())
}
_ => v.clone(),
}
}
fn w004_diagnostic(expr: &str) -> Diagnostic {
Diagnostic {
severity: DiagnosticSeverity::Warning,
code: "W-004".to_string(),
path: None,
message: format!("unresolvable template reference: '{}'", expr),
}
}
pub fn interpolate_value(
value: &Value,
extractors: &HashMap<String, String>,
request: Option<&Value>,
response: Option<&Value>,
) -> (Value, Vec<Diagnostic>) {
let mut diagnostics = Vec::new();
let result = interpolate_value_inner(value, extractors, request, response, &mut diagnostics, 0);
(result, diagnostics)
}
fn interpolate_value_inner(
value: &Value,
extractors: &HashMap<String, String>,
request: Option<&Value>,
response: Option<&Value>,
diagnostics: &mut Vec<Diagnostic>,
depth: usize,
) -> Value {
if depth > MAX_VALUE_DEPTH {
return value.clone();
}
match value {
Value::String(s) => {
if s.contains("{{") {
let (interpolated, diags) = interpolate_template(s, extractors, request, response);
diagnostics.extend(diags);
Value::String(interpolated)
} else {
value.clone()
}
}
Value::Object(map) => {
let new_map: serde_json::Map<String, Value> = map
.iter()
.map(|(k, v)| {
let new_v = interpolate_value_inner(
v,
extractors,
request,
response,
diagnostics,
depth + 1,
);
(k.clone(), new_v)
})
.collect();
Value::Object(new_map)
}
Value::Array(arr) => {
let new_arr: Vec<Value> = arr
.iter()
.map(|v| {
interpolate_value_inner(
v,
extractors,
request,
response,
diagnostics,
depth + 1,
)
})
.collect();
Value::Array(new_arr)
}
_ => value.clone(),
}
}
pub fn evaluate_extractor(
extractor: &Extractor,
message: &Value,
direction: crate::enums::ExtractorSource,
) -> Option<String> {
if extractor.source != direction {
return None;
}
match extractor.extractor_type {
crate::enums::ExtractorType::JsonPath => {
evaluate_extractor_jsonpath(&extractor.selector, message)
}
crate::enums::ExtractorType::Regex => {
evaluate_extractor_regex(&extractor.selector, message)
}
}
}
fn evaluate_extractor_jsonpath(selector: &str, message: &Value) -> Option<String> {
let path = serde_json_path::JsonPath::parse(selector).ok()?;
let node_list = path.query(message);
let first = node_list.first()?;
Some(extractor_value_to_string(first))
}
fn extractor_value_to_string(v: &Value) -> String {
match v {
Value::String(s) => s.clone(),
Value::Null => "null".to_string(),
Value::Bool(b) => b.to_string(),
Value::Number(n) => n.to_string(),
_ => serde_json::to_string(v).unwrap_or_else(|_| "<unserializable>".to_string()),
}
}
fn evaluate_extractor_regex(selector: &str, message: &Value) -> Option<String> {
let text = extractor_value_to_string(message);
let re = compile_user_regex(selector).ok()?;
let caps = re.captures(&text)?;
if caps.len() < 2 {
return None; }
caps.get(1).map(|m| m.as_str().to_string())
}
pub fn select_response<'a>(
entries: &'a [ResponseEntry],
request: &Value,
) -> Option<&'a ResponseEntry> {
let mut default_entry: Option<&ResponseEntry> = None;
for entry in entries {
match &entry.when {
Some(predicate) => {
if evaluate_predicate(predicate, request) {
return Some(entry);
}
}
None => {
if default_entry.is_none() {
default_entry = Some(entry);
}
}
}
}
default_entry
}
pub fn evaluate_trigger(
trigger: &Trigger,
event: Option<&ProtocolEvent>,
elapsed: Duration,
state: &mut TriggerState,
) -> TriggerResult {
if let Some(after) = &trigger.after
&& let Ok(timeout) = parse_duration(after)
&& elapsed >= timeout
{
return TriggerResult::Advanced {
reason: AdvanceReason::Timeout,
};
}
if let (Some(trigger_event), Some(ev)) = (&trigger.event, event) {
if trigger_event != &ev.event_type {
return TriggerResult::NotAdvanced;
}
if let Some(predicate) = &trigger.match_predicate
&& !evaluate_predicate(predicate, &ev.content)
{
return TriggerResult::NotAdvanced;
}
state.event_count += 1;
let required_count = trigger.count.unwrap_or(1).max(1) as u64;
if state.event_count >= required_count {
return TriggerResult::Advanced {
reason: AdvanceReason::EventMatched,
};
}
}
TriggerResult::NotAdvanced
}
pub fn compute_effective_state(phases: &[Phase], phase_index: usize) -> Value {
let mut effective = Value::Null;
for (i, phase) in phases.iter().enumerate() {
if i > phase_index {
break;
}
if let Some(state) = &phase.state {
effective = state.clone();
}
}
effective
}