1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct Predicate {
13 #[serde(flatten)]
14 pub parameters: PredicateParameters,
15 #[serde(flatten)]
16 pub operation: PredicateOperation,
17}
18
19#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase")]
22pub enum PredicateOperation {
23 Equals(HashMap<String, serde_json::Value>),
24 DeepEquals(HashMap<String, serde_json::Value>),
25 Contains(HashMap<String, serde_json::Value>),
26 StartsWith(HashMap<String, serde_json::Value>),
27 EndsWith(HashMap<String, serde_json::Value>),
28 Matches(HashMap<String, serde_json::Value>),
29 Exists(HashMap<String, serde_json::Value>),
30 Not(Box<Predicate>),
31 Or(Vec<Predicate>),
32 And(Vec<Predicate>),
33 Inject(String),
34}
35
36#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
38#[serde(rename_all = "camelCase")]
39pub struct PredicateParameters {
40 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub case_sensitive: Option<bool>,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub key_case_sensitive: Option<bool>,
44 #[serde(default, skip_serializing_if = "String::is_empty")]
45 pub except: String,
46 #[serde(flatten)]
47 pub selector: Option<PredicateSelector>,
48}
49
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
52#[serde(rename_all = "lowercase")]
53pub enum PredicateSelector {
54 XPath {
55 selector: String,
56 #[serde(rename = "ns", default, skip_serializing_if = "Option::is_none")]
57 namespaces: Option<HashMap<String, String>>,
58 },
59 JsonPath {
60 selector: String,
61 },
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67 use serde_json::json;
68
69 #[test]
70 fn deserializes_into_concrete_operation() {
71 let pred: Predicate =
74 serde_json::from_value(json!({ "equals": { "path": "/hello" } })).unwrap();
75 match pred.operation {
76 PredicateOperation::Equals(fields) => {
77 assert_eq!(fields.get("path").unwrap(), "/hello");
78 }
79 other => panic!("expected Equals, got {other:?}"),
80 }
81 }
82
83 #[test]
84 fn round_trips_parameters_and_selector() {
85 let value = json!({
86 "equals": { "body": "x" },
87 "caseSensitive": false,
88 "jsonpath": { "selector": "$.id" }
89 });
90 let pred: Predicate = serde_json::from_value(value).unwrap();
91 assert_eq!(pred.parameters.case_sensitive, Some(false));
92 assert!(matches!(
93 pred.parameters.selector,
94 Some(PredicateSelector::JsonPath { .. })
95 ));
96 assert!(matches!(pred.operation, PredicateOperation::Equals(_)));
97
98 let back = serde_json::to_value(&pred).unwrap();
100 assert_eq!(back["caseSensitive"], json!(false));
101 assert!(back["jsonpath"]["selector"] == json!("$.id"));
102 }
103
104 #[test]
105 fn xpath_selector_round_trips_with_namespaces() {
106 let value = json!({
109 "equals": { "body": "x" },
110 "xpath": { "selector": "//a:user", "ns": { "a": "urn:y" } }
111 });
112 let pred: Predicate = serde_json::from_value(value).unwrap();
113 let Some(PredicateSelector::XPath {
114 selector,
115 namespaces,
116 }) = &pred.parameters.selector
117 else {
118 panic!("expected an XPath selector");
119 };
120 assert_eq!(selector, "//a:user");
121 assert_eq!(namespaces.as_ref().unwrap().get("a").unwrap(), "urn:y");
122
123 let back = serde_json::to_value(&pred).unwrap();
124 assert_eq!(back["xpath"]["selector"], json!("//a:user"));
125 assert_eq!(
126 back["xpath"]["ns"]["a"],
127 json!("urn:y"),
128 "the `ns` rename is preserved"
129 );
130 }
131
132 #[test]
133 fn except_and_key_case_sensitive_round_trip() {
134 let pred: Predicate = serde_json::from_value(json!({
135 "matches": { "path": "/x" },
136 "except": "^/skip",
137 "keyCaseSensitive": true
138 }))
139 .unwrap();
140 assert_eq!(pred.parameters.except, "^/skip");
141 assert_eq!(pred.parameters.key_case_sensitive, Some(true));
142 let back = serde_json::to_value(&pred).unwrap();
143 assert_eq!(back["except"], json!("^/skip"));
144 assert_eq!(back["keyCaseSensitive"], json!(true));
145
146 let empty: Predicate =
148 serde_json::from_value(json!({ "matches": { "path": "/x" } })).unwrap();
149 assert!(
150 serde_json::to_value(&empty)
151 .unwrap()
152 .get("except")
153 .is_none()
154 );
155 }
156
157 #[test]
158 fn nests_logical_operators() {
159 let pred: Predicate = serde_json::from_value(json!({
160 "and": [
161 { "equals": { "method": "GET" } },
162 { "not": { "exists": { "headers": { "X": true } } } }
163 ]
164 }))
165 .unwrap();
166 let PredicateOperation::And(subs) = pred.operation else {
167 panic!("expected And");
168 };
169 assert_eq!(subs.len(), 2);
170 assert!(matches!(subs[1].operation, PredicateOperation::Not(_)));
171 }
172}