pub(crate) const MOCK_REQUESTS_ROOT: &str = "mock.requests";
#[cfg_attr(
not(test),
expect(
dead_code,
reason = "no emitter consumes it until the reference-backend wave -- #433"
)
)]
pub(crate) const MOCK_VIRTUAL_FIELDS: &[&str] = &["mock.requests.total"];
pub(crate) const MOCK_RECIPE: &str = "mock_requests";
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum MockQuery {
Total,
Count(String),
BodyPredicate {
key: String,
path: String,
op: BodyOp,
value_json: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum BodyOp {
Equals,
NotEquals,
}
impl BodyOp {
pub(crate) fn symbol(self) -> &'static str {
match self {
Self::Equals => "==",
Self::NotEquals => "!=",
}
}
pub(crate) fn wire_name(self) -> &'static str {
match self {
Self::Equals => "equals",
Self::NotEquals => "not_equals",
}
}
}
const BODY_SEGMENT: &str = "].body.";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum MockFieldSyntax {
NotMockField,
BareRoot,
UnrecognizedTail,
UnbalancedBracket,
MissingQuotedKey,
EmptyKey,
}
pub(crate) fn parse_mock_field(field: &str) -> Result<MockQuery, MockFieldSyntax> {
let Some(rest) = field.strip_prefix(MOCK_REQUESTS_ROOT) else {
return Err(MockFieldSyntax::NotMockField);
};
if rest.is_empty() {
return Err(MockFieldSyntax::BareRoot);
}
if rest == ".total" {
return Ok(MockQuery::Total);
}
let Some(bracketed) = rest.strip_prefix('[') else {
return Err(MockFieldSyntax::UnrecognizedTail);
};
if let Some((head, body_tail)) = bracketed.split_once(BODY_SEGMENT) {
let key = parse_quoted_key(head)?;
return parse_body_predicate(key, body_tail);
}
let Some(inner) = bracketed.strip_suffix(']') else {
return Err(MockFieldSyntax::UnbalancedBracket);
};
let Some(key) = inner.strip_prefix('"').and_then(|rest| rest.strip_suffix('"')) else {
return Err(MockFieldSyntax::MissingQuotedKey);
};
if key.is_empty() {
return Err(MockFieldSyntax::EmptyKey);
}
Ok(MockQuery::Count(key.to_string()))
}
fn parse_quoted_key(inner: &str) -> Result<String, MockFieldSyntax> {
let Some(key) = inner.strip_prefix('"').and_then(|rest| rest.strip_suffix('"')) else {
return Err(MockFieldSyntax::MissingQuotedKey);
};
if key.is_empty() {
return Err(MockFieldSyntax::EmptyKey);
}
Ok(key.to_string())
}
fn parse_body_predicate(key: String, tail: &str) -> Result<MockQuery, MockFieldSyntax> {
let Some((path, comparison)) = tail.split_once(' ') else {
return Err(MockFieldSyntax::UnrecognizedTail);
};
if path.is_empty() {
return Err(MockFieldSyntax::UnrecognizedTail);
}
let (op, value_json) = if let Some(value) = comparison.strip_prefix("== ") {
(BodyOp::Equals, value)
} else if let Some(value) = comparison.strip_prefix("!= ") {
(BodyOp::NotEquals, value)
} else {
return Err(MockFieldSyntax::UnrecognizedTail);
};
if serde_json::from_str::<serde_json::Value>(value_json).is_err() {
return Err(MockFieldSyntax::UnrecognizedTail);
}
Ok(MockQuery::BodyPredicate {
key,
path: path.to_string(),
op,
value_json: value_json.to_string(),
})
}
fn parse_body_reference(field: &str) -> Option<(String, String)> {
let bracketed = field.strip_prefix(MOCK_REQUESTS_ROOT)?.strip_prefix('[')?;
let (head, path) = bracketed.split_once(BODY_SEGMENT)?;
if path.is_empty() || path.contains(char::is_whitespace) {
return None;
}
Some((parse_quoted_key(head).ok()?, path.to_string()))
}
pub(crate) fn normalize_body_assertion(assertion: &mut crate::e2e::fixture::Assertion) -> Result<(), String> {
let Some(field) = assertion.field.as_deref() else {
return Ok(());
};
let Some((key, path)) = parse_body_reference(field) else {
return Ok(());
};
let op = match assertion.assertion_type.as_str() {
"equals" => BodyOp::Equals,
"not_equals" => BodyOp::NotEquals,
other => {
return Err(format!(
"assertion type '{other}' on '{field}' is not supported; a request body field takes 'equals' or 'not_equals'"
));
}
};
let Some(value) = assertion.value.as_ref() else {
return Err(format!("assertion on '{field}' needs a 'value'"));
};
let compact = value.to_string();
if value.is_array() || value.is_object() || compact.contains(['\\', '$', '%', '{', '}']) {
return Err(format!(
"value {compact} on '{field}' is not supported; a request body field compares to a number, boolean, null or plain string"
));
}
assertion.field = Some(format!(
"{MOCK_REQUESTS_ROOT}[\"{key}\"].body.{path} {} {compact}",
op.symbol()
));
assertion.assertion_type = "equals".to_string();
assertion.value = Some(serde_json::Value::from(1_u64));
Ok(())
}
pub(crate) fn is_mock_virtual_field(field: &str) -> bool {
parse_mock_field(field).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn total_parses() {
assert_eq!(parse_mock_field("mock.requests.total"), Ok(MockQuery::Total));
}
#[test]
fn method_and_path_key_parses() {
assert_eq!(
parse_mock_field(r#"mock.requests["POST /v1/chat"]"#),
Ok(MockQuery::Count("POST /v1/chat".to_string()))
);
}
#[test]
fn bare_path_key_parses() {
assert_eq!(
parse_mock_field(r#"mock.requests["/v1/chat"]"#),
Ok(MockQuery::Count("/v1/chat".to_string()))
);
}
#[test]
fn round_trips_the_documented_grammar() {
for field in [
"mock.requests.total",
r#"mock.requests["POST /v1/chat"]"#,
r#"mock.requests["/v1/chat"]"#,
] {
assert!(is_mock_virtual_field(field), "expected {field} to parse");
}
}
#[test]
fn rejects_bare_root() {
assert_eq!(parse_mock_field("mock.requests"), Err(MockFieldSyntax::BareRoot));
assert!(!is_mock_virtual_field("mock.requests"));
}
#[test]
fn rejects_unrelated_root() {
assert_eq!(parse_mock_field("mock.foo"), Err(MockFieldSyntax::NotMockField));
assert!(!is_mock_virtual_field("mock.foo"));
}
#[test]
fn rejects_unbalanced_brackets() {
assert_eq!(
parse_mock_field("mock.requests[\"POST /v1/chat\""),
Err(MockFieldSyntax::UnbalancedBracket)
);
assert_eq!(
parse_mock_field("mock.requests["),
Err(MockFieldSyntax::UnbalancedBracket)
);
assert!(!is_mock_virtual_field("mock.requests["));
}
#[test]
fn rejects_an_unquoted_key() {
assert_eq!(
parse_mock_field("mock.requests[chat]"),
Err(MockFieldSyntax::MissingQuotedKey)
);
}
#[test]
fn rejects_an_empty_key() {
assert_eq!(parse_mock_field(r#"mock.requests[""]"#), Err(MockFieldSyntax::EmptyKey));
}
#[test]
fn rejects_an_unrecognized_tail() {
assert_eq!(
parse_mock_field("mock.requests.count"),
Err(MockFieldSyntax::UnrecognizedTail)
);
}
#[test]
fn every_static_field_parses() {
for field in MOCK_VIRTUAL_FIELDS {
assert!(is_mock_virtual_field(field), "{field} is listed but does not parse");
}
}
fn body_assertion(assertion_type: &str, field: &str, value: serde_json::Value) -> crate::e2e::fixture::Assertion {
crate::e2e::fixture::Assertion {
assertion_type: assertion_type.to_string(),
field: Some(field.to_string()),
value: Some(value),
..Default::default()
}
}
#[test]
fn body_predicate_parses_the_key_path_operator_and_value() {
assert_eq!(
parse_mock_field(r#"mock.requests["POST /api/chat"].body.options.num_ctx == 8192"#),
Ok(MockQuery::BodyPredicate {
key: "POST /api/chat".to_string(),
path: "options.num_ctx".to_string(),
op: BodyOp::Equals,
value_json: "8192".to_string(),
})
);
assert_eq!(
parse_mock_field(r#"mock.requests["/api/chat"].body.model != "a == b""#),
Ok(MockQuery::BodyPredicate {
key: "/api/chat".to_string(),
path: "model".to_string(),
op: BodyOp::NotEquals,
value_json: r#""a == b""#.to_string(),
}),
"the first space ends the path, so ` == ` inside the JSON value stays in the value"
);
}
#[test]
fn body_predicate_rejects_a_missing_operator_or_invalid_json() {
for field in [
r#"mock.requests["POST /a"].body.x"#,
r#"mock.requests["POST /a"].body.x 8192"#,
r#"mock.requests["POST /a"].body.x == nope"#,
r#"mock.requests["POST /a"].body. == 1"#,
] {
assert!(!is_mock_virtual_field(field), "{field} must not parse");
}
}
#[test]
fn normalize_rewrites_an_equals_body_assertion_into_the_predicate() {
let mut assertion = body_assertion(
"equals",
r#"mock.requests["POST /api/chat"].body.options.num_ctx"#,
serde_json::json!(8192),
);
normalize_body_assertion(&mut assertion).expect("equals normalizes");
assert_eq!(
assertion.field.as_deref(),
Some(r#"mock.requests["POST /api/chat"].body.options.num_ctx == 8192"#)
);
assert_eq!(assertion.assertion_type, "equals");
assert_eq!(assertion.value, Some(serde_json::json!(1)));
assert!(is_mock_virtual_field(assertion.field.as_deref().unwrap()));
}
#[test]
fn normalize_rewrites_not_equals_with_a_string_value() {
let mut assertion = body_assertion(
"not_equals",
r#"mock.requests["/api/chat"].body.model"#,
serde_json::json!("llama"),
);
normalize_body_assertion(&mut assertion).expect("not_equals normalizes");
assert_eq!(
assertion.field.as_deref(),
Some(r#"mock.requests["/api/chat"].body.model != "llama""#)
);
}
#[test]
fn normalize_leaves_every_other_field_alone() {
for field in ["mock.requests.total", r#"mock.requests["POST /a"]"#, "choices.0.text"] {
let mut assertion = body_assertion("equals", field, serde_json::json!(1));
normalize_body_assertion(&mut assertion).expect("not a body field");
assert_eq!(assertion.field.as_deref(), Some(field));
assert_eq!(assertion.value, Some(serde_json::json!(1)));
}
}
#[test]
fn normalize_refuses_an_assertion_type_a_body_field_cannot_take() {
let mut assertion = body_assertion("contains", r#"mock.requests["POST /a"].body.x"#, serde_json::json!("v"));
let error = normalize_body_assertion(&mut assertion).expect_err("contains is refused");
assert!(error.contains("'contains'"), "{error}");
}
#[test]
fn normalize_refuses_a_value_the_generators_cannot_embed_safely() {
for value in [
serde_json::json!({"a": 1}),
serde_json::json!([1]),
serde_json::json!("a\"b"),
serde_json::json!("100%"),
serde_json::json!("${x}"),
] {
let mut assertion = body_assertion("equals", r#"mock.requests["POST /a"].body.x"#, value.clone());
assert!(
normalize_body_assertion(&mut assertion).is_err(),
"{value} must be refused"
);
}
}
}