Skip to main content

mockserver_client/
model.rs

1//! Domain model types for the MockServer control-plane API.
2//!
3//! All types implement `Serialize`/`Deserialize` and use builder methods that
4//! take `self` and return `Self`, enabling fluent construction.
5
6use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Free-form map used as a forward-compatibility safety net on the wire types
11/// that model MockServer actions. Any JSON field the typed model does not yet
12/// name is captured here (via `#[serde(flatten)]`) so it survives a
13/// deserialize-then-serialize round-trip instead of being silently dropped.
14///
15/// An empty map contributes no keys when serialized, so a `flatten`ed `Extra`
16/// is invisible on the wire unless the server actually sent unknown fields.
17pub type Extra = serde_json::Map<String, serde_json::Value>;
18
19/// Deserialize a MockServer `oneOf: [ <T>, [ <T> ] ]` field (a single object or
20/// an array of objects) into a `Vec<T>`. Used by `beforeActions`, `afterActions`
21/// and `capture`, which the server accepts in either shape.
22fn one_or_many<'de, D, T>(deserializer: D) -> std::result::Result<Option<Vec<T>>, D::Error>
23where
24    D: serde::Deserializer<'de>,
25    T: Deserialize<'de>,
26{
27    #[derive(Deserialize)]
28    #[serde(untagged)]
29    enum OneOrMany<T> {
30        One(T),
31        Many(Vec<T>),
32    }
33    let opt = Option::<OneOrMany<T>>::deserialize(deserializer)?;
34    Ok(opt.map(|v| match v {
35        OneOrMany::One(t) => vec![t],
36        OneOrMany::Many(v) => v,
37    }))
38}
39
40// ---------------------------------------------------------------------------
41// ParameterValues
42// ---------------------------------------------------------------------------
43
44/// The value of a single key in a MockServer keyToMultiValue matcher (path
45/// parameters, and in general query-string parameters / headers).
46///
47/// MockServer accepts two wire encodings for a key's value:
48///   * the **plain** form — a list of exact-or-regex strings (`["42", "^\\d+$"]`),
49///     modelled by [`Values`](Self::Values); and
50///   * the **schema-matcher** form — a list of matcher objects
51///     (`[{ "schema": { … } }]`, `[{ "not": true, "value": "x" }]`), or the
52///     `{ "parameterStyle": …, "values": [ … ] }` object form — captured verbatim
53///     by [`Matcher`](Self::Matcher) so no field is dropped on a round-trip.
54///
55/// The enum is `#[serde(untagged)]`: the plain string-array form deserialises to
56/// [`Values`](Self::Values); anything else (a matcher-object array, an object, or
57/// even a bare string) falls through to [`Matcher`](Self::Matcher).
58#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
59#[serde(untagged)]
60pub enum ParameterValues {
61    /// Plain multi-value form: a list of exact-or-regex string values.
62    Values(Vec<String>),
63    /// Schema / nottable / optional / parameter-style matcher form, kept verbatim.
64    Matcher(serde_json::Value),
65}
66
67impl ParameterValues {
68    /// Borrow the plain string values, if this is the [`Values`](Self::Values) form.
69    pub fn as_values(&self) -> Option<&[String]> {
70        match self {
71            ParameterValues::Values(v) => Some(v),
72            ParameterValues::Matcher(_) => None,
73        }
74    }
75}
76
77impl From<Vec<String>> for ParameterValues {
78    fn from(values: Vec<String>) -> Self {
79        ParameterValues::Values(values)
80    }
81}
82
83// ---------------------------------------------------------------------------
84// MatcherValue
85// ---------------------------------------------------------------------------
86
87/// The markers MockServer strips from the front of a plain matcher string:
88/// `!` negates the matcher and `?` makes it optional.
89const NOT_CHAR: char = '!';
90const OPTIONAL_CHAR: char = '?';
91
92/// A single matcher value for a request header, query-string parameter, cookie
93/// or path parameter.
94///
95/// MockServer's plain-string wire form encodes negation and optionality as
96/// leading markers — `!` negates and `?` marks optional — and the server strips
97/// them unconditionally when reading. A value whose own first character is `!`
98/// or `?` therefore cannot be sent as a bare string:
99/// [`header("X-Tag", "!foo")`](HttpRequest::header) is read back as "`X-Tag` is
100/// anything but `foo`", which matches almost every request, so the expectation
101/// silently passes for the wrong reason instead of failing.
102///
103/// `MatcherValue` keeps the value and the flags apart and serialises to the
104/// object form (`{"not":false,"value":"!foo"}`) only when the plain form would
105/// be misread. Every value that already round-trips through the plain form stays
106/// byte-identical on the wire, so existing expectations are unaffected.
107///
108/// Use [`literal`](Self::literal) for an exact value, [`not_literal`](Self::not_literal)
109/// to negate one, and [`optional_literal`](Self::optional_literal) for an
110/// optional exact value:
111///
112/// ```
113/// use mockserver_client::{HttpRequest, MatcherValue};
114///
115/// let request = HttpRequest::new()
116///     .header_matcher("X-Tag", MatcherValue::literal("!foo"))   // X-Tag IS "!foo"
117///     .header_matcher("X-Env", MatcherValue::not_literal("dev")); // X-Env is NOT "dev"
118/// ```
119#[derive(Debug, Clone, PartialEq, Eq, Default)]
120pub struct MatcherValue {
121    /// The matcher text, taken verbatim — markers in it are not parsed.
122    pub value: String,
123    /// Negate the matcher (`"not": true`).
124    pub not: bool,
125    /// Mark the header/parameter/cookie as optional (`"optional": true`).
126    pub optional: bool,
127}
128
129/// Mirrors Java's `StringUtils.isBlank`: empty or whitespace only.
130fn is_blank(s: &str) -> bool {
131    s.trim().is_empty()
132}
133
134impl MatcherValue {
135    /// A matcher for exactly `value`, even when it starts with `!` or `?`.
136    pub fn literal(value: impl Into<String>) -> Self {
137        Self {
138            value: value.into(),
139            not: false,
140            optional: false,
141        }
142    }
143
144    /// A matcher that matches anything except exactly `value`.
145    pub fn not_literal(value: impl Into<String>) -> Self {
146        Self {
147            value: value.into(),
148            not: true,
149            optional: false,
150        }
151    }
152
153    /// A matcher for exactly `value` that need not be present.
154    pub fn optional_literal(value: impl Into<String>) -> Self {
155        Self {
156            value: value.into(),
157            not: false,
158            optional: true,
159        }
160    }
161
162    /// Render the plain-string form, matching `NottableString.serialise()`.
163    fn serialise(&self) -> String {
164        let mut s = String::new();
165        if self.optional {
166            s.push(OPTIONAL_CHAR);
167        }
168        if self.not {
169            s.push(NOT_CHAR);
170        }
171        if !is_blank(&self.value) {
172            s.push_str(&self.value);
173        }
174        s
175    }
176
177    /// Parse a plain wire string exactly as the server would, mirroring
178    /// `NottableString.string(String)`: strip an optional marker, then a not
179    /// marker, then an optional marker again.
180    fn parse_plain(s: &str) -> Self {
181        let mut optional = false;
182        let mut not = false;
183        let mut rest = s;
184        if !is_blank(s) {
185            if let Some(r) = rest.strip_prefix(OPTIONAL_CHAR) {
186                optional = true;
187                rest = r;
188            }
189            if let Some(r) = rest.strip_prefix(NOT_CHAR) {
190                not = true;
191                rest = r;
192            }
193            if let Some(r) = rest.strip_prefix(OPTIONAL_CHAR) {
194                optional = true;
195                rest = r;
196            }
197        }
198        Self {
199            value: rest.to_string(),
200            not,
201            optional,
202        }
203    }
204
205    /// Whether re-reading the plain form would change the value, the negation or
206    /// the optionality. Decided by actually re-parsing rather than by testing
207    /// for a leading marker, so it stays correct for compound markers (`?!`,
208    /// `!?`) and any marker added later.
209    fn ambiguous(&self) -> bool {
210        if is_blank(&self.value) {
211            // A blank value has no marker to misread, and it cannot be expressed
212            // in the object form either: the server's object-form reader ignores
213            // a blank "value", so escaping one would silently drop the matcher.
214            return false;
215        }
216        let reparsed = Self::parse_plain(&self.serialise());
217        reparsed.not != self.not
218            || reparsed.optional != self.optional
219            || reparsed.value != self.value
220    }
221}
222
223impl From<String> for MatcherValue {
224    /// Parse a plain wire string, so an existing `"!foo"` keeps meaning "not
225    /// foo". The meaning is preserved; only the representation changes.
226    fn from(s: String) -> Self {
227        Self::parse_plain(&s)
228    }
229}
230
231impl From<&str> for MatcherValue {
232    fn from(s: &str) -> Self {
233        Self::parse_plain(s)
234    }
235}
236
237impl std::fmt::Display for MatcherValue {
238    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
239        f.write_str(&self.serialise())
240    }
241}
242
243impl Serialize for MatcherValue {
244    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
245    where
246        S: serde::Serializer,
247    {
248        if !self.ambiguous() {
249            return serializer.serialize_str(&self.serialise());
250        }
251        use serde::ser::SerializeMap;
252        // "not" is written even when false so the intent is unmistakable to a
253        // reader and to the other clients, rather than relying on absent==false.
254        let count = if self.optional { 3 } else { 2 };
255        let mut map = serializer.serialize_map(Some(count))?;
256        map.serialize_entry("not", &self.not)?;
257        if self.optional {
258            map.serialize_entry("optional", &self.optional)?;
259        }
260        map.serialize_entry("value", &self.value)?;
261        map.end()
262    }
263}
264
265impl<'de> Deserialize<'de> for MatcherValue {
266    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
267    where
268        D: serde::Deserializer<'de>,
269    {
270        // A hand-written visitor rather than an `#[serde(untagged)]` enum: an
271        // untagged enum re-buffers into `Content`, which does not compose with
272        // the `Content` buffering that `#[serde(flatten)]` already applies to
273        // the enclosing `HttpRequest`, so the object form is misread as a string
274        // inside a flattened struct. `visit_str`/`visit_map` dispatch directly
275        // and work in both contexts.
276        struct MatcherValueVisitor;
277
278        impl<'de> serde::de::Visitor<'de> for MatcherValueVisitor {
279            type Value = MatcherValue;
280
281            fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
282                f.write_str("a matcher string or an object with a \"value\" field")
283            }
284
285            fn visit_str<E>(self, v: &str) -> std::result::Result<MatcherValue, E>
286            where
287                E: serde::de::Error,
288            {
289                Ok(MatcherValue::parse_plain(v))
290            }
291
292            fn visit_map<A>(self, mut map: A) -> std::result::Result<MatcherValue, A::Error>
293            where
294                A: serde::de::MapAccess<'de>,
295            {
296                let mut value: Option<String> = None;
297                let mut not = false;
298                let mut optional = false;
299                while let Some(key) = map.next_key::<String>()? {
300                    match key.as_str() {
301                        "value" => value = Some(map.next_value()?),
302                        "not" => not = map.next_value()?,
303                        "optional" => optional = map.next_value()?,
304                        // Ignore any other key so the object form stays
305                        // forward-compatible with fields added later.
306                        _ => {
307                            let _ = map.next_value::<serde::de::IgnoredAny>()?;
308                        }
309                    }
310                }
311                let value = value.ok_or_else(|| serde::de::Error::missing_field("value"))?;
312                Ok(MatcherValue {
313                    value,
314                    not,
315                    optional,
316                })
317            }
318        }
319
320        deserializer.deserialize_any(MatcherValueVisitor)
321    }
322}
323
324/// Lenient deserializer for `path` / `method`, which this model represents as
325/// plain strings. When an expectation carries their object (nottable) form —
326/// which the plain-string field cannot hold — the object is discarded rather
327/// than failing the whole expectation, so the rest of the request (headers,
328/// query parameters, the enclosing response, …) still decodes. The dropped
329/// field is recorded in the round-trip fidelity harness's known-gaps ledger as
330/// `httpRequest.path` / `httpRequest.method`.
331fn de_lenient_optional_string<'de, D>(
332    deserializer: D,
333) -> std::result::Result<Option<String>, D::Error>
334where
335    D: serde::Deserializer<'de>,
336{
337    struct LenientString;
338
339    impl<'de> serde::de::Visitor<'de> for LenientString {
340        type Value = Option<String>;
341
342        fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
343            f.write_str("a string, or an object matcher form that is discarded")
344        }
345
346        fn visit_str<E>(self, v: &str) -> std::result::Result<Option<String>, E>
347        where
348            E: serde::de::Error,
349        {
350            Ok(Some(v.to_owned()))
351        }
352
353        fn visit_none<E>(self) -> std::result::Result<Option<String>, E>
354        where
355            E: serde::de::Error,
356        {
357            Ok(None)
358        }
359
360        fn visit_unit<E>(self) -> std::result::Result<Option<String>, E>
361        where
362            E: serde::de::Error,
363        {
364            Ok(None)
365        }
366
367        fn visit_some<D>(self, deserializer: D) -> std::result::Result<Option<String>, D::Error>
368        where
369            D: serde::Deserializer<'de>,
370        {
371            deserializer.deserialize_any(self)
372        }
373
374        fn visit_map<A>(self, mut map: A) -> std::result::Result<Option<String>, A::Error>
375        where
376            A: serde::de::MapAccess<'de>,
377        {
378            // The object (nottable) form cannot be represented as a plain string;
379            // drain it and discard so the surrounding decode still succeeds.
380            while map
381                .next_entry::<serde::de::IgnoredAny, serde::de::IgnoredAny>()?
382                .is_some()
383            {}
384            Ok(None)
385        }
386    }
387
388    deserializer.deserialize_any(LenientString)
389}
390
391// ---------------------------------------------------------------------------
392// HttpRequest
393// ---------------------------------------------------------------------------
394
395/// Matcher for an HTTP request. Uses builder methods for fluent construction.
396///
397/// # Example
398/// ```
399/// use mockserver_client::HttpRequest;
400///
401/// let request = HttpRequest::new()
402///     .method("POST")
403///     .path("/api/users")
404///     .header("Content-Type", "application/json")
405///     .query_param("page", "1")
406///     .body("{}");
407/// ```
408/// `Serialize`/`Deserialize` are implemented by hand (see below) rather than
409/// derived, so the additive `*_matchers` fields can stand in for their plain
410/// counterparts under the same wire key — the same shape the Go client uses.
411#[derive(Debug, Clone, Default, PartialEq)]
412pub struct HttpRequest {
413    pub method: Option<String>,
414
415    pub path: Option<String>,
416
417    /// Query-string parameters to match, multiple values per key (plain form).
418    pub query_string_parameters: Option<HashMap<String, Vec<String>>>,
419
420    /// Headers to match, multiple values per key (plain form).
421    pub headers: Option<HashMap<String, Vec<String>>>,
422
423    pub body: Option<Body>,
424
425    pub jwt: Option<Jwt>,
426
427    pub socket_address: Option<SocketAddress>,
428
429    /// Negate the whole request matcher (`"not": true`).
430    pub not: Option<bool>,
431
432    /// Match only requests received over TLS (`"secure"`).
433    pub secure: Option<bool>,
434
435    /// Match only keep-alive requests (`"keepAlive"`).
436    pub keep_alive: Option<bool>,
437
438    /// Match the request protocol (e.g. `"HTTP_1_1"`, `"HTTP_2"`, `"HTTP_3"`).
439    pub protocol: Option<String>,
440
441    /// Path parameters (`/users/{id}` style), multiple values per key.
442    ///
443    /// Each key's value is a [`ParameterValues`], accepting both the plain
444    /// string-list form and the schema/nottable matcher form.
445    pub path_parameters: Option<HashMap<String, ParameterValues>>,
446
447    /// Cookies to match (single value per name, plain form).
448    pub cookies: Option<HashMap<String, String>>,
449
450    /// Header matchers whose values are taken verbatim, expressing a value that
451    /// the plain [`headers`](Self::headers) map cannot: one starting with `!` or
452    /// `?`. When non-empty this REPLACES [`headers`](Self::headers) on the wire
453    /// under the same `headers` key. Additive — the plain map keeps its type and
454    /// meaning. Populate via [`header_matcher`](Self::header_matcher).
455    pub header_matchers: Option<HashMap<String, Vec<MatcherValue>>>,
456
457    /// Query-string parameter matchers. See [`header_matchers`](Self::header_matchers)
458    /// and [`query_param_matcher`](Self::query_param_matcher).
459    pub query_string_parameter_matchers: Option<HashMap<String, Vec<MatcherValue>>>,
460
461    /// Cookie matchers (single value per name). See
462    /// [`header_matchers`](Self::header_matchers) and
463    /// [`cookie_matcher`](Self::cookie_matcher).
464    pub cookie_matchers: Option<HashMap<String, MatcherValue>>,
465
466    /// Forward-compatibility catch-all for request fields the typed model does
467    /// not yet name (e.g. `clientCertificate`, `localAddress`, `remoteAddress`).
468    pub extra: Extra,
469}
470
471/// Convert a plain multi-value map to matcher values by parsing each string
472/// exactly as the server would, so an existing `"!foo"` keeps meaning "not foo".
473/// The meaning is preserved; only the representation changes.
474fn plain_multi_as_matchers(
475    map: &HashMap<String, Vec<String>>,
476) -> HashMap<String, Vec<MatcherValue>> {
477    map.iter()
478        .map(|(k, vs)| {
479            (
480                k.clone(),
481                vs.iter().map(|v| MatcherValue::from(v.clone())).collect(),
482            )
483        })
484        .collect()
485}
486
487/// The effective multi-value map for the wire: the matcher map when it carries
488/// anything, otherwise the plain map re-expressed as matcher values (which
489/// serialise back to the identical plain strings).
490fn effective_multi(
491    plain: &Option<HashMap<String, Vec<String>>>,
492    matchers: &Option<HashMap<String, Vec<MatcherValue>>>,
493) -> Option<HashMap<String, Vec<MatcherValue>>> {
494    match matchers {
495        Some(m) if !m.is_empty() => Some(m.clone()),
496        _ => plain.as_ref().map(plain_multi_as_matchers),
497    }
498}
499
500/// The effective single-value (cookie) map for the wire. See [`effective_multi`].
501fn effective_single(
502    plain: &Option<HashMap<String, String>>,
503    matchers: &Option<HashMap<String, MatcherValue>>,
504) -> Option<HashMap<String, MatcherValue>> {
505    match matchers {
506        Some(m) if !m.is_empty() => Some(m.clone()),
507        _ => plain.as_ref().map(|p| {
508            p.iter()
509                .map(|(k, v)| (k.clone(), MatcherValue::from(v.clone())))
510                .collect()
511        }),
512    }
513}
514
515/// A decoded multi-value field split into (plain map, matcher map) — at most one
516/// is `Some`.
517type SplitMulti = (
518    Option<HashMap<String, Vec<String>>>,
519    Option<HashMap<String, Vec<MatcherValue>>>,
520);
521
522/// A decoded single-value (cookie) field split into (plain map, matcher map).
523type SplitSingle = (
524    Option<HashMap<String, String>>,
525    Option<HashMap<String, MatcherValue>>,
526);
527
528/// Split a decoded multi-value matcher map into the plain map (when every value
529/// round-trips through the plain form) or the matcher map (when any value needs
530/// the object form). Mirrors the Go client's `decodeMultiValueMatchers`: the
531/// choice is per-FIELD, not per-value, because both share one wire key.
532fn split_multi(decoded: Option<HashMap<String, Vec<MatcherValue>>>) -> SplitMulti {
533    let Some(map) = decoded else {
534        return (None, None);
535    };
536    if map.values().flatten().any(MatcherValue::ambiguous) {
537        return (None, Some(map));
538    }
539    let plain = map
540        .into_iter()
541        .map(|(k, vs)| (k, vs.iter().map(MatcherValue::serialise).collect()))
542        .collect();
543    (Some(plain), None)
544}
545
546/// [`split_multi`] for a single-value (cookie) field.
547fn split_single(decoded: Option<HashMap<String, MatcherValue>>) -> SplitSingle {
548    let Some(map) = decoded else {
549        return (None, None);
550    };
551    if map.values().any(MatcherValue::ambiguous) {
552        return (None, Some(map));
553    }
554    let plain = map.into_iter().map(|(k, v)| (k, v.serialise())).collect();
555    (Some(plain), None)
556}
557
558impl Serialize for HttpRequest {
559    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
560    where
561        S: serde::Serializer,
562    {
563        #[derive(Serialize)]
564        #[serde(rename_all = "camelCase")]
565        struct Wire<'a> {
566            #[serde(skip_serializing_if = "Option::is_none")]
567            method: &'a Option<String>,
568            #[serde(skip_serializing_if = "Option::is_none")]
569            path: &'a Option<String>,
570            #[serde(skip_serializing_if = "Option::is_none")]
571            query_string_parameters: Option<HashMap<String, Vec<MatcherValue>>>,
572            #[serde(skip_serializing_if = "Option::is_none")]
573            headers: Option<HashMap<String, Vec<MatcherValue>>>,
574            #[serde(skip_serializing_if = "Option::is_none")]
575            body: &'a Option<Body>,
576            #[serde(skip_serializing_if = "Option::is_none")]
577            jwt: &'a Option<Jwt>,
578            #[serde(skip_serializing_if = "Option::is_none")]
579            socket_address: &'a Option<SocketAddress>,
580            #[serde(skip_serializing_if = "Option::is_none")]
581            not: &'a Option<bool>,
582            #[serde(skip_serializing_if = "Option::is_none")]
583            secure: &'a Option<bool>,
584            #[serde(skip_serializing_if = "Option::is_none")]
585            keep_alive: &'a Option<bool>,
586            #[serde(skip_serializing_if = "Option::is_none")]
587            protocol: &'a Option<String>,
588            #[serde(skip_serializing_if = "Option::is_none")]
589            path_parameters: &'a Option<HashMap<String, ParameterValues>>,
590            #[serde(skip_serializing_if = "Option::is_none")]
591            cookies: Option<HashMap<String, MatcherValue>>,
592            #[serde(flatten)]
593            extra: &'a Extra,
594        }
595
596        Wire {
597            method: &self.method,
598            path: &self.path,
599            query_string_parameters: effective_multi(
600                &self.query_string_parameters,
601                &self.query_string_parameter_matchers,
602            ),
603            headers: effective_multi(&self.headers, &self.header_matchers),
604            body: &self.body,
605            jwt: &self.jwt,
606            socket_address: &self.socket_address,
607            not: &self.not,
608            secure: &self.secure,
609            keep_alive: &self.keep_alive,
610            protocol: &self.protocol,
611            path_parameters: &self.path_parameters,
612            cookies: effective_single(&self.cookies, &self.cookie_matchers),
613            extra: &self.extra,
614        }
615        .serialize(serializer)
616    }
617}
618
619impl<'de> Deserialize<'de> for HttpRequest {
620    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
621    where
622        D: serde::Deserializer<'de>,
623    {
624        #[derive(Deserialize)]
625        #[serde(rename_all = "camelCase")]
626        struct Wire {
627            #[serde(default, deserialize_with = "de_lenient_optional_string")]
628            method: Option<String>,
629            #[serde(default, deserialize_with = "de_lenient_optional_string")]
630            path: Option<String>,
631            #[serde(default)]
632            query_string_parameters: Option<HashMap<String, Vec<MatcherValue>>>,
633            #[serde(default)]
634            headers: Option<HashMap<String, Vec<MatcherValue>>>,
635            #[serde(default)]
636            body: Option<Body>,
637            #[serde(default)]
638            jwt: Option<Jwt>,
639            #[serde(default)]
640            socket_address: Option<SocketAddress>,
641            #[serde(default)]
642            not: Option<bool>,
643            #[serde(default)]
644            secure: Option<bool>,
645            #[serde(default)]
646            keep_alive: Option<bool>,
647            #[serde(default)]
648            protocol: Option<String>,
649            #[serde(default)]
650            path_parameters: Option<HashMap<String, ParameterValues>>,
651            #[serde(default)]
652            cookies: Option<HashMap<String, MatcherValue>>,
653            #[serde(flatten)]
654            extra: Extra,
655        }
656
657        let wire = Wire::deserialize(deserializer)?;
658        let (headers, header_matchers) = split_multi(wire.headers);
659        let (query_string_parameters, query_string_parameter_matchers) =
660            split_multi(wire.query_string_parameters);
661        let (cookies, cookie_matchers) = split_single(wire.cookies);
662        Ok(HttpRequest {
663            method: wire.method,
664            path: wire.path,
665            query_string_parameters,
666            headers,
667            body: wire.body,
668            jwt: wire.jwt,
669            socket_address: wire.socket_address,
670            not: wire.not,
671            secure: wire.secure,
672            keep_alive: wire.keep_alive,
673            protocol: wire.protocol,
674            path_parameters: wire.path_parameters,
675            cookies,
676            header_matchers,
677            query_string_parameter_matchers,
678            cookie_matchers,
679            extra: wire.extra,
680        })
681    }
682}
683
684impl HttpRequest {
685    /// Create a new empty request matcher.
686    pub fn new() -> Self {
687        Self::default()
688    }
689
690    /// Set the downstream socket address to connect to.
691    ///
692    /// Used by load-scenario steps (and forwarded/proxied requests) to direct
693    /// the rendered request at a specific host/port/scheme rather than relying
694    /// on the request's `Host` header.
695    pub fn socket_address(mut self, socket_address: SocketAddress) -> Self {
696        self.socket_address = Some(socket_address);
697        self
698    }
699
700    /// Set the HTTP method to match.
701    pub fn method(mut self, method: impl Into<String>) -> Self {
702        self.method = Some(method.into());
703        self
704    }
705
706    /// Set the path to match.
707    pub fn path(mut self, path: impl Into<String>) -> Self {
708        self.path = Some(path.into());
709        self
710    }
711
712    /// Add a query string parameter (multiple values per key supported).
713    ///
714    /// A leading `!` in the value negates and a leading `?` marks it optional —
715    /// the server's marker syntax, and the existing meaning. To match a value
716    /// that literally starts with `!` or `?`, use
717    /// [`query_param_matcher`](Self::query_param_matcher) with
718    /// [`MatcherValue::literal`].
719    pub fn query_param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
720        let params = self
721            .query_string_parameters
722            .get_or_insert_with(HashMap::new);
723        params.entry(key.into()).or_default().push(value.into());
724        self
725    }
726
727    /// Add a query string parameter matcher whose value is taken verbatim, so a
728    /// value starting with `!` or `?` means itself rather than being read as a
729    /// negation or optionality marker.
730    ///
731    /// Use one style per field: once any `query_param_matcher` call is made, the
732    /// matcher map replaces the plain [`query_param`](Self::query_param) map for
733    /// the whole `queryStringParameters` field on the wire, so a plain
734    /// `query_param` call made AFTER this one is ignored.
735    pub fn query_param_matcher(mut self, key: impl Into<String>, value: MatcherValue) -> Self {
736        if self.query_string_parameter_matchers.is_none() {
737            let migrated = self
738                .query_string_parameters
739                .take()
740                .map(|m| plain_multi_as_matchers(&m))
741                .unwrap_or_default();
742            self.query_string_parameter_matchers = Some(migrated);
743        }
744        self.query_string_parameter_matchers
745            .as_mut()
746            .unwrap()
747            .entry(key.into())
748            .or_default()
749            .push(value);
750        self
751    }
752
753    /// Add a header (multiple values per key supported).
754    ///
755    /// A leading `!` in the value negates and a leading `?` marks it optional —
756    /// the server's marker syntax, and the existing meaning. To match a value
757    /// that literally starts with `!` or `?`, use
758    /// [`header_matcher`](Self::header_matcher) with [`MatcherValue::literal`].
759    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
760        let headers = self.headers.get_or_insert_with(HashMap::new);
761        headers.entry(key.into()).or_default().push(value.into());
762        self
763    }
764
765    /// Add a header matcher whose value is taken verbatim, so a value starting
766    /// with `!` or `?` means itself rather than being read as a negation or
767    /// optionality marker.
768    ///
769    /// Use one style per field: once any `header_matcher` call is made, the
770    /// matcher map replaces the plain [`header`](Self::header) map for the whole
771    /// `headers` field on the wire, so a plain `header` call made AFTER this one
772    /// is ignored.
773    pub fn header_matcher(mut self, key: impl Into<String>, value: MatcherValue) -> Self {
774        if self.header_matchers.is_none() {
775            let migrated = self
776                .headers
777                .take()
778                .map(|m| plain_multi_as_matchers(&m))
779                .unwrap_or_default();
780            self.header_matchers = Some(migrated);
781        }
782        self.header_matchers
783            .as_mut()
784            .unwrap()
785            .entry(key.into())
786            .or_default()
787            .push(value);
788        self
789    }
790
791    /// Set a plain string body matcher.
792    pub fn body(mut self, body: impl Into<String>) -> Self {
793        self.body = Some(Body::Plain(body.into()));
794        self
795    }
796
797    /// Set a typed JSON body matcher.
798    pub fn json_body(mut self, json: serde_json::Value) -> Self {
799        self.body = Some(Body::Typed {
800            body_type: "JSON".to_string(),
801            json: json.to_string(),
802        });
803        self
804    }
805
806    /// Set a file body (type "FILE") with optional content type and template type.
807    ///
808    /// Use [`Body::file`] for richer construction if you need content type or
809    /// template type set.
810    pub fn file_body(mut self, file_path: impl Into<String>) -> Self {
811        self.body = Some(Body::File {
812            file_path: file_path.into(),
813            content_type: None,
814            template_type: None,
815        });
816        self
817    }
818
819    /// Set a pre-built [`Body`] value (use with [`Body::file`] for FILE bodies).
820    pub fn body_value(mut self, body: Body) -> Self {
821        self.body = Some(body);
822        self
823    }
824
825    /// Set a JWT request matcher.
826    ///
827    /// Serialised under the `"jwt"` key alongside `method`/`path`/`headers`.
828    ///
829    /// # Example
830    /// ```
831    /// use mockserver_client::{HttpRequest, Jwt};
832    ///
833    /// let request = HttpRequest::new()
834    ///     .method("GET")
835    ///     .path("/secure")
836    ///     .jwt(
837    ///         Jwt::new()
838    ///             .claim("sub", "user-123")
839    ///             .claim("role", "!admin")
840    ///             .issuer("https://issuer.example.com")
841    ///             .algorithm("RS256"),
842    ///     );
843    /// ```
844    pub fn jwt(mut self, jwt: Jwt) -> Self {
845        self.jwt = Some(jwt);
846        self
847    }
848
849    /// Negate the whole request matcher.
850    pub fn not(mut self, not: bool) -> Self {
851        self.not = Some(not);
852        self
853    }
854
855    /// Match only requests received over TLS.
856    pub fn secure(mut self, secure: bool) -> Self {
857        self.secure = Some(secure);
858        self
859    }
860
861    /// Match only keep-alive requests.
862    pub fn keep_alive(mut self, keep_alive: bool) -> Self {
863        self.keep_alive = Some(keep_alive);
864        self
865    }
866
867    /// Match the request protocol (e.g. `"HTTP_1_1"`, `"HTTP_2"`).
868    pub fn protocol(mut self, protocol: impl Into<String>) -> Self {
869        self.protocol = Some(protocol.into());
870        self
871    }
872
873    /// Add a plain path parameter value (multiple values per key supported).
874    ///
875    /// For schema/nottable matcher forms, set [`path_parameters`](Self::path_parameters)
876    /// directly with a [`ParameterValues::Matcher`].
877    pub fn path_param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
878        let params = self.path_parameters.get_or_insert_with(HashMap::new);
879        match params
880            .entry(key.into())
881            .or_insert_with(|| ParameterValues::Values(Vec::new()))
882        {
883            ParameterValues::Values(v) => v.push(value.into()),
884            ParameterValues::Matcher(_) => {
885                // Existing entry is a verbatim matcher form; leave it untouched
886                // rather than silently coercing it to a plain value.
887            }
888        }
889        self
890    }
891
892    /// Add a cookie to match (single value per name).
893    ///
894    /// A leading `!` in the value negates and a leading `?` marks it optional —
895    /// the server's marker syntax, and the existing meaning. To match a value
896    /// that literally starts with `!` or `?`, use
897    /// [`cookie_matcher`](Self::cookie_matcher) with [`MatcherValue::literal`].
898    pub fn cookie(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
899        let cookies = self.cookies.get_or_insert_with(HashMap::new);
900        cookies.insert(name.into(), value.into());
901        self
902    }
903
904    /// Add a cookie matcher whose value is taken verbatim, so a value starting
905    /// with `!` or `?` means itself rather than being read as a negation or
906    /// optionality marker.
907    ///
908    /// Use one style per field: once any `cookie_matcher` call is made, the
909    /// matcher map replaces the plain [`cookie`](Self::cookie) map for the whole
910    /// `cookies` field on the wire, so a plain `cookie` call made AFTER this one
911    /// is ignored.
912    pub fn cookie_matcher(mut self, name: impl Into<String>, value: MatcherValue) -> Self {
913        if self.cookie_matchers.is_none() {
914            let migrated = self
915                .cookies
916                .take()
917                .map(|m| {
918                    m.into_iter()
919                        .map(|(k, v)| (k, MatcherValue::from(v)))
920                        .collect()
921                })
922                .unwrap_or_default();
923            self.cookie_matchers = Some(migrated);
924        }
925        self.cookie_matchers
926            .as_mut()
927            .unwrap()
928            .insert(name.into(), value);
929        self
930    }
931
932    /// Add a path parameter matcher whose value is taken verbatim, so a value
933    /// starting with `!` or `?` means itself rather than being read as a
934    /// negation or optionality marker.
935    ///
936    /// Path parameters are carried as [`ParameterValues`]; a literal value is
937    /// stored in the schema-matcher (object) form so the server reads it exactly
938    /// as written. Calling this for a key that already holds plain
939    /// [`path_param`](Self::path_param) values REPLACES them with the matcher
940    /// form, so use one style per key.
941    pub fn path_param_matcher(mut self, key: impl Into<String>, value: MatcherValue) -> Self {
942        let element = serde_json::to_value(&value)
943            .expect("BUG: MatcherValue::serialize returned Err for an in-memory value");
944        let params = self.path_parameters.get_or_insert_with(HashMap::new);
945        match params
946            .entry(key.into())
947            .or_insert_with(|| ParameterValues::Matcher(serde_json::Value::Array(Vec::new())))
948        {
949            ParameterValues::Matcher(serde_json::Value::Array(values)) => values.push(element),
950            slot => {
951                *slot = ParameterValues::Matcher(serde_json::Value::Array(vec![element]));
952            }
953        }
954        self
955    }
956}
957
958// ---------------------------------------------------------------------------
959// Body
960// ---------------------------------------------------------------------------
961
962/// Request/response body — either a plain string, a typed object, or a file reference.
963#[derive(Debug, Clone, PartialEq)]
964pub enum Body {
965    /// A plain string body.
966    Plain(String),
967    /// A typed body (e.g., JSON).
968    Typed { body_type: String, json: String },
969    /// A file body (`type: "FILE"`), with optional template evaluation.
970    File {
971        file_path: String,
972        content_type: Option<String>,
973        template_type: Option<String>,
974    },
975    /// An `ALL_OF` composite body matcher — every nested body matcher must match.
976    ///
977    /// Serialises to `{ "type": "ALL_OF", "bodyAllOf": [ <body>, ... ] }`,
978    /// recursing through the normal [`Body`] serialisation for each sub-body.
979    AllOf(Vec<Body>),
980    /// A single-value typed body matcher whose value lives under a named key
981    /// (e.g. `JSON_PATH` → `jsonPath`, `REGEX` → `regex`, `XPATH` → `xpath`).
982    ///
983    /// Serialises to `{ "type": <body_type>, <value_key>: <value> }`. Use the
984    /// [`Body::json_path`] / [`Body::regex`] constructors for the common cases.
985    Matcher {
986        body_type: String,
987        value_key: String,
988        value: String,
989    },
990    /// Any typed body object captured verbatim as a JSON object — the
991    /// forward-compatible catch-all for body matcher/value types that do not
992    /// have a dedicated variant (`STRING`/`subString`, `XML`, `XML_SCHEMA`,
993    /// `JSON_SCHEMA`, `PARAMETERS`, `BINARY`, `GRAPHQL`, `MULTIPART`, `WASM`,
994    /// `JSON_RPC`, `FUZZY`, …). Serialises the map back exactly, so every body
995    /// shape round-trips without silent field loss.
996    Object(serde_json::Map<String, serde_json::Value>),
997}
998
999impl Body {
1000    /// Create a FILE body referencing a path on the server filesystem.
1001    ///
1002    /// # Example
1003    /// ```
1004    /// use mockserver_client::Body;
1005    ///
1006    /// let body = Body::file("/data/response.json")
1007    ///     .with_content_type("application/json")
1008    ///     .with_template_type("VELOCITY");
1009    /// ```
1010    pub fn file(file_path: impl Into<String>) -> Self {
1011        Body::File {
1012            file_path: file_path.into(),
1013            content_type: None,
1014            template_type: None,
1015        }
1016    }
1017
1018    /// Set the content type on a FILE body. No-op on other variants.
1019    pub fn with_content_type(mut self, content_type: impl Into<String>) -> Self {
1020        if let Body::File {
1021            content_type: ref mut ct,
1022            ..
1023        } = self
1024        {
1025            *ct = Some(content_type.into());
1026        }
1027        self
1028    }
1029
1030    /// Set the template type (e.g., "VELOCITY", "MUSTACHE") on a FILE body.
1031    /// No-op on other variants.
1032    pub fn with_template_type(mut self, template_type: impl Into<String>) -> Self {
1033        if let Body::File {
1034            template_type: ref mut tt,
1035            ..
1036        } = self
1037        {
1038            *tt = Some(template_type.into());
1039        }
1040        self
1041    }
1042
1043    /// Create an `ALL_OF` composite body matcher — every nested body matcher
1044    /// must match for the request body to match.
1045    ///
1046    /// # Example
1047    /// ```
1048    /// use mockserver_client::Body;
1049    ///
1050    /// let body = Body::all_of(vec![
1051    ///     Body::json_path("$.name"),
1052    ///     Body::regex(".*active.*"),
1053    /// ]);
1054    /// ```
1055    pub fn all_of(bodies: Vec<Body>) -> Self {
1056        Body::AllOf(bodies)
1057    }
1058
1059    /// Create a `JSON_PATH` body matcher.
1060    ///
1061    /// Serialises to `{ "type": "JSON_PATH", "jsonPath": <expression> }`.
1062    pub fn json_path(expression: impl Into<String>) -> Self {
1063        Body::Matcher {
1064            body_type: "JSON_PATH".to_string(),
1065            value_key: "jsonPath".to_string(),
1066            value: expression.into(),
1067        }
1068    }
1069
1070    /// Create a `REGEX` body matcher.
1071    ///
1072    /// Serialises to `{ "type": "REGEX", "regex": <pattern> }`.
1073    pub fn regex(pattern: impl Into<String>) -> Self {
1074        Body::Matcher {
1075            body_type: "REGEX".to_string(),
1076            value_key: "regex".to_string(),
1077            value: pattern.into(),
1078        }
1079    }
1080
1081    /// Create an `XPATH` body matcher (`{ "type": "XPATH", "xpath": <expr> }`).
1082    pub fn xpath(expression: impl Into<String>) -> Self {
1083        Body::Matcher {
1084            body_type: "XPATH".to_string(),
1085            value_key: "xpath".to_string(),
1086            value: expression.into(),
1087        }
1088    }
1089
1090    /// Create a `STRING` body matcher. When `sub_string` is true the value need
1091    /// only be a substring of the request body.
1092    ///
1093    /// Serialises to `{ "type": "STRING", "string": <value>, "subString": <b> }`.
1094    pub fn string(value: impl Into<String>, sub_string: bool) -> Self {
1095        let mut map = serde_json::Map::new();
1096        map.insert("type".into(), serde_json::Value::from("STRING"));
1097        map.insert("string".into(), serde_json::Value::from(value.into()));
1098        map.insert("subString".into(), serde_json::Value::from(sub_string));
1099        Body::Object(map)
1100    }
1101
1102    /// Create an `XML` body matcher (`{ "type": "XML", "xml": <value> }`).
1103    pub fn xml(value: impl Into<String>) -> Self {
1104        Self::single_object("XML", "xml", value.into())
1105    }
1106
1107    /// Create an `XML_SCHEMA` body matcher
1108    /// (`{ "type": "XML_SCHEMA", "xmlSchema": <schema> }`).
1109    pub fn xml_schema(schema: impl Into<String>) -> Self {
1110        Self::single_object("XML_SCHEMA", "xmlSchema", schema.into())
1111    }
1112
1113    /// Create a `JSON_SCHEMA` body matcher
1114    /// (`{ "type": "JSON_SCHEMA", "jsonSchema": <schema> }`).
1115    pub fn json_schema(schema: impl Into<String>) -> Self {
1116        Self::single_object("JSON_SCHEMA", "jsonSchema", schema.into())
1117    }
1118
1119    /// Create a `PARAMETERS` (form/body parameter) matcher
1120    /// (`{ "type": "PARAMETERS", "parameters": { name: [values] } }`).
1121    pub fn parameters(parameters: HashMap<String, Vec<String>>) -> Self {
1122        let mut map = serde_json::Map::new();
1123        map.insert("type".into(), serde_json::Value::from("PARAMETERS"));
1124        map.insert(
1125            "parameters".into(),
1126            serde_json::to_value(parameters).unwrap_or(serde_json::Value::Null),
1127        );
1128        Body::Object(map)
1129    }
1130
1131    /// Create a `BINARY` body matcher/value from raw bytes (base64-encoded on
1132    /// the wire as `base64Bytes`), with an optional content type.
1133    pub fn binary(data: impl AsRef<[u8]>, content_type: Option<String>) -> Self {
1134        let mut map = serde_json::Map::new();
1135        map.insert("type".into(), serde_json::Value::from("BINARY"));
1136        map.insert(
1137            "base64Bytes".into(),
1138            serde_json::Value::from(BASE64.encode(data.as_ref())),
1139        );
1140        if let Some(ct) = content_type {
1141            map.insert("contentType".into(), serde_json::Value::from(ct));
1142        }
1143        Body::Object(map)
1144    }
1145
1146    /// Create a `GRAPHQL` body matcher (`{ "type": "GRAPHQL", "query": <query> }`).
1147    pub fn graphql(query: impl Into<String>) -> Self {
1148        let mut map = serde_json::Map::new();
1149        map.insert("type".into(), serde_json::Value::from("GRAPHQL"));
1150        map.insert("query".into(), serde_json::Value::from(query.into()));
1151        Body::Object(map)
1152    }
1153
1154    /// Create a `WASM` custom-rule body matcher from a pre-built JSON object.
1155    ///
1156    /// Captured verbatim, so any current or future WASM matcher fields survive
1157    /// a round-trip.
1158    pub fn wasm(object: serde_json::Map<String, serde_json::Value>) -> Self {
1159        Body::Object(object)
1160    }
1161
1162    /// Build a `Body::Object` from a raw JSON object — the escape hatch for any
1163    /// body type not covered by a dedicated constructor.
1164    pub fn object(object: serde_json::Map<String, serde_json::Value>) -> Self {
1165        Body::Object(object)
1166    }
1167
1168    fn single_object(body_type: &str, key: &str, value: String) -> Self {
1169        let mut map = serde_json::Map::new();
1170        map.insert("type".into(), serde_json::Value::from(body_type));
1171        map.insert(key.into(), serde_json::Value::from(value));
1172        Body::Object(map)
1173    }
1174}
1175
1176impl Serialize for Body {
1177    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
1178    where
1179        S: serde::Serializer,
1180    {
1181        match self {
1182            Body::Plain(s) => serializer.serialize_str(s),
1183            Body::Typed { body_type, json } => {
1184                use serde::ser::SerializeMap;
1185                let mut map = serializer.serialize_map(Some(2))?;
1186                map.serialize_entry("type", body_type)?;
1187                map.serialize_entry("json", json)?;
1188                map.end()
1189            }
1190            Body::File {
1191                file_path,
1192                content_type,
1193                template_type,
1194            } => {
1195                use serde::ser::SerializeMap;
1196                let count = 2
1197                    + content_type.as_ref().map_or(0, |_| 1)
1198                    + template_type.as_ref().map_or(0, |_| 1);
1199                let mut map = serializer.serialize_map(Some(count))?;
1200                map.serialize_entry("type", "FILE")?;
1201                map.serialize_entry("filePath", file_path)?;
1202                if let Some(ct) = content_type {
1203                    map.serialize_entry("contentType", ct)?;
1204                }
1205                if let Some(tt) = template_type {
1206                    map.serialize_entry("templateType", tt)?;
1207                }
1208                map.end()
1209            }
1210            Body::AllOf(bodies) => {
1211                use serde::ser::SerializeMap;
1212                let mut map = serializer.serialize_map(Some(2))?;
1213                map.serialize_entry("type", "ALL_OF")?;
1214                map.serialize_entry("bodyAllOf", bodies)?;
1215                map.end()
1216            }
1217            Body::Matcher {
1218                body_type,
1219                value_key,
1220                value,
1221            } => {
1222                use serde::ser::SerializeMap;
1223                let mut map = serializer.serialize_map(Some(2))?;
1224                map.serialize_entry("type", body_type)?;
1225                map.serialize_entry(value_key.as_str(), value)?;
1226                map.end()
1227            }
1228            Body::Object(object) => object.serialize(serializer),
1229        }
1230    }
1231}
1232
1233/// Map a single-value matcher `body_type` to its wire value-key and extract the
1234/// string value from the deserialised object (e.g. `JSON_PATH` → `jsonPath`).
1235fn matcher_key_value(
1236    body_type: &str,
1237    map: &serde_json::Map<String, serde_json::Value>,
1238) -> Option<(String, String)> {
1239    let key = match body_type {
1240        "JSON_PATH" => "jsonPath",
1241        "REGEX" => "regex",
1242        "XPATH" => "xpath",
1243        _ => return None,
1244    };
1245    map.get(key)
1246        .and_then(|v| v.as_str())
1247        .map(|s| (key.to_string(), s.to_string()))
1248}
1249
1250impl<'de> Deserialize<'de> for Body {
1251    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
1252    where
1253        D: serde::Deserializer<'de>,
1254    {
1255        use serde_json::Value;
1256        let v = Value::deserialize(deserializer)?;
1257        match v {
1258            Value::String(s) => Ok(Body::Plain(s)),
1259            Value::Object(map) => {
1260                let body_type = map
1261                    .get("type")
1262                    .and_then(|v| v.as_str())
1263                    .unwrap_or("JSON")
1264                    .to_string();
1265                if body_type == "FILE" {
1266                    let file_path = map
1267                        .get("filePath")
1268                        .and_then(|v| v.as_str())
1269                        .unwrap_or("")
1270                        .to_string();
1271                    let content_type = map
1272                        .get("contentType")
1273                        .and_then(|v| v.as_str())
1274                        .map(|s| s.to_string());
1275                    let template_type = map
1276                        .get("templateType")
1277                        .and_then(|v| v.as_str())
1278                        .map(|s| s.to_string());
1279                    Ok(Body::File {
1280                        file_path,
1281                        content_type,
1282                        template_type,
1283                    })
1284                } else if body_type == "ALL_OF" {
1285                    let bodies = map
1286                        .get("bodyAllOf")
1287                        .and_then(|v| v.as_array())
1288                        .map(|arr| {
1289                            arr.iter()
1290                                .cloned()
1291                                .map(serde_json::from_value)
1292                                .collect::<std::result::Result<Vec<Body>, _>>()
1293                        })
1294                        .transpose()
1295                        .map_err(serde::de::Error::custom)?
1296                        .unwrap_or_default();
1297                    Ok(Body::AllOf(bodies))
1298                } else if map.len() == 2 && matcher_key_value(&body_type, &map).is_some() {
1299                    // Only the bare `{ "type": <T>, <valueKey>: <v> }` shape uses
1300                    // the dedicated Matcher variant. A matcher carrying extra keys
1301                    // (not, optional, matchType, contentType, …) falls through to
1302                    // the verbatim Object variant so those fields are not dropped.
1303                    let (value_key, value) = matcher_key_value(&body_type, &map)
1304                        .expect("matcher_key_value checked above");
1305                    Ok(Body::Matcher {
1306                        body_type,
1307                        value_key,
1308                        value,
1309                    })
1310                } else if body_type == "JSON"
1311                    && map.len() == 2
1312                    && map.get("json").is_some_and(|v| v.is_string())
1313                {
1314                    // Preserve the dedicated typed-JSON representation, but only
1315                    // for the bare `{ "type": "JSON", "json": ... }` shape — a
1316                    // JSON body carrying extra keys (matchType, contentType,
1317                    // not, optional, …) falls through to the verbatim Object
1318                    // variant so those fields are not dropped.
1319                    let json = map
1320                        .get("json")
1321                        .and_then(|v| v.as_str())
1322                        .unwrap_or("")
1323                        .to_string();
1324                    Ok(Body::Typed { body_type, json })
1325                } else {
1326                    // Any other typed body object (STRING, XML, XML_SCHEMA,
1327                    // JSON_SCHEMA, PARAMETERS, BINARY, GRAPHQL, MULTIPART, WASM,
1328                    // JSON_RPC, FUZZY, an inline JSON object literal, …) is kept
1329                    // verbatim so no field is silently dropped.
1330                    Ok(Body::Object(map))
1331                }
1332            }
1333            _ => Ok(Body::Plain(v.to_string())),
1334        }
1335    }
1336}
1337
1338// ---------------------------------------------------------------------------
1339// Jwt
1340// ---------------------------------------------------------------------------
1341
1342/// JWT request matcher — matches a JSON Web Token carried on the request.
1343///
1344/// Serialised under the request's `"jwt"` key. Each entry in [`claims`](Self::claims)
1345/// is a claim name mapped to an exact-or-regex string; a leading `!` negates the
1346/// match. The optional [`issuer`](Self::issuer), [`audience`](Self::audience),
1347/// [`algorithm`](Self::algorithm), [`header`](Self::header) and
1348/// [`scheme`](Self::scheme) fields are omitted from the wire form when unset.
1349///
1350/// # Example
1351/// ```
1352/// use mockserver_client::Jwt;
1353///
1354/// let jwt = Jwt::new()
1355///     .claim("sub", "user-123")
1356///     .claim("role", "!admin")
1357///     .claim("email", "^.+@example.com$")
1358///     .issuer("https://issuer.example.com")
1359///     .audience("my-api")
1360///     .algorithm("RS256")
1361///     .header("authorization")
1362///     .scheme("Bearer");
1363/// ```
1364#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1365#[serde(rename_all = "camelCase")]
1366pub struct Jwt {
1367    /// Claim name → exact-or-regex value (leading `!` negates).
1368    pub claims: HashMap<String, String>,
1369
1370    #[serde(skip_serializing_if = "Option::is_none")]
1371    pub issuer: Option<String>,
1372
1373    #[serde(skip_serializing_if = "Option::is_none")]
1374    pub audience: Option<String>,
1375
1376    #[serde(skip_serializing_if = "Option::is_none")]
1377    pub algorithm: Option<String>,
1378
1379    #[serde(skip_serializing_if = "Option::is_none")]
1380    pub header: Option<String>,
1381
1382    #[serde(skip_serializing_if = "Option::is_none")]
1383    pub scheme: Option<String>,
1384}
1385
1386impl Jwt {
1387    /// Create a new empty JWT matcher (no claims, no constraints).
1388    pub fn new() -> Self {
1389        Self::default()
1390    }
1391
1392    /// Add a claim constraint. The value is an exact-or-regex string; a leading
1393    /// `!` negates the match.
1394    pub fn claim(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
1395        self.claims.insert(name.into(), value.into());
1396        self
1397    }
1398
1399    /// Replace the full claims map.
1400    pub fn claims(mut self, claims: HashMap<String, String>) -> Self {
1401        self.claims = claims;
1402        self
1403    }
1404
1405    /// Require the `iss` (issuer) claim to equal the given value.
1406    pub fn issuer(mut self, issuer: impl Into<String>) -> Self {
1407        self.issuer = Some(issuer.into());
1408        self
1409    }
1410
1411    /// Require the `aud` (audience) claim to equal the given value.
1412    pub fn audience(mut self, audience: impl Into<String>) -> Self {
1413        self.audience = Some(audience.into());
1414        self
1415    }
1416
1417    /// Require the token to be signed with the given algorithm (e.g. "RS256").
1418    pub fn algorithm(mut self, algorithm: impl Into<String>) -> Self {
1419        self.algorithm = Some(algorithm.into());
1420        self
1421    }
1422
1423    /// Set the request header the token is carried in (default "authorization").
1424    pub fn header(mut self, header: impl Into<String>) -> Self {
1425        self.header = Some(header.into());
1426        self
1427    }
1428
1429    /// Set the auth scheme prefix stripped from the header value (e.g. "Bearer").
1430    pub fn scheme(mut self, scheme: impl Into<String>) -> Self {
1431        self.scheme = Some(scheme.into());
1432        self
1433    }
1434}
1435
1436// ---------------------------------------------------------------------------
1437// HttpResponse
1438// ---------------------------------------------------------------------------
1439
1440/// Builder for an HTTP response action.
1441///
1442/// # Example
1443/// ```
1444/// use mockserver_client::HttpResponse;
1445///
1446/// let response = HttpResponse::new()
1447///     .status_code(201)
1448///     .header("Location", "/api/users/42")
1449///     .body("{\"id\": 42}");
1450/// ```
1451#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1452#[serde(rename_all = "camelCase")]
1453pub struct HttpResponse {
1454    #[serde(skip_serializing_if = "Option::is_none")]
1455    pub status_code: Option<u16>,
1456
1457    #[serde(skip_serializing_if = "Option::is_none")]
1458    pub headers: Option<HashMap<String, Vec<String>>>,
1459
1460    #[serde(skip_serializing_if = "Option::is_none")]
1461    pub body: Option<String>,
1462
1463    #[serde(skip_serializing_if = "Option::is_none")]
1464    pub delay: Option<Delay>,
1465
1466    /// Response cookies (single value per name; emitted as `Set-Cookie`).
1467    #[serde(skip_serializing_if = "Option::is_none")]
1468    pub cookies: Option<HashMap<String, String>>,
1469
1470    /// HTTP reason phrase (e.g. `"Not Found"`); overrides the default for the
1471    /// status code.
1472    #[serde(skip_serializing_if = "Option::is_none")]
1473    pub reason_phrase: Option<String>,
1474
1475    /// A status-code range to respond with a random status from (e.g. `"2xx"`).
1476    #[serde(skip_serializing_if = "Option::is_none")]
1477    pub status_code_range: Option<String>,
1478
1479    /// Response trailers (HTTP/2 trailing headers), multiple values per key.
1480    #[serde(skip_serializing_if = "Option::is_none")]
1481    pub trailers: Option<HashMap<String, Vec<String>>>,
1482
1483    /// Generate the response body from an inline/JSON-schema string.
1484    #[serde(skip_serializing_if = "Option::is_none")]
1485    pub generate_from_schema: Option<String>,
1486
1487    /// Connection-level options (chunking, content-length, socket close, …).
1488    #[serde(skip_serializing_if = "Option::is_none")]
1489    pub connection_options: Option<ConnectionOptions>,
1490
1491    /// Fail the first N requests then recover (circuit-breaker style).
1492    #[serde(skip_serializing_if = "Option::is_none")]
1493    pub recover_after: Option<RecoverAfter>,
1494
1495    /// Mark this response as the primary action of a composite expectation.
1496    #[serde(skip_serializing_if = "Option::is_none")]
1497    pub primary: Option<bool>,
1498
1499    /// Forward-compatibility catch-all for response fields the typed model does
1500    /// not yet name.
1501    #[serde(flatten, default)]
1502    pub extra: Extra,
1503}
1504
1505impl HttpResponse {
1506    /// Create a new empty response.
1507    pub fn new() -> Self {
1508        Self::default()
1509    }
1510
1511    /// Set the HTTP status code.
1512    pub fn status_code(mut self, code: u16) -> Self {
1513        self.status_code = Some(code);
1514        self
1515    }
1516
1517    /// Add a response header.
1518    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
1519        let headers = self.headers.get_or_insert_with(HashMap::new);
1520        headers.entry(key.into()).or_default().push(value.into());
1521        self
1522    }
1523
1524    /// Set the response body as a string.
1525    pub fn body(mut self, body: impl Into<String>) -> Self {
1526        self.body = Some(body.into());
1527        self
1528    }
1529
1530    /// Set a response delay.
1531    pub fn delay(mut self, delay: Delay) -> Self {
1532        self.delay = Some(delay);
1533        self
1534    }
1535
1536    /// Add a response cookie (single value per name).
1537    pub fn cookie(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
1538        let cookies = self.cookies.get_or_insert_with(HashMap::new);
1539        cookies.insert(name.into(), value.into());
1540        self
1541    }
1542
1543    /// Set the HTTP reason phrase (e.g. `"Not Found"`).
1544    pub fn reason_phrase(mut self, reason_phrase: impl Into<String>) -> Self {
1545        self.reason_phrase = Some(reason_phrase.into());
1546        self
1547    }
1548
1549    /// Set a status-code range to respond with a random status from (e.g. `"2xx"`).
1550    pub fn status_code_range(mut self, range: impl Into<String>) -> Self {
1551        self.status_code_range = Some(range.into());
1552        self
1553    }
1554
1555    /// Add a response trailer (HTTP/2 trailing header), multiple values per key.
1556    pub fn trailer(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
1557        let trailers = self.trailers.get_or_insert_with(HashMap::new);
1558        trailers.entry(key.into()).or_default().push(value.into());
1559        self
1560    }
1561
1562    /// Generate the response body from a schema string.
1563    pub fn generate_from_schema(mut self, schema: impl Into<String>) -> Self {
1564        self.generate_from_schema = Some(schema.into());
1565        self
1566    }
1567
1568    /// Set connection-level options.
1569    pub fn connection_options(mut self, options: ConnectionOptions) -> Self {
1570        self.connection_options = Some(options);
1571        self
1572    }
1573
1574    /// Set a recover-after (fail-first-N) policy.
1575    pub fn recover_after(mut self, recover_after: RecoverAfter) -> Self {
1576        self.recover_after = Some(recover_after);
1577        self
1578    }
1579
1580    /// Mark this response as the primary action of a composite expectation.
1581    pub fn primary(mut self, primary: bool) -> Self {
1582        self.primary = Some(primary);
1583        self
1584    }
1585}
1586
1587// ---------------------------------------------------------------------------
1588// HttpTemplate (response or forward)
1589// ---------------------------------------------------------------------------
1590
1591/// Template action — evaluate a response or forward template (Velocity, Mustache, etc.).
1592///
1593/// Used as `httpResponseTemplate` or `httpForwardTemplate` in an expectation.
1594///
1595/// # Example
1596/// ```
1597/// use mockserver_client::HttpTemplate;
1598///
1599/// let tmpl = HttpTemplate::new("VELOCITY", "{ \"statusCode\": 200 }")
1600///     .template_file("/path/to/template.vm");
1601/// ```
1602#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1603#[serde(rename_all = "camelCase")]
1604pub struct HttpTemplate {
1605    #[serde(skip_serializing_if = "Option::is_none")]
1606    pub template_type: Option<String>,
1607
1608    #[serde(skip_serializing_if = "Option::is_none")]
1609    pub template: Option<String>,
1610
1611    #[serde(skip_serializing_if = "Option::is_none")]
1612    pub template_file: Option<String>,
1613}
1614
1615impl HttpTemplate {
1616    /// Create a template action with the given type and inline template body.
1617    pub fn new(template_type: impl Into<String>, template: impl Into<String>) -> Self {
1618        Self {
1619            template_type: Some(template_type.into()),
1620            template: Some(template.into()),
1621            template_file: None,
1622        }
1623    }
1624
1625    /// Create a template action that loads from a file path.
1626    pub fn from_file(template_type: impl Into<String>, file_path: impl Into<String>) -> Self {
1627        Self {
1628            template_type: Some(template_type.into()),
1629            template: None,
1630            template_file: Some(file_path.into()),
1631        }
1632    }
1633
1634    /// Set the template type (e.g., "VELOCITY", "MUSTACHE").
1635    pub fn template_type(mut self, template_type: impl Into<String>) -> Self {
1636        self.template_type = Some(template_type.into());
1637        self
1638    }
1639
1640    /// Set the inline template body.
1641    pub fn template(mut self, template: impl Into<String>) -> Self {
1642        self.template = Some(template.into());
1643        self
1644    }
1645
1646    /// Set the template file path (alternative to inline template).
1647    pub fn template_file(mut self, file_path: impl Into<String>) -> Self {
1648        self.template_file = Some(file_path.into());
1649        self
1650    }
1651}
1652
1653// ---------------------------------------------------------------------------
1654// HttpForward
1655// ---------------------------------------------------------------------------
1656
1657/// Forward action — proxy the matched request to another host.
1658///
1659/// # Example
1660/// ```
1661/// use mockserver_client::HttpForward;
1662///
1663/// let forward = HttpForward::new("backend.local", 8080);
1664/// ```
1665#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1666#[serde(rename_all = "camelCase")]
1667pub struct HttpForward {
1668    pub host: String,
1669
1670    #[serde(skip_serializing_if = "Option::is_none")]
1671    pub port: Option<u16>,
1672
1673    #[serde(skip_serializing_if = "Option::is_none")]
1674    pub scheme: Option<String>,
1675
1676    /// Delay applied before the request is forwarded.
1677    #[serde(skip_serializing_if = "Option::is_none")]
1678    pub delay: Option<Delay>,
1679
1680    #[serde(skip_serializing_if = "Option::is_none")]
1681    pub primary: Option<bool>,
1682
1683    /// Catch-all for server fields this client does not model yet, so a
1684    /// retrieve -> re-submit cycle cannot silently destroy them.
1685    #[serde(flatten, default)]
1686    pub extra: Extra,
1687}
1688
1689impl HttpForward {
1690    /// Create a forward action to the given host and port.
1691    pub fn new(host: impl Into<String>, port: u16) -> Self {
1692        Self {
1693            host: host.into(),
1694            port: Some(port),
1695            scheme: None,
1696            delay: None,
1697            primary: None,
1698            extra: Extra::default(),
1699        }
1700    }
1701
1702    /// Set the scheme (HTTP or HTTPS).
1703    pub fn scheme(mut self, scheme: impl Into<String>) -> Self {
1704        self.scheme = Some(scheme.into());
1705        self
1706    }
1707
1708    /// Set a delay applied before the request is forwarded.
1709    pub fn delay(mut self, delay: Delay) -> Self {
1710        self.delay = Some(delay);
1711        self
1712    }
1713
1714    /// Mark this action as the primary action of the expectation.
1715    pub fn primary(mut self, primary: bool) -> Self {
1716        self.primary = Some(primary);
1717        self
1718    }
1719}
1720
1721// ---------------------------------------------------------------------------
1722// HttpClassCallback
1723// ---------------------------------------------------------------------------
1724
1725/// Class callback action — delegates the response (or forward) to a server-side
1726/// class that implements MockServer's callback interface.
1727///
1728/// This is a purely declarative (REST-only) callback: no WebSocket is involved.
1729/// The named class must be on the MockServer server's classpath. Serialized as
1730/// `httpResponseClassCallback` or `httpForwardClassCallback` in an expectation.
1731///
1732/// # Example
1733/// ```
1734/// use mockserver_client::HttpClassCallback;
1735///
1736/// let cb = HttpClassCallback::new("com.example.MyCallback").primary(true);
1737/// ```
1738#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1739#[serde(rename_all = "camelCase")]
1740pub struct HttpClassCallback {
1741    pub callback_class: String,
1742
1743    #[serde(skip_serializing_if = "Option::is_none")]
1744    pub delay: Option<Delay>,
1745
1746    #[serde(skip_serializing_if = "Option::is_none")]
1747    pub primary: Option<bool>,
1748}
1749
1750impl HttpClassCallback {
1751    /// Create a class callback referencing the fully-qualified class name of a
1752    /// server-side callback implementation.
1753    pub fn new(callback_class: impl Into<String>) -> Self {
1754        Self {
1755            callback_class: callback_class.into(),
1756            delay: None,
1757            primary: None,
1758        }
1759    }
1760
1761    /// Set a delay applied before the callback runs.
1762    pub fn delay(mut self, delay: Delay) -> Self {
1763        self.delay = Some(delay);
1764        self
1765    }
1766
1767    /// Mark this callback as primary (kept on the primary event-loop thread).
1768    pub fn primary(mut self, primary: bool) -> Self {
1769        self.primary = Some(primary);
1770        self
1771    }
1772}
1773
1774// ---------------------------------------------------------------------------
1775// HttpObjectCallback
1776// ---------------------------------------------------------------------------
1777
1778/// Object (closure) callback action — delegates the response (or forward) to a
1779/// client-side closure invoked over the callback WebSocket.
1780///
1781/// The `client_id` is the id assigned by MockServer when the client opens the
1782/// callback WebSocket (`/_mockserver_callback_websocket`). When a request
1783/// matches, the server pushes it over that socket and the client's registered
1784/// closure produces the response. Serialized as `httpResponseObjectCallback` or
1785/// `httpForwardObjectCallback` in an expectation.
1786///
1787/// Most users do not construct this directly — use
1788/// [`MockServerClient::mock_with_callback`](crate::MockServerClient::mock_with_callback),
1789/// which opens the shared WebSocket, registers the closure, and wires up the
1790/// `client_id` automatically.
1791#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1792#[serde(rename_all = "camelCase")]
1793pub struct HttpObjectCallback {
1794    pub client_id: String,
1795
1796    #[serde(skip_serializing_if = "Option::is_none")]
1797    pub response_callback: Option<bool>,
1798
1799    #[serde(skip_serializing_if = "Option::is_none")]
1800    pub delay: Option<Delay>,
1801
1802    #[serde(skip_serializing_if = "Option::is_none")]
1803    pub primary: Option<bool>,
1804}
1805
1806impl HttpObjectCallback {
1807    /// Create an object callback bound to the given callback-WebSocket client id.
1808    pub fn new(client_id: impl Into<String>) -> Self {
1809        Self {
1810            client_id: client_id.into(),
1811            response_callback: None,
1812            delay: None,
1813            primary: None,
1814        }
1815    }
1816
1817    /// Set whether the callback also receives the response (forward + response form).
1818    pub fn response_callback(mut self, response_callback: bool) -> Self {
1819        self.response_callback = Some(response_callback);
1820        self
1821    }
1822
1823    /// Set a delay applied before the callback runs.
1824    pub fn delay(mut self, delay: Delay) -> Self {
1825        self.delay = Some(delay);
1826        self
1827    }
1828
1829    /// Mark this callback as primary (kept on the primary event-loop thread).
1830    pub fn primary(mut self, primary: bool) -> Self {
1831        self.primary = Some(primary);
1832        self
1833    }
1834}
1835
1836// ---------------------------------------------------------------------------
1837// HttpError
1838// ---------------------------------------------------------------------------
1839
1840/// Error action — return a connection-level error to the caller.
1841#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1842#[serde(rename_all = "camelCase")]
1843pub struct HttpError {
1844    #[serde(skip_serializing_if = "Option::is_none")]
1845    pub drop_connection: Option<bool>,
1846
1847    #[serde(skip_serializing_if = "Option::is_none")]
1848    pub response_bytes: Option<String>,
1849
1850    /// Delay applied before the error is returned.
1851    #[serde(skip_serializing_if = "Option::is_none")]
1852    pub delay: Option<Delay>,
1853
1854    /// Reset the matched request stream with this error code (HTTP/2 RST_STREAM /
1855    /// HTTP/3 RESET_STREAM) instead of returning a response. Takes precedence over
1856    /// `drop_connection`; HTTP/1.1 has no stream concept.
1857    #[serde(skip_serializing_if = "Option::is_none")]
1858    pub stream_error: Option<i64>,
1859
1860    #[serde(skip_serializing_if = "Option::is_none")]
1861    pub primary: Option<bool>,
1862
1863    /// Catch-all for server fields this client does not model yet, so a
1864    /// retrieve -> re-submit cycle cannot silently destroy them.
1865    #[serde(flatten, default)]
1866    pub extra: Extra,
1867}
1868
1869impl HttpError {
1870    /// Create a new error action.
1871    pub fn new() -> Self {
1872        Self::default()
1873    }
1874
1875    /// Drop the connection without a response.
1876    pub fn drop_connection(mut self, drop: bool) -> Self {
1877        self.drop_connection = Some(drop);
1878        self
1879    }
1880
1881    /// Send arbitrary bytes then close.
1882    pub fn response_bytes(mut self, bytes: impl Into<String>) -> Self {
1883        self.response_bytes = Some(bytes.into());
1884        self
1885    }
1886
1887    /// Set a delay applied before the error is returned.
1888    pub fn delay(mut self, delay: Delay) -> Self {
1889        self.delay = Some(delay);
1890        self
1891    }
1892
1893    /// Reset the matched request stream with this error code instead of responding.
1894    pub fn stream_error(mut self, code: i64) -> Self {
1895        self.stream_error = Some(code);
1896        self
1897    }
1898
1899    /// Mark this action as the primary action of the expectation.
1900    pub fn primary(mut self, primary: bool) -> Self {
1901        self.primary = Some(primary);
1902        self
1903    }
1904}
1905
1906// ---------------------------------------------------------------------------
1907// HttpSseResponse (Server-Sent Events)
1908// ---------------------------------------------------------------------------
1909
1910/// A single Server-Sent Event in an [`HttpSseResponse`].
1911///
1912/// Maps to the `events[]` entries of the `httpSseResponse` wire shape.
1913#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1914#[serde(rename_all = "camelCase")]
1915pub struct SseEvent {
1916    #[serde(skip_serializing_if = "Option::is_none")]
1917    pub event: Option<String>,
1918
1919    #[serde(skip_serializing_if = "Option::is_none")]
1920    pub data: Option<String>,
1921
1922    #[serde(skip_serializing_if = "Option::is_none")]
1923    pub id: Option<String>,
1924
1925    #[serde(skip_serializing_if = "Option::is_none")]
1926    pub retry: Option<u32>,
1927
1928    #[serde(skip_serializing_if = "Option::is_none")]
1929    pub delay: Option<Delay>,
1930}
1931
1932impl SseEvent {
1933    /// Create a new empty SSE event.
1934    pub fn new() -> Self {
1935        Self::default()
1936    }
1937
1938    /// Set the `event:` field (event type/name).
1939    pub fn event(mut self, event: impl Into<String>) -> Self {
1940        self.event = Some(event.into());
1941        self
1942    }
1943
1944    /// Set the `data:` payload.
1945    pub fn data(mut self, data: impl Into<String>) -> Self {
1946        self.data = Some(data.into());
1947        self
1948    }
1949
1950    /// Set the `id:` field.
1951    pub fn id(mut self, id: impl Into<String>) -> Self {
1952        self.id = Some(id.into());
1953        self
1954    }
1955
1956    /// Set the `retry:` reconnection time in milliseconds.
1957    pub fn retry(mut self, retry: u32) -> Self {
1958        self.retry = Some(retry);
1959        self
1960    }
1961
1962    /// Set a delay before this event is emitted.
1963    pub fn delay(mut self, delay: Delay) -> Self {
1964        self.delay = Some(delay);
1965        self
1966    }
1967}
1968
1969/// Builder for a Server-Sent Events (SSE) streaming response action.
1970///
1971/// Serialized as the `httpSseResponse` action in an expectation.
1972///
1973/// # Example
1974/// ```
1975/// use mockserver_client::{HttpSseResponse, SseEvent};
1976///
1977/// let sse = HttpSseResponse::new()
1978///     .status_code(200)
1979///     .header("Content-Type", "text/event-stream")
1980///     .event(SseEvent::new().event("message").data("hello").id("1"))
1981///     .close_connection(true);
1982/// ```
1983#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1984#[serde(rename_all = "camelCase")]
1985pub struct HttpSseResponse {
1986    #[serde(skip_serializing_if = "Option::is_none")]
1987    pub status_code: Option<u16>,
1988
1989    #[serde(skip_serializing_if = "Option::is_none")]
1990    pub headers: Option<HashMap<String, Vec<String>>>,
1991
1992    #[serde(skip_serializing_if = "Option::is_none")]
1993    pub events: Option<Vec<SseEvent>>,
1994
1995    #[serde(skip_serializing_if = "Option::is_none")]
1996    pub close_connection: Option<bool>,
1997
1998    #[serde(skip_serializing_if = "Option::is_none")]
1999    pub delay: Option<Delay>,
2000
2001    #[serde(skip_serializing_if = "Option::is_none")]
2002    pub template_type: Option<String>,
2003
2004    #[serde(skip_serializing_if = "Option::is_none")]
2005    pub primary: Option<bool>,
2006
2007    /// Catch-all for server fields this client does not model yet, so a
2008    /// retrieve -> re-submit cycle cannot silently destroy them.
2009    #[serde(flatten, default)]
2010    pub extra: Extra,
2011}
2012
2013impl HttpSseResponse {
2014    /// Create a new empty SSE response.
2015    pub fn new() -> Self {
2016        Self::default()
2017    }
2018
2019    /// Set the HTTP status code.
2020    pub fn status_code(mut self, code: u16) -> Self {
2021        self.status_code = Some(code);
2022        self
2023    }
2024
2025    /// Add a response header.
2026    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
2027        let headers = self.headers.get_or_insert_with(HashMap::new);
2028        headers.entry(key.into()).or_default().push(value.into());
2029        self
2030    }
2031
2032    /// Append an SSE event to the stream.
2033    pub fn event(mut self, event: SseEvent) -> Self {
2034        self.events.get_or_insert_with(Vec::new).push(event);
2035        self
2036    }
2037
2038    /// Replace all SSE events.
2039    pub fn events(mut self, events: Vec<SseEvent>) -> Self {
2040        self.events = Some(events);
2041        self
2042    }
2043
2044    /// Whether to close the connection after emitting all events.
2045    pub fn close_connection(mut self, close: bool) -> Self {
2046        self.close_connection = Some(close);
2047        self
2048    }
2049
2050    /// Set a delay before the response starts.
2051    pub fn delay(mut self, delay: Delay) -> Self {
2052        self.delay = Some(delay);
2053        self
2054    }
2055
2056    /// Set the template type (`VELOCITY`, `JAVASCRIPT` or `MUSTACHE`).
2057    pub fn template_type(mut self, template_type: impl Into<String>) -> Self {
2058        self.template_type = Some(template_type.into());
2059        self
2060    }
2061
2062    /// Mark this action as the primary action of the expectation.
2063    pub fn primary(mut self, primary: bool) -> Self {
2064        self.primary = Some(primary);
2065        self
2066    }
2067}
2068
2069// ---------------------------------------------------------------------------
2070// GraphqlSubscriptionFilter
2071// ---------------------------------------------------------------------------
2072
2073/// GraphQL subscription query filter for the `graphql-transport-ws` protocol.
2074///
2075/// When set on an [`HttpWebSocketResponse`], incoming subscribe messages are
2076/// AST-matched against this filter.
2077#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2078#[serde(rename_all = "camelCase")]
2079pub struct GraphqlSubscriptionFilter {
2080    pub query: String,
2081
2082    #[serde(skip_serializing_if = "Option::is_none")]
2083    pub operation_name: Option<String>,
2084
2085    #[serde(skip_serializing_if = "Option::is_none")]
2086    pub variables_schema: Option<String>,
2087
2088    /// One of `NORMALISED_STRING`, `AST_EXACT`, `AST_SUBSET`.
2089    #[serde(skip_serializing_if = "Option::is_none")]
2090    pub selection_set_match_type: Option<String>,
2091
2092    #[serde(skip_serializing_if = "Option::is_none")]
2093    pub fields: Option<Vec<String>>,
2094
2095    #[serde(flatten, default)]
2096    pub extra: Extra,
2097}
2098
2099impl GraphqlSubscriptionFilter {
2100    /// Create a filter for the given subscription query.
2101    pub fn new(query: impl Into<String>) -> Self {
2102        Self { query: query.into(), ..Default::default() }
2103    }
2104
2105    /// Set the operation name.
2106    pub fn operation_name(mut self, operation_name: impl Into<String>) -> Self {
2107        self.operation_name = Some(operation_name.into());
2108        self
2109    }
2110
2111    /// Set the variables JSON schema.
2112    pub fn variables_schema(mut self, variables_schema: impl Into<String>) -> Self {
2113        self.variables_schema = Some(variables_schema.into());
2114        self
2115    }
2116
2117    /// Set the selection-set match type.
2118    pub fn selection_set_match_type(mut self, selection_set_match_type: impl Into<String>) -> Self {
2119        self.selection_set_match_type = Some(selection_set_match_type.into());
2120        self
2121    }
2122
2123    /// Set the fields to match.
2124    pub fn fields(mut self, fields: Vec<String>) -> Self {
2125        self.fields = Some(fields);
2126        self
2127    }
2128}
2129// ---------------------------------------------------------------------------
2130
2131/// A single WebSocket message in an [`HttpWebSocketResponse`].
2132///
2133/// Either `text` or `binary` should be set. Binary data is base64-encoded
2134/// on the wire (the schema declares `binary` as `format: byte`).
2135#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2136#[serde(rename_all = "camelCase")]
2137pub struct WebSocketMessage {
2138    #[serde(skip_serializing_if = "Option::is_none")]
2139    pub text: Option<String>,
2140
2141    #[serde(skip_serializing_if = "Option::is_none")]
2142    pub binary: Option<String>,
2143
2144    #[serde(skip_serializing_if = "Option::is_none")]
2145    pub delay: Option<Delay>,
2146}
2147
2148impl WebSocketMessage {
2149    /// Create a text WebSocket message.
2150    pub fn text(text: impl Into<String>) -> Self {
2151        Self {
2152            text: Some(text.into()),
2153            binary: None,
2154            delay: None,
2155        }
2156    }
2157
2158    /// Create a binary WebSocket message from raw bytes (base64-encoded on the wire).
2159    pub fn binary(data: impl AsRef<[u8]>) -> Self {
2160        Self {
2161            text: None,
2162            binary: Some(BASE64.encode(data.as_ref())),
2163            delay: None,
2164        }
2165    }
2166
2167    /// Create a binary WebSocket message from an already base64-encoded string.
2168    pub fn binary_base64(base64: impl Into<String>) -> Self {
2169        Self {
2170            text: None,
2171            binary: Some(base64.into()),
2172            delay: None,
2173        }
2174    }
2175
2176    /// Set a delay before this message is sent.
2177    pub fn delay(mut self, delay: Delay) -> Self {
2178        self.delay = Some(delay);
2179        self
2180    }
2181}
2182
2183/// A per-incoming-frame response rule inside an [`HttpWebSocketResponse::matchers`].
2184///
2185/// When an incoming WebSocket frame matches this rule (by `frame_type` and/or
2186/// `text_matcher`), the paired [`responses`](Self::responses) are sent back.
2187/// Unknown fields are captured in [`extra`](Self::extra) so the shape round-trips
2188/// without loss.
2189#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2190#[serde(rename_all = "camelCase")]
2191pub struct WebSocketMatcher {
2192    /// Frame type to match: `"TEXT"`, `"BINARY"`, `"PING"`, `"PONG"` or `"ANY"`.
2193    #[serde(skip_serializing_if = "Option::is_none")]
2194    pub frame_type: Option<String>,
2195
2196    /// Exact-or-regex matcher applied to a text frame's payload.
2197    #[serde(skip_serializing_if = "Option::is_none")]
2198    pub text_matcher: Option<String>,
2199
2200    /// Messages sent in reply when an incoming frame matches this rule.
2201    #[serde(skip_serializing_if = "Option::is_none")]
2202    pub responses: Option<Vec<WebSocketMessage>>,
2203
2204    /// Forward-compatibility catch-all for matcher fields not yet named.
2205    #[serde(flatten, default)]
2206    pub extra: Extra,
2207}
2208
2209impl WebSocketMatcher {
2210    /// Create a new empty matcher rule.
2211    pub fn new() -> Self {
2212        Self::default()
2213    }
2214
2215    /// Set the frame type to match (`"TEXT"`, `"BINARY"`, `"PING"`, `"PONG"`, `"ANY"`).
2216    pub fn frame_type(mut self, frame_type: impl Into<String>) -> Self {
2217        self.frame_type = Some(frame_type.into());
2218        self
2219    }
2220
2221    /// Set an exact-or-regex matcher for a text frame's payload.
2222    pub fn text_matcher(mut self, text_matcher: impl Into<String>) -> Self {
2223        self.text_matcher = Some(text_matcher.into());
2224        self
2225    }
2226
2227    /// Append a reply message sent when an incoming frame matches this rule.
2228    pub fn response(mut self, response: WebSocketMessage) -> Self {
2229        self.responses.get_or_insert_with(Vec::new).push(response);
2230        self
2231    }
2232
2233    /// Replace all reply messages.
2234    pub fn responses(mut self, responses: Vec<WebSocketMessage>) -> Self {
2235        self.responses = Some(responses);
2236        self
2237    }
2238}
2239
2240// ---------------------------------------------------------------------------
2241// HttpWebSocketResponse
2242// ---------------------------------------------------------------------------
2243
2244/// Builder for a WebSocket streaming response action.
2245///
2246/// Serialized as the `httpWebSocketResponse` action in an expectation.
2247///
2248/// # Example
2249/// ```
2250/// use mockserver_client::{HttpWebSocketResponse, WebSocketMessage};
2251///
2252/// let ws = HttpWebSocketResponse::new()
2253///     .subprotocol("chat")
2254///     .message(WebSocketMessage::text("hello"))
2255///     .message(WebSocketMessage::binary([0x01, 0x02]))
2256///     .close_connection(true);
2257/// ```
2258#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2259#[serde(rename_all = "camelCase")]
2260pub struct HttpWebSocketResponse {
2261    #[serde(skip_serializing_if = "Option::is_none")]
2262    pub subprotocol: Option<String>,
2263
2264    #[serde(skip_serializing_if = "Option::is_none")]
2265    pub messages: Option<Vec<WebSocketMessage>>,
2266
2267    /// Per-incoming-frame response rules; when set, an incoming frame matching a
2268    /// rule triggers that rule's `responses`.
2269    #[serde(skip_serializing_if = "Option::is_none")]
2270    pub matchers: Option<Vec<WebSocketMatcher>>,
2271
2272    #[serde(skip_serializing_if = "Option::is_none")]
2273    pub close_connection: Option<bool>,
2274
2275    #[serde(skip_serializing_if = "Option::is_none")]
2276    pub delay: Option<Delay>,
2277
2278    #[serde(skip_serializing_if = "Option::is_none")]
2279    pub template_type: Option<String>,
2280
2281    /// GraphQL subscription query filter for the graphql-transport-ws protocol.
2282    #[serde(skip_serializing_if = "Option::is_none")]
2283    pub graphql_subscription_filter: Option<GraphqlSubscriptionFilter>,
2284
2285    #[serde(skip_serializing_if = "Option::is_none")]
2286    pub primary: Option<bool>,
2287
2288    /// Catch-all for server fields this client does not model yet, so a
2289    /// retrieve -> re-submit cycle cannot silently destroy them.
2290    #[serde(flatten, default)]
2291    pub extra: Extra,
2292}
2293
2294impl HttpWebSocketResponse {
2295    /// Create a new empty WebSocket response.
2296    pub fn new() -> Self {
2297        Self::default()
2298    }
2299
2300    /// Append an incoming-frame matcher rule.
2301    pub fn matcher(mut self, matcher: WebSocketMatcher) -> Self {
2302        self.matchers.get_or_insert_with(Vec::new).push(matcher);
2303        self
2304    }
2305
2306    /// Replace all incoming-frame matcher rules.
2307    pub fn matchers(mut self, matchers: Vec<WebSocketMatcher>) -> Self {
2308        self.matchers = Some(matchers);
2309        self
2310    }
2311
2312    /// Set the negotiated subprotocol.
2313    pub fn subprotocol(mut self, subprotocol: impl Into<String>) -> Self {
2314        self.subprotocol = Some(subprotocol.into());
2315        self
2316    }
2317
2318    /// Append a WebSocket message to send.
2319    pub fn message(mut self, message: WebSocketMessage) -> Self {
2320        self.messages.get_or_insert_with(Vec::new).push(message);
2321        self
2322    }
2323
2324    /// Replace all WebSocket messages.
2325    pub fn messages(mut self, messages: Vec<WebSocketMessage>) -> Self {
2326        self.messages = Some(messages);
2327        self
2328    }
2329
2330    /// Whether to close the connection after emitting all messages.
2331    pub fn close_connection(mut self, close: bool) -> Self {
2332        self.close_connection = Some(close);
2333        self
2334    }
2335
2336    /// Set a delay before the response starts.
2337    pub fn delay(mut self, delay: Delay) -> Self {
2338        self.delay = Some(delay);
2339        self
2340    }
2341
2342    /// Set the template type (`VELOCITY`, `JAVASCRIPT` or `MUSTACHE`).
2343    pub fn template_type(mut self, template_type: impl Into<String>) -> Self {
2344        self.template_type = Some(template_type.into());
2345        self
2346    }
2347
2348    /// Set the GraphQL subscription filter.
2349    pub fn graphql_subscription_filter(mut self, filter: GraphqlSubscriptionFilter) -> Self {
2350        self.graphql_subscription_filter = Some(filter);
2351        self
2352    }
2353
2354    /// Mark this action as the primary action of the expectation.
2355    pub fn primary(mut self, primary: bool) -> Self {
2356        self.primary = Some(primary);
2357        self
2358    }
2359}
2360
2361// ---------------------------------------------------------------------------
2362// DnsResponse
2363// ---------------------------------------------------------------------------
2364
2365/// A single DNS resource record in a [`DnsResponse`].
2366#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2367#[serde(rename_all = "camelCase")]
2368pub struct DnsRecord {
2369    #[serde(skip_serializing_if = "Option::is_none")]
2370    pub name: Option<String>,
2371
2372    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
2373    pub record_type: Option<String>,
2374
2375    #[serde(skip_serializing_if = "Option::is_none")]
2376    pub dns_class: Option<String>,
2377
2378    #[serde(skip_serializing_if = "Option::is_none")]
2379    pub ttl: Option<u32>,
2380
2381    #[serde(skip_serializing_if = "Option::is_none")]
2382    pub value: Option<String>,
2383
2384    #[serde(skip_serializing_if = "Option::is_none")]
2385    pub priority: Option<u32>,
2386
2387    #[serde(skip_serializing_if = "Option::is_none")]
2388    pub weight: Option<u32>,
2389
2390    #[serde(skip_serializing_if = "Option::is_none")]
2391    pub port: Option<u16>,
2392}
2393
2394impl DnsRecord {
2395    /// Create a new empty DNS record.
2396    pub fn new() -> Self {
2397        Self::default()
2398    }
2399
2400    /// Create an `A` (IPv4 address) record.
2401    pub fn a(name: impl Into<String>, ip: impl Into<String>) -> Self {
2402        Self::new().name(name).record_type("A").value(ip)
2403    }
2404
2405    /// Create an `AAAA` (IPv6 address) record.
2406    pub fn aaaa(name: impl Into<String>, ip: impl Into<String>) -> Self {
2407        Self::new().name(name).record_type("AAAA").value(ip)
2408    }
2409
2410    /// Create a `CNAME` record.
2411    pub fn cname(name: impl Into<String>, target: impl Into<String>) -> Self {
2412        Self::new().name(name).record_type("CNAME").value(target)
2413    }
2414
2415    /// Create a `TXT` record.
2416    pub fn txt(name: impl Into<String>, text: impl Into<String>) -> Self {
2417        Self::new().name(name).record_type("TXT").value(text)
2418    }
2419
2420    /// Set the record name.
2421    pub fn name(mut self, name: impl Into<String>) -> Self {
2422        self.name = Some(name.into());
2423        self
2424    }
2425
2426    /// Set the record type (e.g. "A", "AAAA", "CNAME", "MX", "SRV", "TXT", "PTR").
2427    pub fn record_type(mut self, record_type: impl Into<String>) -> Self {
2428        self.record_type = Some(record_type.into());
2429        self
2430    }
2431
2432    /// Set the DNS class (e.g. "IN", "CH", "HS", "ANY").
2433    pub fn dns_class(mut self, dns_class: impl Into<String>) -> Self {
2434        self.dns_class = Some(dns_class.into());
2435        self
2436    }
2437
2438    /// Set the time-to-live in seconds.
2439    pub fn ttl(mut self, ttl: u32) -> Self {
2440        self.ttl = Some(ttl);
2441        self
2442    }
2443
2444    /// Set the record value (address, target, text, etc.).
2445    pub fn value(mut self, value: impl Into<String>) -> Self {
2446        self.value = Some(value.into());
2447        self
2448    }
2449
2450    /// Set the priority (MX/SRV).
2451    pub fn priority(mut self, priority: u32) -> Self {
2452        self.priority = Some(priority);
2453        self
2454    }
2455
2456    /// Set the weight (SRV).
2457    pub fn weight(mut self, weight: u32) -> Self {
2458        self.weight = Some(weight);
2459        self
2460    }
2461
2462    /// Set the port (SRV).
2463    pub fn port(mut self, port: u16) -> Self {
2464        self.port = Some(port);
2465        self
2466    }
2467}
2468
2469/// Builder for a DNS response action.
2470///
2471/// Serialized as the `dnsResponse` action in an expectation.
2472///
2473/// # Example
2474/// ```
2475/// use mockserver_client::{DnsResponse, DnsRecord};
2476///
2477/// let dns = DnsResponse::new()
2478///     .response_code("NOERROR")
2479///     .answer_record(DnsRecord::a("example.com", "1.2.3.4").ttl(300));
2480/// ```
2481#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2482#[serde(rename_all = "camelCase")]
2483pub struct DnsResponse {
2484    #[serde(skip_serializing_if = "Option::is_none")]
2485    pub answer_records: Option<Vec<DnsRecord>>,
2486
2487    #[serde(skip_serializing_if = "Option::is_none")]
2488    pub authority_records: Option<Vec<DnsRecord>>,
2489
2490    #[serde(skip_serializing_if = "Option::is_none")]
2491    pub additional_records: Option<Vec<DnsRecord>>,
2492
2493    #[serde(skip_serializing_if = "Option::is_none")]
2494    pub response_code: Option<String>,
2495
2496    #[serde(skip_serializing_if = "Option::is_none")]
2497    pub delay: Option<Delay>,
2498
2499    #[serde(skip_serializing_if = "Option::is_none")]
2500    pub primary: Option<bool>,
2501
2502    /// Catch-all for server fields this client does not model yet, so a
2503    /// retrieve -> re-submit cycle cannot silently destroy them.
2504    #[serde(flatten, default)]
2505    pub extra: Extra,
2506}
2507
2508impl DnsResponse {
2509    /// Create a new empty DNS response.
2510    pub fn new() -> Self {
2511        Self::default()
2512    }
2513
2514    /// Append an answer-section record.
2515    pub fn answer_record(mut self, record: DnsRecord) -> Self {
2516        self.answer_records
2517            .get_or_insert_with(Vec::new)
2518            .push(record);
2519        self
2520    }
2521
2522    /// Replace all answer-section records.
2523    pub fn answer_records(mut self, records: Vec<DnsRecord>) -> Self {
2524        self.answer_records = Some(records);
2525        self
2526    }
2527
2528    /// Append an authority-section record.
2529    pub fn authority_record(mut self, record: DnsRecord) -> Self {
2530        self.authority_records
2531            .get_or_insert_with(Vec::new)
2532            .push(record);
2533        self
2534    }
2535
2536    /// Append an additional-section record.
2537    pub fn additional_record(mut self, record: DnsRecord) -> Self {
2538        self.additional_records
2539            .get_or_insert_with(Vec::new)
2540            .push(record);
2541        self
2542    }
2543
2544    /// Set the DNS response code (e.g. "NOERROR", "NXDOMAIN", "SERVFAIL").
2545    pub fn response_code(mut self, code: impl Into<String>) -> Self {
2546        self.response_code = Some(code.into());
2547        self
2548    }
2549
2550    /// Set a delay before the response is returned.
2551    pub fn delay(mut self, delay: Delay) -> Self {
2552        self.delay = Some(delay);
2553        self
2554    }
2555
2556    /// Mark this action as the primary action of the expectation.
2557    pub fn primary(mut self, primary: bool) -> Self {
2558        self.primary = Some(primary);
2559        self
2560    }
2561}
2562
2563// ---------------------------------------------------------------------------
2564// BinaryResponse
2565// ---------------------------------------------------------------------------
2566
2567/// Builder for a raw binary response action.
2568///
2569/// Serialized as the `binaryResponse` action in an expectation. The binary
2570/// payload is base64-encoded on the wire (the schema declares `binaryData`
2571/// as a string).
2572///
2573/// # Example
2574/// ```
2575/// use mockserver_client::BinaryResponse;
2576///
2577/// let resp = BinaryResponse::from_bytes([0xDE, 0xAD, 0xBE, 0xEF]);
2578/// ```
2579#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2580#[serde(rename_all = "camelCase")]
2581pub struct BinaryResponse {
2582    #[serde(skip_serializing_if = "Option::is_none")]
2583    pub binary_data: Option<String>,
2584
2585    #[serde(skip_serializing_if = "Option::is_none")]
2586    pub delay: Option<Delay>,
2587
2588    #[serde(skip_serializing_if = "Option::is_none")]
2589    pub primary: Option<bool>,
2590
2591    /// Catch-all for server fields this client does not model yet, so a
2592    /// retrieve -> re-submit cycle cannot silently destroy them.
2593    #[serde(flatten, default)]
2594    pub extra: Extra,
2595}
2596
2597impl BinaryResponse {
2598    /// Create a new empty binary response.
2599    pub fn new() -> Self {
2600        Self::default()
2601    }
2602
2603    /// Create a binary response from raw bytes (base64-encoded on the wire).
2604    pub fn from_bytes(data: impl AsRef<[u8]>) -> Self {
2605        Self {
2606            binary_data: Some(BASE64.encode(data.as_ref())),
2607            delay: None,
2608            ..Default::default()
2609        }
2610    }
2611
2612    /// Create a binary response from an already base64-encoded string.
2613    pub fn from_base64(base64: impl Into<String>) -> Self {
2614        Self {
2615            binary_data: Some(base64.into()),
2616            delay: None,
2617            ..Default::default()
2618        }
2619    }
2620
2621    /// Set the binary payload from raw bytes (base64-encoded on the wire).
2622    pub fn binary_data(mut self, data: impl AsRef<[u8]>) -> Self {
2623        self.binary_data = Some(BASE64.encode(data.as_ref()));
2624        self
2625    }
2626
2627    /// Set a delay before the response is returned.
2628    pub fn delay(mut self, delay: Delay) -> Self {
2629        self.delay = Some(delay);
2630        self
2631    }
2632
2633    /// Mark this action as the primary action of the expectation.
2634    pub fn primary(mut self, primary: bool) -> Self {
2635        self.primary = Some(primary);
2636        self
2637    }
2638}
2639
2640// ---------------------------------------------------------------------------
2641// GrpcStreamResponse
2642// ---------------------------------------------------------------------------
2643
2644/// A single gRPC stream message in a [`GrpcStreamResponse`].
2645#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2646#[serde(rename_all = "camelCase")]
2647pub struct GrpcStreamMessage {
2648    #[serde(skip_serializing_if = "Option::is_none")]
2649    pub json: Option<String>,
2650
2651    /// Template engine used to render the message (`"VELOCITY"`, `"JAVASCRIPT"`
2652    /// or `"MUSTACHE"`); when unset the `json` is sent verbatim.
2653    #[serde(skip_serializing_if = "Option::is_none")]
2654    pub template_type: Option<String>,
2655
2656    #[serde(skip_serializing_if = "Option::is_none")]
2657    pub delay: Option<Delay>,
2658}
2659
2660impl GrpcStreamMessage {
2661    /// Create a gRPC stream message from a JSON-encoded protobuf message string.
2662    pub fn json(json: impl Into<String>) -> Self {
2663        Self {
2664            json: Some(json.into()),
2665            template_type: None,
2666            delay: None,
2667        }
2668    }
2669
2670    /// Set the template engine used to render this message.
2671    pub fn template_type(mut self, template_type: impl Into<String>) -> Self {
2672        self.template_type = Some(template_type.into());
2673        self
2674    }
2675
2676    /// Set a delay before this message is sent.
2677    pub fn delay(mut self, delay: Delay) -> Self {
2678        self.delay = Some(delay);
2679        self
2680    }
2681}
2682
2683/// Builder for a gRPC streaming response action.
2684///
2685/// Serialized as the `grpcStreamResponse` action in an expectation.
2686///
2687/// # Example
2688/// ```
2689/// use mockserver_client::{GrpcStreamResponse, GrpcStreamMessage};
2690///
2691/// let grpc = GrpcStreamResponse::new()
2692///     .status_name("OK")
2693///     .message(GrpcStreamMessage::json("{\"id\":1}"))
2694///     .close_connection(true);
2695/// ```
2696#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2697#[serde(rename_all = "camelCase")]
2698pub struct GrpcStreamResponse {
2699    #[serde(skip_serializing_if = "Option::is_none")]
2700    pub status_name: Option<String>,
2701
2702    #[serde(skip_serializing_if = "Option::is_none")]
2703    pub status_message: Option<String>,
2704
2705    #[serde(skip_serializing_if = "Option::is_none")]
2706    pub headers: Option<HashMap<String, Vec<String>>>,
2707
2708    #[serde(skip_serializing_if = "Option::is_none")]
2709    pub messages: Option<Vec<GrpcStreamMessage>>,
2710
2711    #[serde(skip_serializing_if = "Option::is_none")]
2712    pub close_connection: Option<bool>,
2713
2714    #[serde(skip_serializing_if = "Option::is_none")]
2715    pub delay: Option<Delay>,
2716
2717    #[serde(skip_serializing_if = "Option::is_none")]
2718    pub primary: Option<bool>,
2719
2720    /// Catch-all for server fields this client does not model yet, so a
2721    /// retrieve -> re-submit cycle cannot silently destroy them.
2722    #[serde(flatten, default)]
2723    pub extra: Extra,
2724}
2725
2726impl GrpcStreamResponse {
2727    /// Create a new empty gRPC stream response.
2728    pub fn new() -> Self {
2729        Self::default()
2730    }
2731
2732    /// Set the gRPC status name (e.g. "OK", "NOT_FOUND").
2733    pub fn status_name(mut self, status_name: impl Into<String>) -> Self {
2734        self.status_name = Some(status_name.into());
2735        self
2736    }
2737
2738    /// Set the gRPC status message.
2739    pub fn status_message(mut self, status_message: impl Into<String>) -> Self {
2740        self.status_message = Some(status_message.into());
2741        self
2742    }
2743
2744    /// Add a response header (gRPC metadata).
2745    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
2746        let headers = self.headers.get_or_insert_with(HashMap::new);
2747        headers.entry(key.into()).or_default().push(value.into());
2748        self
2749    }
2750
2751    /// Append a gRPC stream message.
2752    pub fn message(mut self, message: GrpcStreamMessage) -> Self {
2753        self.messages.get_or_insert_with(Vec::new).push(message);
2754        self
2755    }
2756
2757    /// Replace all gRPC stream messages.
2758    pub fn messages(mut self, messages: Vec<GrpcStreamMessage>) -> Self {
2759        self.messages = Some(messages);
2760        self
2761    }
2762
2763    /// Whether to close the stream after emitting all messages.
2764    pub fn close_connection(mut self, close: bool) -> Self {
2765        self.close_connection = Some(close);
2766        self
2767    }
2768
2769    /// Set a delay before the response starts.
2770    pub fn delay(mut self, delay: Delay) -> Self {
2771        self.delay = Some(delay);
2772        self
2773    }
2774
2775    /// Mark this action as the primary action of the expectation.
2776    pub fn primary(mut self, primary: bool) -> Self {
2777        self.primary = Some(primary);
2778        self
2779    }
2780}
2781
2782// ---------------------------------------------------------------------------
2783// OpenApiExpectation
2784// ---------------------------------------------------------------------------
2785
2786/// An OpenAPI specification import — registers matchers and example responses
2787/// for the operations in an OpenAPI/Swagger spec.
2788///
2789/// Sent via `PUT /mockserver/openapi`. The spec may be a URL, a filesystem
2790/// path (`file://...`), a classpath resource, or an inline JSON/YAML payload.
2791///
2792/// # Example
2793/// ```
2794/// use mockserver_client::OpenApiExpectation;
2795///
2796/// let expectation = OpenApiExpectation::new(
2797///     "https://example.com/petstore.yaml",
2798/// )
2799/// .operation("listPets", "200")
2800/// .operation("showPetById", "200");
2801/// ```
2802#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2803#[serde(rename_all = "camelCase")]
2804pub struct OpenApiExpectation {
2805    pub spec_url_or_payload: String,
2806
2807    #[serde(skip_serializing_if = "Option::is_none")]
2808    pub operations_and_responses: Option<HashMap<String, String>>,
2809
2810    #[serde(skip_serializing_if = "Option::is_none")]
2811    pub context_path_prefix: Option<String>,
2812}
2813
2814impl OpenApiExpectation {
2815    /// Create an OpenAPI import from a spec URL, file path, classpath resource,
2816    /// or inline JSON/YAML payload.
2817    pub fn new(spec_url_or_payload: impl Into<String>) -> Self {
2818        Self {
2819            spec_url_or_payload: spec_url_or_payload.into(),
2820            operations_and_responses: None,
2821            context_path_prefix: None,
2822        }
2823    }
2824
2825    /// Map an `operationId` to the status code (or example name) to respond with.
2826    ///
2827    /// When no operations are specified, MockServer creates example responses
2828    /// for every operation in the spec.
2829    pub fn operation(
2830        mut self,
2831        operation_id: impl Into<String>,
2832        status_code: impl Into<String>,
2833    ) -> Self {
2834        self.operations_and_responses
2835            .get_or_insert_with(HashMap::new)
2836            .insert(operation_id.into(), status_code.into());
2837        self
2838    }
2839
2840    /// Replace the full operations-to-responses map.
2841    pub fn operations_and_responses(mut self, map: HashMap<String, String>) -> Self {
2842        self.operations_and_responses = Some(map);
2843        self
2844    }
2845
2846    /// Set a context-path prefix to prepend to every generated matcher path.
2847    pub fn context_path_prefix(mut self, prefix: impl Into<String>) -> Self {
2848        self.context_path_prefix = Some(prefix.into());
2849        self
2850    }
2851}
2852
2853// ---------------------------------------------------------------------------
2854// Delay
2855// ---------------------------------------------------------------------------
2856
2857/// A time delay (e.g., for response delays).
2858#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2859#[serde(rename_all = "camelCase")]
2860pub struct Delay {
2861    pub time_unit: String,
2862    pub value: u64,
2863}
2864
2865impl Delay {
2866    /// Create a delay in milliseconds.
2867    pub fn milliseconds(value: u64) -> Self {
2868        Self {
2869            time_unit: "MILLISECONDS".to_string(),
2870            value,
2871        }
2872    }
2873
2874    /// Create a delay in seconds.
2875    pub fn seconds(value: u64) -> Self {
2876        Self {
2877            time_unit: "SECONDS".to_string(),
2878            value,
2879        }
2880    }
2881}
2882
2883// ---------------------------------------------------------------------------
2884// Times
2885// ---------------------------------------------------------------------------
2886
2887/// How many times an expectation should be matched.
2888#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2889#[serde(rename_all = "camelCase")]
2890pub struct Times {
2891    #[serde(skip_serializing_if = "Option::is_none")]
2892    pub remaining_times: Option<u32>,
2893
2894    #[serde(default)]
2895    pub unlimited: bool,
2896}
2897
2898impl Times {
2899    /// Match unlimited times.
2900    pub fn unlimited() -> Self {
2901        Self {
2902            remaining_times: None,
2903            unlimited: true,
2904        }
2905    }
2906
2907    /// Match exactly `n` times.
2908    pub fn exactly(n: u32) -> Self {
2909        Self {
2910            remaining_times: Some(n),
2911            unlimited: false,
2912        }
2913    }
2914
2915    /// Match once.
2916    pub fn once() -> Self {
2917        Self::exactly(1)
2918    }
2919}
2920
2921// ---------------------------------------------------------------------------
2922// TimeToLive
2923// ---------------------------------------------------------------------------
2924
2925/// How long an expectation remains active.
2926#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2927#[serde(rename_all = "camelCase")]
2928pub struct TimeToLive {
2929    #[serde(skip_serializing_if = "Option::is_none")]
2930    pub time_unit: Option<String>,
2931
2932    #[serde(skip_serializing_if = "Option::is_none")]
2933    pub time_to_live: Option<u64>,
2934
2935    #[serde(default)]
2936    pub unlimited: bool,
2937}
2938
2939impl TimeToLive {
2940    /// Unlimited TTL (never expires).
2941    pub fn unlimited() -> Self {
2942        Self {
2943            time_unit: None,
2944            time_to_live: None,
2945            unlimited: true,
2946        }
2947    }
2948
2949    /// Expire after the given number of seconds.
2950    pub fn seconds(seconds: u64) -> Self {
2951        Self {
2952            time_unit: Some("SECONDS".to_string()),
2953            time_to_live: Some(seconds),
2954            unlimited: false,
2955        }
2956    }
2957
2958    /// Expire after the given number of milliseconds.
2959    pub fn milliseconds(millis: u64) -> Self {
2960        Self {
2961            time_unit: Some("MILLISECONDS".to_string()),
2962            time_to_live: Some(millis),
2963            unlimited: false,
2964        }
2965    }
2966}
2967
2968// ---------------------------------------------------------------------------
2969// VerificationTimes
2970// ---------------------------------------------------------------------------
2971
2972/// Verification constraints — how many times a request must have been received.
2973///
2974/// On the wire both `atLeast` and `atMost` are ALWAYS sent, using `-1` to mean
2975/// "unbounded". The MockServer server deserializes these into primitive `int`
2976/// fields, so an omitted bound defaults to `0` server-side — which would turn
2977/// `at_least(n)` into an impossible `between(n, 0)` constraint. Emitting the
2978/// explicit `-1` sentinel (matching the Java client) avoids that.
2979#[derive(Debug, Clone, Deserialize, PartialEq)]
2980#[serde(rename_all = "camelCase")]
2981pub struct VerificationTimes {
2982    pub at_least: Option<u32>,
2983    pub at_most: Option<u32>,
2984}
2985
2986impl Serialize for VerificationTimes {
2987    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2988    where
2989        S: serde::Serializer,
2990    {
2991        use serde::ser::SerializeStruct;
2992        let mut state = serializer.serialize_struct("VerificationTimes", 2)?;
2993        state.serialize_field("atLeast", &self.at_least.map_or(-1_i64, i64::from))?;
2994        state.serialize_field("atMost", &self.at_most.map_or(-1_i64, i64::from))?;
2995        state.end()
2996    }
2997}
2998
2999impl VerificationTimes {
3000    /// Require at least `n` matching requests.
3001    pub fn at_least(n: u32) -> Self {
3002        Self {
3003            at_least: Some(n),
3004            at_most: None,
3005        }
3006    }
3007
3008    /// Require at most `n` matching requests.
3009    pub fn at_most(n: u32) -> Self {
3010        Self {
3011            at_least: None,
3012            at_most: Some(n),
3013        }
3014    }
3015
3016    /// Require exactly `n` matching requests.
3017    pub fn exactly(n: u32) -> Self {
3018        Self {
3019            at_least: Some(n),
3020            at_most: Some(n),
3021        }
3022    }
3023
3024    /// Require between `min` and `max` matching requests (inclusive).
3025    pub fn between(min: u32, max: u32) -> Self {
3026        Self {
3027            at_least: Some(min),
3028            at_most: Some(max),
3029        }
3030    }
3031}
3032
3033// ---------------------------------------------------------------------------
3034// Stateful scenarios
3035// ---------------------------------------------------------------------------
3036
3037/// How MockServer selects which of an expectation's multiple `http_responses`
3038/// to return on each match. Maps to the `responseMode` field.
3039///
3040/// - `Sequential` (default) — cycle through the responses in order.
3041/// - `Random` — pick a response uniformly at random.
3042/// - `Weighted` — pick a response weighted by the index-aligned
3043///   [`Expectation::response_weights`].
3044/// - `Switch` — return the same response for [`Expectation::switch_after`]
3045///   matches before advancing to the next.
3046#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
3047#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
3048pub enum ResponseMode {
3049    /// Cycle through the responses in order (default).
3050    Sequential,
3051    /// Pick a response uniformly at random.
3052    Random,
3053    /// Pick a response weighted by [`Expectation::response_weights`].
3054    Weighted,
3055    /// Return each response for [`Expectation::switch_after`] matches before advancing.
3056    Switch,
3057}
3058
3059/// The protocol event that triggers a [`CrossProtocolScenario`] state
3060/// transition. Maps to the `trigger` field.
3061#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
3062#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
3063pub enum CrossProtocolTrigger {
3064    /// A DNS query is observed.
3065    DnsQuery,
3066    /// A WebSocket connection is established.
3067    WebsocketConnect,
3068    /// A gRPC request is observed.
3069    GrpcRequest,
3070    /// An HTTP request is observed.
3071    HttpRequest,
3072}
3073
3074/// A cross-protocol scenario correlation: when a protocol event matching
3075/// [`trigger`](Self::trigger) (and optionally [`match_pattern`](Self::match_pattern))
3076/// is observed, the named scenario is advanced to [`target_state`](Self::target_state).
3077///
3078/// Maps to entries of the `crossProtocolScenarios` array.
3079///
3080/// # Example
3081/// ```
3082/// use mockserver_client::{CrossProtocolScenario, CrossProtocolTrigger};
3083///
3084/// let scenario = CrossProtocolScenario::new(
3085///     CrossProtocolTrigger::DnsQuery,
3086///     "Deploy",
3087///     "DnsObserved",
3088/// )
3089/// .match_pattern("api.example.com");
3090/// ```
3091#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3092#[serde(rename_all = "camelCase")]
3093pub struct CrossProtocolScenario {
3094    pub trigger: CrossProtocolTrigger,
3095
3096    #[serde(skip_serializing_if = "Option::is_none")]
3097    pub match_pattern: Option<String>,
3098
3099    pub scenario_name: String,
3100
3101    pub target_state: String,
3102}
3103
3104impl CrossProtocolScenario {
3105    /// Create a cross-protocol scenario for the given trigger that advances
3106    /// `scenario_name` to `target_state` when an event fires.
3107    pub fn new(
3108        trigger: CrossProtocolTrigger,
3109        scenario_name: impl Into<String>,
3110        target_state: impl Into<String>,
3111    ) -> Self {
3112        Self {
3113            trigger,
3114            match_pattern: None,
3115            scenario_name: scenario_name.into(),
3116            target_state: target_state.into(),
3117        }
3118    }
3119
3120    /// Set the substring filter on the event identifier (omit to match all).
3121    pub fn match_pattern(mut self, pattern: impl Into<String>) -> Self {
3122        self.match_pattern = Some(pattern.into());
3123        self
3124    }
3125}
3126
3127// ---------------------------------------------------------------------------
3128// ConnectionOptions / RecoverAfter
3129// ---------------------------------------------------------------------------
3130
3131/// Connection-level options for an [`HttpResponse`] — control content-length,
3132/// chunking, keep-alive and socket-close behaviour.
3133#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
3134#[serde(rename_all = "camelCase")]
3135pub struct ConnectionOptions {
3136    #[serde(skip_serializing_if = "Option::is_none")]
3137    pub suppress_content_length_header: Option<bool>,
3138
3139    #[serde(skip_serializing_if = "Option::is_none")]
3140    pub content_length_header_override: Option<i64>,
3141
3142    #[serde(skip_serializing_if = "Option::is_none")]
3143    pub suppress_connection_header: Option<bool>,
3144
3145    #[serde(skip_serializing_if = "Option::is_none")]
3146    pub chunk_size: Option<i64>,
3147
3148    #[serde(skip_serializing_if = "Option::is_none")]
3149    pub chunk_delay: Option<Delay>,
3150
3151    #[serde(skip_serializing_if = "Option::is_none")]
3152    pub keep_alive_override: Option<bool>,
3153
3154    #[serde(skip_serializing_if = "Option::is_none")]
3155    pub close_socket: Option<bool>,
3156
3157    #[serde(skip_serializing_if = "Option::is_none")]
3158    pub close_socket_delay: Option<Delay>,
3159
3160    #[serde(flatten, default)]
3161    pub extra: Extra,
3162}
3163
3164impl ConnectionOptions {
3165    /// Create a new empty set of connection options.
3166    pub fn new() -> Self {
3167        Self::default()
3168    }
3169}
3170
3171/// Circuit-breaker style policy on an [`HttpResponse`]: fail the first
3172/// `fail_times` requests with `fail_response`, then serve the real response.
3173#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
3174#[serde(rename_all = "camelCase")]
3175pub struct RecoverAfter {
3176    #[serde(skip_serializing_if = "Option::is_none")]
3177    pub fail_times: Option<i64>,
3178
3179    #[serde(skip_serializing_if = "Option::is_none")]
3180    pub fail_response: Option<serde_json::Value>,
3181
3182    #[serde(skip_serializing_if = "Option::is_none")]
3183    pub idempotency_header: Option<String>,
3184
3185    #[serde(flatten, default)]
3186    pub extra: Extra,
3187}
3188
3189impl RecoverAfter {
3190    /// Create a new empty recover-after policy.
3191    pub fn new() -> Self {
3192        Self::default()
3193    }
3194}
3195
3196// ---------------------------------------------------------------------------
3197// RateLimit
3198// ---------------------------------------------------------------------------
3199
3200/// Declarative, protocol-agnostic rate limit / quota attached to an expectation.
3201#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
3202#[serde(rename_all = "camelCase")]
3203pub struct RateLimit {
3204    #[serde(skip_serializing_if = "Option::is_none")]
3205    pub name: Option<String>,
3206
3207    /// `"fixed_window"` (default) or `"token_bucket"`.
3208    #[serde(skip_serializing_if = "Option::is_none")]
3209    pub algorithm: Option<String>,
3210
3211    #[serde(skip_serializing_if = "Option::is_none")]
3212    pub limit: Option<i64>,
3213
3214    #[serde(skip_serializing_if = "Option::is_none")]
3215    pub window_millis: Option<i64>,
3216
3217    #[serde(skip_serializing_if = "Option::is_none")]
3218    pub burst: Option<i64>,
3219
3220    #[serde(skip_serializing_if = "Option::is_none")]
3221    pub refill_per_second: Option<f64>,
3222
3223    #[serde(skip_serializing_if = "Option::is_none")]
3224    pub error_status: Option<i32>,
3225
3226    #[serde(skip_serializing_if = "Option::is_none")]
3227    pub retry_after: Option<String>,
3228
3229    #[serde(flatten, default)]
3230    pub extra: Extra,
3231}
3232
3233impl RateLimit {
3234    /// Create a new empty rate limit.
3235    pub fn new() -> Self {
3236        Self::default()
3237    }
3238
3239    /// Create a `fixed_window` rate limit of `limit` requests per `window_millis`.
3240    pub fn fixed_window(limit: i64, window_millis: i64) -> Self {
3241        Self {
3242            algorithm: Some("fixed_window".to_string()),
3243            limit: Some(limit),
3244            window_millis: Some(window_millis),
3245            ..Default::default()
3246        }
3247    }
3248
3249    /// Create a `token_bucket` rate limit with `burst` capacity refilled at
3250    /// `refill_per_second` tokens per second.
3251    pub fn token_bucket(burst: i64, refill_per_second: f64) -> Self {
3252        Self {
3253            algorithm: Some("token_bucket".to_string()),
3254            burst: Some(burst),
3255            refill_per_second: Some(refill_per_second),
3256            ..Default::default()
3257        }
3258    }
3259}
3260
3261// ---------------------------------------------------------------------------
3262// HttpForwardWithFallback / HttpForwardValidateAction / HttpOverrideForwardedRequest
3263// ---------------------------------------------------------------------------
3264
3265/// Forward action that falls back to a canned response when the upstream fails
3266/// (serialised as `httpForwardWithFallback`).
3267#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3268#[serde(rename_all = "camelCase")]
3269pub struct HttpForwardWithFallback {
3270    pub http_forward: HttpForward,
3271
3272    pub fallback_response: HttpResponse,
3273
3274    #[serde(skip_serializing_if = "Option::is_none")]
3275    pub fallback_on_status_codes: Option<Vec<i32>>,
3276
3277    #[serde(skip_serializing_if = "Option::is_none")]
3278    pub fallback_on_timeout: Option<bool>,
3279
3280    #[serde(skip_serializing_if = "Option::is_none")]
3281    pub delay: Option<Delay>,
3282
3283    #[serde(skip_serializing_if = "Option::is_none")]
3284    pub primary: Option<bool>,
3285
3286    #[serde(flatten, default)]
3287    pub extra: Extra,
3288}
3289
3290impl HttpForwardWithFallback {
3291    /// Create a forward-with-fallback action.
3292    pub fn new(http_forward: HttpForward, fallback_response: HttpResponse) -> Self {
3293        Self {
3294            http_forward,
3295            fallback_response,
3296            fallback_on_status_codes: None,
3297            fallback_on_timeout: None,
3298            delay: None,
3299            primary: None,
3300            extra: Extra::new(),
3301        }
3302    }
3303
3304    /// Fall back when the upstream returns any of these status codes.
3305    pub fn fallback_on_status_codes(mut self, codes: Vec<i32>) -> Self {
3306        self.fallback_on_status_codes = Some(codes);
3307        self
3308    }
3309
3310    /// Fall back when the upstream request times out.
3311    pub fn fallback_on_timeout(mut self, fallback: bool) -> Self {
3312        self.fallback_on_timeout = Some(fallback);
3313        self
3314    }
3315}
3316
3317/// Forward action that also validates request/response against an OpenAPI spec
3318/// (serialised as `httpForwardValidateAction`).
3319#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3320#[serde(rename_all = "camelCase")]
3321pub struct HttpForwardValidateAction {
3322    pub spec_url_or_payload: String,
3323
3324    pub host: String,
3325
3326    #[serde(skip_serializing_if = "Option::is_none")]
3327    pub port: Option<u16>,
3328
3329    #[serde(skip_serializing_if = "Option::is_none")]
3330    pub scheme: Option<String>,
3331
3332    #[serde(skip_serializing_if = "Option::is_none")]
3333    pub validate_request: Option<bool>,
3334
3335    #[serde(skip_serializing_if = "Option::is_none")]
3336    pub validate_response: Option<bool>,
3337
3338    /// `"STRICT"` or `"LOG_ONLY"`.
3339    #[serde(skip_serializing_if = "Option::is_none")]
3340    pub validation_mode: Option<String>,
3341
3342    #[serde(skip_serializing_if = "Option::is_none")]
3343    pub delay: Option<Delay>,
3344
3345    #[serde(skip_serializing_if = "Option::is_none")]
3346    pub primary: Option<bool>,
3347
3348    #[serde(flatten, default)]
3349    pub extra: Extra,
3350}
3351
3352impl HttpForwardValidateAction {
3353    /// Create a forward-and-validate action against the given spec and host.
3354    pub fn new(spec_url_or_payload: impl Into<String>, host: impl Into<String>) -> Self {
3355        Self {
3356            spec_url_or_payload: spec_url_or_payload.into(),
3357            host: host.into(),
3358            port: None,
3359            scheme: None,
3360            validate_request: None,
3361            validate_response: None,
3362            validation_mode: None,
3363            delay: None,
3364            primary: None,
3365            extra: Extra::new(),
3366        }
3367    }
3368}
3369
3370/// Override the forwarded request and/or response (serialised as
3371/// `httpOverrideForwardedRequest`).
3372///
3373/// Covers both wire shapes accepted by the server: the modern
3374/// `requestOverride`/`requestModifier`/`responseOverride`/`responseModifier`
3375/// form and the legacy `httpRequest`/`httpResponse` form. The `requestModifier`
3376/// and `responseModifier` sub-objects are kept as free-form JSON
3377/// ([`serde_json::Value`]) — they round-trip exactly, and the `extra` catch-all
3378/// preserves any other field.
3379#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
3380#[serde(rename_all = "camelCase")]
3381pub struct HttpOverrideForwardedRequest {
3382    #[serde(skip_serializing_if = "Option::is_none")]
3383    pub delay: Option<Delay>,
3384
3385    #[serde(skip_serializing_if = "Option::is_none")]
3386    pub request_override: Option<HttpRequest>,
3387
3388    #[serde(skip_serializing_if = "Option::is_none")]
3389    pub request_modifier: Option<serde_json::Value>,
3390
3391    #[serde(skip_serializing_if = "Option::is_none")]
3392    pub response_override: Option<HttpResponse>,
3393
3394    #[serde(skip_serializing_if = "Option::is_none")]
3395    pub response_modifier: Option<serde_json::Value>,
3396
3397    #[serde(skip_serializing_if = "Option::is_none")]
3398    pub response_template: Option<HttpTemplate>,
3399
3400    /// Legacy shape: request to forward.
3401    #[serde(skip_serializing_if = "Option::is_none")]
3402    pub http_request: Option<HttpRequest>,
3403
3404    /// Legacy shape: response to return.
3405    #[serde(skip_serializing_if = "Option::is_none")]
3406    pub http_response: Option<HttpResponse>,
3407
3408    #[serde(skip_serializing_if = "Option::is_none")]
3409    pub primary: Option<bool>,
3410
3411    #[serde(flatten, default)]
3412    pub extra: Extra,
3413}
3414
3415impl HttpOverrideForwardedRequest {
3416    /// Create a new empty override action.
3417    pub fn new() -> Self {
3418        Self::default()
3419    }
3420
3421    /// Set the request override.
3422    pub fn request_override(mut self, request: HttpRequest) -> Self {
3423        self.request_override = Some(request);
3424        self
3425    }
3426
3427    /// Set the response override.
3428    pub fn response_override(mut self, response: HttpResponse) -> Self {
3429        self.response_override = Some(response);
3430        self
3431    }
3432}
3433
3434// ---------------------------------------------------------------------------
3435// ExpectationAction (before/after) / CaptureRule / ExpectationStep
3436// ---------------------------------------------------------------------------
3437
3438/// A side-effect action run before (`beforeActions`) or after (`afterActions`)
3439/// an expectation's main action fires: an out-of-band request, or a class/object
3440/// callback.
3441#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
3442#[serde(rename_all = "camelCase")]
3443pub struct ExpectationAction {
3444    #[serde(skip_serializing_if = "Option::is_none")]
3445    pub http_request: Option<HttpRequest>,
3446
3447    #[serde(skip_serializing_if = "Option::is_none")]
3448    pub http_class_callback: Option<HttpClassCallback>,
3449
3450    #[serde(skip_serializing_if = "Option::is_none")]
3451    pub http_object_callback: Option<HttpObjectCallback>,
3452
3453    #[serde(skip_serializing_if = "Option::is_none")]
3454    pub delay: Option<Delay>,
3455
3456    #[serde(skip_serializing_if = "Option::is_none")]
3457    pub blocking: Option<bool>,
3458
3459    #[serde(skip_serializing_if = "Option::is_none")]
3460    pub timeout: Option<Delay>,
3461
3462    /// `"FAIL_FAST"` or `"BEST_EFFORT"`.
3463    #[serde(skip_serializing_if = "Option::is_none")]
3464    pub failure_policy: Option<String>,
3465
3466    #[serde(flatten, default)]
3467    pub extra: Extra,
3468}
3469
3470impl ExpectationAction {
3471    /// Create a before/after action that fires an out-of-band HTTP request.
3472    pub fn request(request: HttpRequest) -> Self {
3473        Self {
3474            http_request: Some(request),
3475            ..Default::default()
3476        }
3477    }
3478
3479    /// Create a before/after action that invokes a server-side class callback.
3480    pub fn class_callback(callback: HttpClassCallback) -> Self {
3481        Self {
3482            http_class_callback: Some(callback),
3483            ..Default::default()
3484        }
3485    }
3486}
3487
3488/// A capture rule (`capture`) — extract a value from the matched request and
3489/// bind it into scenario/template state under `into`.
3490#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3491#[serde(rename_all = "camelCase")]
3492pub struct CaptureRule {
3493    /// One of `jsonPath`, `xpath`, `header`, `queryStringParameter`, `cookie`,
3494    /// `pathParameter`.
3495    pub source: String,
3496
3497    pub expression: String,
3498
3499    pub into: String,
3500
3501    #[serde(flatten, default)]
3502    pub extra: Extra,
3503}
3504
3505impl CaptureRule {
3506    /// Create a capture rule binding `expression` (evaluated against `source`)
3507    /// into the variable `into`.
3508    pub fn new(
3509        source: impl Into<String>,
3510        expression: impl Into<String>,
3511        into: impl Into<String>,
3512    ) -> Self {
3513        Self {
3514            source: source.into(),
3515            expression: expression.into(),
3516            into: into.into(),
3517            extra: Extra::new(),
3518        }
3519    }
3520}
3521
3522/// One step of a multi-step expectation (`steps`) — used to script a sequence of
3523/// responder/side-effect actions for a single match.
3524#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
3525#[serde(rename_all = "camelCase")]
3526pub struct ExpectationStep {
3527    #[serde(skip_serializing_if = "Option::is_none")]
3528    pub http_request: Option<HttpRequest>,
3529
3530    #[serde(skip_serializing_if = "Option::is_none")]
3531    pub http_class_callback: Option<HttpClassCallback>,
3532
3533    #[serde(skip_serializing_if = "Option::is_none")]
3534    pub http_object_callback: Option<HttpObjectCallback>,
3535
3536    #[serde(skip_serializing_if = "Option::is_none")]
3537    pub http_forward: Option<HttpForward>,
3538
3539    #[serde(skip_serializing_if = "Option::is_none")]
3540    pub http_override_forwarded_request: Option<HttpOverrideForwardedRequest>,
3541
3542    #[serde(skip_serializing_if = "Option::is_none")]
3543    pub http_response: Option<HttpResponse>,
3544
3545    #[serde(skip_serializing_if = "Option::is_none")]
3546    pub http_error: Option<HttpError>,
3547
3548    #[serde(skip_serializing_if = "Option::is_none")]
3549    pub responder: Option<bool>,
3550
3551    #[serde(skip_serializing_if = "Option::is_none")]
3552    pub delay: Option<Delay>,
3553
3554    #[serde(skip_serializing_if = "Option::is_none")]
3555    pub blocking: Option<bool>,
3556
3557    #[serde(skip_serializing_if = "Option::is_none")]
3558    pub timeout: Option<Delay>,
3559
3560    /// `"FAIL_FAST"` or `"BEST_EFFORT"`.
3561    #[serde(skip_serializing_if = "Option::is_none")]
3562    pub failure_policy: Option<String>,
3563
3564    #[serde(flatten, default)]
3565    pub extra: Extra,
3566}
3567
3568impl ExpectationStep {
3569    /// Create a new empty step.
3570    pub fn new() -> Self {
3571        Self::default()
3572    }
3573
3574    /// Create a step whose responder action is the given response.
3575    pub fn response(response: HttpResponse) -> Self {
3576        Self {
3577            http_response: Some(response),
3578            responder: Some(true),
3579            ..Default::default()
3580        }
3581    }
3582}
3583
3584// ---------------------------------------------------------------------------
3585// GrpcBidiResponse
3586// ---------------------------------------------------------------------------
3587
3588/// A single message in a [`GrpcBidiResponse`] (or one of its rule responses).
3589#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
3590#[serde(rename_all = "camelCase")]
3591pub struct GrpcBidiMessage {
3592    #[serde(skip_serializing_if = "Option::is_none")]
3593    pub json: Option<String>,
3594
3595    /// `"VELOCITY"`, `"JAVASCRIPT"` or `"MUSTACHE"`.
3596    #[serde(skip_serializing_if = "Option::is_none")]
3597    pub template_type: Option<String>,
3598
3599    #[serde(skip_serializing_if = "Option::is_none")]
3600    pub delay: Option<Delay>,
3601
3602    #[serde(flatten, default)]
3603    pub extra: Extra,
3604}
3605
3606impl GrpcBidiMessage {
3607    /// Create a bidi message from a JSON-encoded protobuf message string.
3608    pub fn json(json: impl Into<String>) -> Self {
3609        Self {
3610            json: Some(json.into()),
3611            ..Default::default()
3612        }
3613    }
3614}
3615
3616/// A request-keyed rule in a [`GrpcBidiResponse`] — when an incoming message
3617/// matches `match_json`, the paired `responses` are sent.
3618#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
3619#[serde(rename_all = "camelCase")]
3620pub struct GrpcBidiRule {
3621    #[serde(skip_serializing_if = "Option::is_none")]
3622    pub match_json: Option<String>,
3623
3624    #[serde(skip_serializing_if = "Option::is_none")]
3625    pub responses: Option<Vec<GrpcBidiMessage>>,
3626
3627    #[serde(flatten, default)]
3628    pub extra: Extra,
3629}
3630
3631/// A gRPC bidirectional-streaming response action (`grpcBidiResponse`).
3632#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
3633#[serde(rename_all = "camelCase")]
3634pub struct GrpcBidiResponse {
3635    #[serde(skip_serializing_if = "Option::is_none")]
3636    pub status_name: Option<String>,
3637
3638    #[serde(skip_serializing_if = "Option::is_none")]
3639    pub status_message: Option<String>,
3640
3641    #[serde(skip_serializing_if = "Option::is_none")]
3642    pub headers: Option<HashMap<String, Vec<String>>>,
3643
3644    #[serde(skip_serializing_if = "Option::is_none")]
3645    pub messages: Option<Vec<GrpcBidiMessage>>,
3646
3647    #[serde(skip_serializing_if = "Option::is_none")]
3648    pub rules: Option<Vec<GrpcBidiRule>>,
3649
3650    #[serde(skip_serializing_if = "Option::is_none")]
3651    pub close_connection: Option<bool>,
3652
3653    #[serde(skip_serializing_if = "Option::is_none")]
3654    pub delay: Option<Delay>,
3655
3656    #[serde(skip_serializing_if = "Option::is_none")]
3657    pub primary: Option<bool>,
3658
3659    #[serde(flatten, default)]
3660    pub extra: Extra,
3661}
3662
3663impl GrpcBidiResponse {
3664    /// Create a new empty gRPC bidi response.
3665    pub fn new() -> Self {
3666        Self::default()
3667    }
3668
3669    /// Append a streamed message.
3670    pub fn message(mut self, message: GrpcBidiMessage) -> Self {
3671        self.messages.get_or_insert_with(Vec::new).push(message);
3672        self
3673    }
3674
3675    /// Append a request-keyed rule.
3676    pub fn rule(mut self, rule: GrpcBidiRule) -> Self {
3677        self.rules.get_or_insert_with(Vec::new).push(rule);
3678        self
3679    }
3680}
3681
3682// ---------------------------------------------------------------------------
3683// HttpLlmResponse
3684// ---------------------------------------------------------------------------
3685
3686/// A single tool call in an [`LlmCompletion`].
3687#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
3688#[serde(rename_all = "camelCase")]
3689pub struct LlmToolCall {
3690    #[serde(skip_serializing_if = "Option::is_none")]
3691    pub id: Option<String>,
3692
3693    #[serde(skip_serializing_if = "Option::is_none")]
3694    pub name: Option<String>,
3695
3696    #[serde(skip_serializing_if = "Option::is_none")]
3697    pub arguments: Option<String>,
3698
3699    #[serde(flatten, default)]
3700    pub extra: Extra,
3701}
3702
3703/// Token-usage accounting for an [`LlmCompletion`].
3704#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
3705#[serde(rename_all = "camelCase")]
3706pub struct LlmUsage {
3707    #[serde(skip_serializing_if = "Option::is_none")]
3708    pub input_tokens: Option<i64>,
3709
3710    #[serde(skip_serializing_if = "Option::is_none")]
3711    pub output_tokens: Option<i64>,
3712
3713    #[serde(skip_serializing_if = "Option::is_none")]
3714    pub cached_input_tokens: Option<i64>,
3715
3716    #[serde(skip_serializing_if = "Option::is_none")]
3717    pub cache_creation_tokens: Option<i64>,
3718
3719    #[serde(skip_serializing_if = "Option::is_none")]
3720    pub reasoning_tokens: Option<i64>,
3721
3722    #[serde(flatten, default)]
3723    pub extra: Extra,
3724}
3725
3726/// Streaming timing model (physics) for an [`LlmCompletion`].
3727#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
3728#[serde(rename_all = "camelCase")]
3729pub struct LlmStreamingPhysics {
3730    #[serde(skip_serializing_if = "Option::is_none")]
3731    pub time_to_first_token: Option<Delay>,
3732
3733    #[serde(skip_serializing_if = "Option::is_none")]
3734    pub tokens_per_second: Option<i32>,
3735
3736    #[serde(skip_serializing_if = "Option::is_none")]
3737    pub jitter: Option<f64>,
3738
3739    #[serde(skip_serializing_if = "Option::is_none")]
3740    pub seed: Option<i64>,
3741
3742    #[serde(skip_serializing_if = "Option::is_none")]
3743    pub subword_streaming: Option<bool>,
3744
3745    #[serde(flatten, default)]
3746    pub extra: Extra,
3747}
3748
3749/// The chat/text completion of an [`HttpLlmResponse`].
3750#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
3751#[serde(rename_all = "camelCase")]
3752pub struct LlmCompletion {
3753    #[serde(skip_serializing_if = "Option::is_none")]
3754    pub text: Option<String>,
3755
3756    #[serde(skip_serializing_if = "Option::is_none")]
3757    pub tool_calls: Option<Vec<LlmToolCall>>,
3758
3759    #[serde(skip_serializing_if = "Option::is_none")]
3760    pub stop_reason: Option<String>,
3761
3762    #[serde(skip_serializing_if = "Option::is_none")]
3763    pub usage: Option<LlmUsage>,
3764
3765    #[serde(skip_serializing_if = "Option::is_none")]
3766    pub streaming: Option<bool>,
3767
3768    #[serde(skip_serializing_if = "Option::is_none")]
3769    pub output_schema: Option<String>,
3770
3771    #[serde(skip_serializing_if = "Option::is_none")]
3772    pub enforce_output_schema: Option<bool>,
3773
3774    #[serde(skip_serializing_if = "Option::is_none")]
3775    pub tool_choice: Option<String>,
3776
3777    #[serde(skip_serializing_if = "Option::is_none")]
3778    pub reasoning_text: Option<String>,
3779
3780    #[serde(skip_serializing_if = "Option::is_none")]
3781    pub reasoning_signature: Option<String>,
3782
3783    #[serde(skip_serializing_if = "Option::is_none")]
3784    pub model: Option<String>,
3785
3786    #[serde(skip_serializing_if = "Option::is_none")]
3787    pub streaming_physics: Option<LlmStreamingPhysics>,
3788
3789    #[serde(flatten, default)]
3790    pub extra: Extra,
3791}
3792
3793impl LlmCompletion {
3794    /// Create a completion with the given assistant text.
3795    pub fn text(text: impl Into<String>) -> Self {
3796        Self {
3797            text: Some(text.into()),
3798            ..Default::default()
3799        }
3800    }
3801}
3802
3803/// The LLM response action (`httpLlmResponse`). Only the completion, embedding,
3804/// rerank, moderation and content-filter sub-objects are commonly set; each is
3805/// optional and every unknown/nested field is preserved via its own `extra`
3806/// catch-all so full LLM configs round-trip.
3807#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
3808#[serde(rename_all = "camelCase")]
3809pub struct HttpLlmResponse {
3810    #[serde(skip_serializing_if = "Option::is_none")]
3811    pub delay: Option<Delay>,
3812
3813    /// `"ANTHROPIC"`, `"OPENAI"`, `"OPENAI_RESPONSES"`, `"GEMINI"`, `"BEDROCK"`,
3814    /// `"AZURE_OPENAI"`, `"OLLAMA"`, `"COHERE"`, `"VOYAGE"`, and the OpenAI-chat-compatible
3815    /// `"MISTRAL"`, `"XAI"`, `"DEEPSEEK"`, `"GROQ"`, `"OPENROUTER"`.
3816    #[serde(skip_serializing_if = "Option::is_none")]
3817    pub provider: Option<String>,
3818
3819    #[serde(skip_serializing_if = "Option::is_none")]
3820    pub model: Option<String>,
3821
3822    #[serde(skip_serializing_if = "Option::is_none")]
3823    pub completion: Option<LlmCompletion>,
3824
3825    /// Embedding-response config (kept free-form; round-trips verbatim).
3826    #[serde(skip_serializing_if = "Option::is_none")]
3827    pub embedding: Option<serde_json::Value>,
3828
3829    /// Rerank-response config (kept free-form; round-trips verbatim).
3830    #[serde(skip_serializing_if = "Option::is_none")]
3831    pub rerank: Option<serde_json::Value>,
3832
3833    /// Moderation-response config (kept free-form; round-trips verbatim).
3834    #[serde(skip_serializing_if = "Option::is_none")]
3835    pub moderation: Option<serde_json::Value>,
3836
3837    /// Content-filter config (kept free-form; round-trips verbatim).
3838    #[serde(skip_serializing_if = "Option::is_none")]
3839    pub content_filter: Option<serde_json::Value>,
3840
3841    /// Conversation-matching predicates (kept free-form; round-trips verbatim).
3842    #[serde(skip_serializing_if = "Option::is_none")]
3843    pub conversation_predicates: Option<serde_json::Value>,
3844
3845    /// LLM-specific chaos config (kept free-form; round-trips verbatim).
3846    #[serde(skip_serializing_if = "Option::is_none")]
3847    pub chaos: Option<serde_json::Value>,
3848
3849    #[serde(skip_serializing_if = "Option::is_none")]
3850    pub primary: Option<bool>,
3851
3852    #[serde(flatten, default)]
3853    pub extra: Extra,
3854}
3855
3856impl HttpLlmResponse {
3857    /// Create a new empty LLM response.
3858    pub fn new() -> Self {
3859        Self::default()
3860    }
3861
3862    /// Set the provider (e.g. `"ANTHROPIC"`, `"OPENAI"`).
3863    pub fn provider(mut self, provider: impl Into<String>) -> Self {
3864        self.provider = Some(provider.into());
3865        self
3866    }
3867
3868    /// Set the model name.
3869    pub fn model(mut self, model: impl Into<String>) -> Self {
3870        self.model = Some(model.into());
3871        self
3872    }
3873
3874    /// Set the completion.
3875    pub fn completion(mut self, completion: LlmCompletion) -> Self {
3876        self.completion = Some(completion);
3877        self
3878    }
3879}
3880
3881// ---------------------------------------------------------------------------
3882// Expectation
3883// ---------------------------------------------------------------------------
3884
3885/// A full expectation combining a request matcher with an action.
3886#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
3887#[serde(rename_all = "camelCase")]
3888pub struct Expectation {
3889    #[serde(skip_serializing_if = "Option::is_none")]
3890    pub id: Option<String>,
3891
3892    #[serde(skip_serializing_if = "Option::is_none")]
3893    pub priority: Option<i32>,
3894
3895    /// Match only a percentage (0–100) of otherwise-matching requests.
3896    #[serde(skip_serializing_if = "Option::is_none")]
3897    pub percentage: Option<i32>,
3898
3899    /// Declarative HTTP chaos / fault-injection profile.
3900    #[serde(skip_serializing_if = "Option::is_none")]
3901    pub chaos: Option<HttpChaosProfile>,
3902
3903    /// Declarative, protocol-agnostic rate limit / quota.
3904    #[serde(skip_serializing_if = "Option::is_none")]
3905    pub rate_limit: Option<RateLimit>,
3906
3907    /// Request matcher. Optional because a `steps`-only (or side-effect-only)
3908    /// expectation carries no top-level request. Omitted from the wire form when
3909    /// `None`; an explicitly empty [`HttpRequest`] still serialises as `{}`.
3910    #[serde(default, skip_serializing_if = "Option::is_none")]
3911    pub http_request: Option<HttpRequest>,
3912
3913    #[serde(skip_serializing_if = "Option::is_none")]
3914    pub http_response: Option<HttpResponse>,
3915
3916    #[serde(skip_serializing_if = "Option::is_none")]
3917    pub http_forward: Option<HttpForward>,
3918
3919    #[serde(skip_serializing_if = "Option::is_none")]
3920    pub http_response_template: Option<HttpTemplate>,
3921
3922    #[serde(skip_serializing_if = "Option::is_none")]
3923    pub http_forward_template: Option<HttpTemplate>,
3924
3925    #[serde(skip_serializing_if = "Option::is_none")]
3926    pub http_error: Option<HttpError>,
3927
3928    /// Class callback that produces the response (serialized as `httpResponseClassCallback`).
3929    #[serde(skip_serializing_if = "Option::is_none")]
3930    pub http_response_class_callback: Option<HttpClassCallback>,
3931
3932    /// Class callback that produces the request to forward (serialized as `httpForwardClassCallback`).
3933    #[serde(skip_serializing_if = "Option::is_none")]
3934    pub http_forward_class_callback: Option<HttpClassCallback>,
3935
3936    /// Object/closure callback that produces the response (serialized as `httpResponseObjectCallback`).
3937    #[serde(skip_serializing_if = "Option::is_none")]
3938    pub http_response_object_callback: Option<HttpObjectCallback>,
3939
3940    /// Object/closure callback that produces the request to forward (serialized as `httpForwardObjectCallback`).
3941    #[serde(skip_serializing_if = "Option::is_none")]
3942    pub http_forward_object_callback: Option<HttpObjectCallback>,
3943
3944    /// Override the forwarded request/response (`httpOverrideForwardedRequest`).
3945    #[serde(skip_serializing_if = "Option::is_none")]
3946    pub http_override_forwarded_request: Option<HttpOverrideForwardedRequest>,
3947
3948    /// Forward and validate against an OpenAPI spec (`httpForwardValidateAction`).
3949    #[serde(skip_serializing_if = "Option::is_none")]
3950    pub http_forward_validate_action: Option<HttpForwardValidateAction>,
3951
3952    /// Forward with a fallback response on failure (`httpForwardWithFallback`).
3953    #[serde(skip_serializing_if = "Option::is_none")]
3954    pub http_forward_with_fallback: Option<HttpForwardWithFallback>,
3955
3956    #[serde(skip_serializing_if = "Option::is_none")]
3957    pub http_sse_response: Option<HttpSseResponse>,
3958
3959    /// LLM response action (`httpLlmResponse`).
3960    #[serde(skip_serializing_if = "Option::is_none")]
3961    pub http_llm_response: Option<HttpLlmResponse>,
3962
3963    #[serde(skip_serializing_if = "Option::is_none")]
3964    pub http_web_socket_response: Option<HttpWebSocketResponse>,
3965
3966    #[serde(skip_serializing_if = "Option::is_none")]
3967    pub dns_response: Option<DnsResponse>,
3968
3969    #[serde(skip_serializing_if = "Option::is_none")]
3970    pub binary_response: Option<BinaryResponse>,
3971
3972    #[serde(skip_serializing_if = "Option::is_none")]
3973    pub grpc_stream_response: Option<GrpcStreamResponse>,
3974
3975    /// gRPC bidirectional-streaming response action (`grpcBidiResponse`).
3976    #[serde(skip_serializing_if = "Option::is_none")]
3977    pub grpc_bidi_response: Option<GrpcBidiResponse>,
3978
3979    #[serde(skip_serializing_if = "Option::is_none")]
3980    pub times: Option<Times>,
3981
3982    #[serde(skip_serializing_if = "Option::is_none")]
3983    pub time_to_live: Option<TimeToLive>,
3984
3985    /// Name of the state-machine this expectation participates in.
3986    #[serde(skip_serializing_if = "Option::is_none")]
3987    pub scenario_name: Option<String>,
3988
3989    /// State the scenario must be in for this expectation to match.
3990    #[serde(skip_serializing_if = "Option::is_none")]
3991    pub scenario_state: Option<String>,
3992
3993    /// State the scenario transitions to after this expectation matches.
3994    #[serde(skip_serializing_if = "Option::is_none")]
3995    pub new_scenario_state: Option<String>,
3996
3997    /// Multiple responses; takes priority over the singular [`Expectation::http_response`].
3998    #[serde(skip_serializing_if = "Option::is_none")]
3999    pub http_responses: Option<Vec<HttpResponse>>,
4000
4001    /// How a response is selected from [`Expectation::http_responses`].
4002    #[serde(skip_serializing_if = "Option::is_none")]
4003    pub response_mode: Option<ResponseMode>,
4004
4005    /// Index-aligned relative weights for [`ResponseMode::Weighted`].
4006    #[serde(skip_serializing_if = "Option::is_none")]
4007    pub response_weights: Option<Vec<i32>>,
4008
4009    /// Requests per response block before advancing under [`ResponseMode::Switch`] (default 1).
4010    #[serde(skip_serializing_if = "Option::is_none")]
4011    pub switch_after: Option<i32>,
4012
4013    /// Cross-protocol scenario correlations that advance scenario state on protocol events.
4014    #[serde(skip_serializing_if = "Option::is_none")]
4015    pub cross_protocol_scenarios: Option<Vec<CrossProtocolScenario>>,
4016
4017    /// Side-effect actions run before the main action fires (`beforeActions`).
4018    ///
4019    /// The server accepts a single object or an array; this client accepts both
4020    /// on the wire and always serialises an array.
4021    #[serde(
4022        skip_serializing_if = "Option::is_none",
4023        deserialize_with = "one_or_many",
4024        default
4025    )]
4026    pub before_actions: Option<Vec<ExpectationAction>>,
4027
4028    /// Side-effect actions run after the main action fires (`afterActions`).
4029    #[serde(
4030        skip_serializing_if = "Option::is_none",
4031        deserialize_with = "one_or_many",
4032        default
4033    )]
4034    pub after_actions: Option<Vec<ExpectationAction>>,
4035
4036    /// Capture rules that bind request values into scenario/template state.
4037    #[serde(
4038        skip_serializing_if = "Option::is_none",
4039        deserialize_with = "one_or_many",
4040        default
4041    )]
4042    pub capture: Option<Vec<CaptureRule>>,
4043
4044    /// Optional namespace (tenant) this expectation belongs to.
4045    #[serde(skip_serializing_if = "Option::is_none")]
4046    pub namespace: Option<String>,
4047
4048    /// Multi-step script for a single match (`steps`).
4049    #[serde(skip_serializing_if = "Option::is_none")]
4050    pub steps: Option<Vec<ExpectationStep>>,
4051
4052    /// Creation timestamp (set by the server; round-tripped when present).
4053    #[serde(skip_serializing_if = "Option::is_none")]
4054    pub timestamp: Option<String>,
4055
4056    /// Forward-compatibility catch-all for any expectation field the typed model
4057    /// does not yet name, so unknown fields survive a round-trip instead of
4058    /// being silently dropped.
4059    #[serde(flatten, default)]
4060    pub extra: Extra,
4061}
4062
4063impl Expectation {
4064    /// Create a new expectation with the given request matcher.
4065    pub fn new(request: HttpRequest) -> Self {
4066        Self {
4067            http_request: Some(request),
4068            ..Default::default()
4069        }
4070    }
4071
4072    /// Set the expectation ID (for upsert semantics).
4073    pub fn id(mut self, id: impl Into<String>) -> Self {
4074        self.id = Some(id.into());
4075        self
4076    }
4077
4078    /// Set the priority (higher = matched first).
4079    pub fn priority(mut self, priority: i32) -> Self {
4080        self.priority = Some(priority);
4081        self
4082    }
4083
4084    /// Set a response action.
4085    pub fn respond(mut self, response: HttpResponse) -> Self {
4086        self.http_response = Some(response);
4087        self
4088    }
4089
4090    /// Set a forward action.
4091    pub fn forward(mut self, forward: HttpForward) -> Self {
4092        self.http_forward = Some(forward);
4093        self
4094    }
4095
4096    /// Set a response template action.
4097    pub fn respond_template(mut self, template: HttpTemplate) -> Self {
4098        self.http_response_template = Some(template);
4099        self
4100    }
4101
4102    /// Set a forward template action.
4103    pub fn forward_template(mut self, template: HttpTemplate) -> Self {
4104        self.http_forward_template = Some(template);
4105        self
4106    }
4107
4108    /// Set an error action.
4109    pub fn error(mut self, error: HttpError) -> Self {
4110        self.http_error = Some(error);
4111        self
4112    }
4113
4114    /// Respond via a server-side class callback (`httpResponseClassCallback`).
4115    ///
4116    /// The named class must implement MockServer's callback interface and be on
4117    /// the server's classpath. Convenience over building an [`HttpClassCallback`]
4118    /// directly; use [`respond_class_callback`](Self::respond_class_callback) for
4119    /// the full builder (delay, primary).
4120    pub fn respond_with_class_callback(mut self, callback_class: impl Into<String>) -> Self {
4121        self.http_response_class_callback = Some(HttpClassCallback::new(callback_class));
4122        self
4123    }
4124
4125    /// Respond via a pre-built [`HttpClassCallback`] (`httpResponseClassCallback`).
4126    pub fn respond_class_callback(mut self, callback: HttpClassCallback) -> Self {
4127        self.http_response_class_callback = Some(callback);
4128        self
4129    }
4130
4131    /// Forward via a server-side class callback (`httpForwardClassCallback`).
4132    pub fn forward_with_class_callback(mut self, callback_class: impl Into<String>) -> Self {
4133        self.http_forward_class_callback = Some(HttpClassCallback::new(callback_class));
4134        self
4135    }
4136
4137    /// Forward via a pre-built [`HttpClassCallback`] (`httpForwardClassCallback`).
4138    pub fn forward_class_callback(mut self, callback: HttpClassCallback) -> Self {
4139        self.http_forward_class_callback = Some(callback);
4140        self
4141    }
4142
4143    /// Respond via an object/closure callback (`httpResponseObjectCallback`).
4144    ///
4145    /// Most users should call
4146    /// [`MockServerClient::mock_with_callback`](crate::MockServerClient::mock_with_callback)
4147    /// instead, which opens the callback WebSocket, registers the closure, and
4148    /// fills in the `client_id` automatically.
4149    pub fn respond_object_callback(mut self, callback: HttpObjectCallback) -> Self {
4150        self.http_response_object_callback = Some(callback);
4151        self
4152    }
4153
4154    /// Forward via an object/closure callback (`httpForwardObjectCallback`).
4155    pub fn forward_object_callback(mut self, callback: HttpObjectCallback) -> Self {
4156        self.http_forward_object_callback = Some(callback);
4157        self
4158    }
4159
4160    /// Set a Server-Sent Events (SSE) response action.
4161    pub fn respond_sse(mut self, sse: HttpSseResponse) -> Self {
4162        self.http_sse_response = Some(sse);
4163        self
4164    }
4165
4166    /// Set a WebSocket response action.
4167    pub fn respond_web_socket(mut self, ws: HttpWebSocketResponse) -> Self {
4168        self.http_web_socket_response = Some(ws);
4169        self
4170    }
4171
4172    /// Set a DNS response action.
4173    pub fn respond_dns(mut self, dns: DnsResponse) -> Self {
4174        self.dns_response = Some(dns);
4175        self
4176    }
4177
4178    /// Set a raw binary response action.
4179    pub fn respond_binary(mut self, binary: BinaryResponse) -> Self {
4180        self.binary_response = Some(binary);
4181        self
4182    }
4183
4184    /// Set a gRPC streaming response action.
4185    pub fn respond_grpc_stream(mut self, grpc: GrpcStreamResponse) -> Self {
4186        self.grpc_stream_response = Some(grpc);
4187        self
4188    }
4189
4190    /// Set the number of times this expectation matches.
4191    pub fn times(mut self, times: Times) -> Self {
4192        self.times = Some(times);
4193        self
4194    }
4195
4196    /// Set the time-to-live.
4197    pub fn time_to_live(mut self, ttl: TimeToLive) -> Self {
4198        self.time_to_live = Some(ttl);
4199        self
4200    }
4201
4202    /// Set the scenario (state-machine) name this expectation participates in.
4203    pub fn scenario_name(mut self, name: impl Into<String>) -> Self {
4204        self.scenario_name = Some(name.into());
4205        self
4206    }
4207
4208    /// Set the state the scenario must be in for this expectation to match.
4209    pub fn scenario_state(mut self, state: impl Into<String>) -> Self {
4210        self.scenario_state = Some(state.into());
4211        self
4212    }
4213
4214    /// Set the state the scenario transitions to after this expectation matches.
4215    pub fn new_scenario_state(mut self, state: impl Into<String>) -> Self {
4216        self.new_scenario_state = Some(state.into());
4217        self
4218    }
4219
4220    /// Append a response to the multiple-responses list (`http_responses`).
4221    ///
4222    /// When set, `http_responses` takes priority over the singular
4223    /// [`respond`](Self::respond) action.
4224    pub fn respond_with(mut self, response: HttpResponse) -> Self {
4225        self.http_responses
4226            .get_or_insert_with(Vec::new)
4227            .push(response);
4228        self
4229    }
4230
4231    /// Replace all multiple responses (`http_responses`).
4232    pub fn http_responses(mut self, responses: Vec<HttpResponse>) -> Self {
4233        self.http_responses = Some(responses);
4234        self
4235    }
4236
4237    /// Set how a response is selected from `http_responses`.
4238    pub fn response_mode(mut self, mode: ResponseMode) -> Self {
4239        self.response_mode = Some(mode);
4240        self
4241    }
4242
4243    /// Set the index-aligned relative weights for [`ResponseMode::Weighted`].
4244    pub fn response_weights(mut self, weights: Vec<i32>) -> Self {
4245        self.response_weights = Some(weights);
4246        self
4247    }
4248
4249    /// Set the number of requests per response block before advancing under
4250    /// [`ResponseMode::Switch`].
4251    pub fn switch_after(mut self, switch_after: i32) -> Self {
4252        self.switch_after = Some(switch_after);
4253        self
4254    }
4255
4256    /// Append a [`CrossProtocolScenario`] correlation.
4257    pub fn cross_protocol_scenario(mut self, scenario: CrossProtocolScenario) -> Self {
4258        self.cross_protocol_scenarios
4259            .get_or_insert_with(Vec::new)
4260            .push(scenario);
4261        self
4262    }
4263
4264    /// Replace all cross-protocol scenario correlations.
4265    pub fn cross_protocol_scenarios(mut self, scenarios: Vec<CrossProtocolScenario>) -> Self {
4266        self.cross_protocol_scenarios = Some(scenarios);
4267        self
4268    }
4269
4270    /// Match only a percentage (0–100) of otherwise-matching requests.
4271    pub fn percentage(mut self, percentage: i32) -> Self {
4272        self.percentage = Some(percentage);
4273        self
4274    }
4275
4276    /// Attach a declarative HTTP chaos / fault-injection profile.
4277    pub fn chaos(mut self, chaos: HttpChaosProfile) -> Self {
4278        self.chaos = Some(chaos);
4279        self
4280    }
4281
4282    /// Attach a declarative rate limit / quota.
4283    pub fn rate_limit(mut self, rate_limit: RateLimit) -> Self {
4284        self.rate_limit = Some(rate_limit);
4285        self
4286    }
4287
4288    /// Override the forwarded request/response (`httpOverrideForwardedRequest`).
4289    pub fn override_forwarded_request(
4290        mut self,
4291        override_request: HttpOverrideForwardedRequest,
4292    ) -> Self {
4293        self.http_override_forwarded_request = Some(override_request);
4294        self
4295    }
4296
4297    /// Forward and validate against an OpenAPI spec (`httpForwardValidateAction`).
4298    pub fn forward_validate(mut self, action: HttpForwardValidateAction) -> Self {
4299        self.http_forward_validate_action = Some(action);
4300        self
4301    }
4302
4303    /// Forward with a fallback response on failure (`httpForwardWithFallback`).
4304    pub fn forward_with_fallback(mut self, action: HttpForwardWithFallback) -> Self {
4305        self.http_forward_with_fallback = Some(action);
4306        self
4307    }
4308
4309    /// Set an LLM response action (`httpLlmResponse`).
4310    pub fn respond_llm(mut self, llm: HttpLlmResponse) -> Self {
4311        self.http_llm_response = Some(llm);
4312        self
4313    }
4314
4315    /// Set a gRPC bidirectional-streaming response action (`grpcBidiResponse`).
4316    pub fn respond_grpc_bidi(mut self, grpc: GrpcBidiResponse) -> Self {
4317        self.grpc_bidi_response = Some(grpc);
4318        self
4319    }
4320
4321    /// Append a before-action (`beforeActions`).
4322    pub fn before_action(mut self, action: ExpectationAction) -> Self {
4323        self.before_actions
4324            .get_or_insert_with(Vec::new)
4325            .push(action);
4326        self
4327    }
4328
4329    /// Append an after-action (`afterActions`).
4330    pub fn after_action(mut self, action: ExpectationAction) -> Self {
4331        self.after_actions.get_or_insert_with(Vec::new).push(action);
4332        self
4333    }
4334
4335    /// Append a capture rule (`capture`).
4336    pub fn capture_rule(mut self, rule: CaptureRule) -> Self {
4337        self.capture.get_or_insert_with(Vec::new).push(rule);
4338        self
4339    }
4340
4341    /// Set the namespace (tenant) this expectation belongs to.
4342    pub fn namespace(mut self, namespace: impl Into<String>) -> Self {
4343        self.namespace = Some(namespace.into());
4344        self
4345    }
4346
4347    /// Append a step to the multi-step script (`steps`).
4348    pub fn step(mut self, step: ExpectationStep) -> Self {
4349        self.steps.get_or_insert_with(Vec::new).push(step);
4350        self
4351    }
4352
4353    /// Replace all steps (`steps`).
4354    pub fn steps(mut self, steps: Vec<ExpectationStep>) -> Self {
4355        self.steps = Some(steps);
4356        self
4357    }
4358}
4359
4360// ---------------------------------------------------------------------------
4361// Verification
4362// ---------------------------------------------------------------------------
4363
4364/// A verification request sent to MockServer.
4365///
4366/// At least one of `http_request` or `http_response` must be set.
4367/// `http_response` uses the same [`HttpResponse`] type as expectations —
4368/// the server matches against the recorded response's status code, headers,
4369/// and body.
4370#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4371#[serde(rename_all = "camelCase")]
4372pub struct Verification {
4373    #[serde(skip_serializing_if = "Option::is_none")]
4374    pub http_request: Option<HttpRequest>,
4375
4376    #[serde(skip_serializing_if = "Option::is_none")]
4377    pub http_response: Option<HttpResponse>,
4378
4379    #[serde(skip_serializing_if = "Option::is_none")]
4380    pub times: Option<VerificationTimes>,
4381
4382    #[serde(skip_serializing_if = "Option::is_none")]
4383    pub maximum_number_of_request_to_return_in_verification_failure: Option<u32>,
4384}
4385
4386/// A verification sequence request.
4387///
4388/// `http_responses` is index-aligned with `http_requests` — each entry
4389/// constrains the response that must have been returned for the
4390/// corresponding request.
4391#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4392#[serde(rename_all = "camelCase")]
4393pub struct VerificationSequence {
4394    #[serde(skip_serializing_if = "Option::is_none")]
4395    pub http_requests: Option<Vec<HttpRequest>>,
4396
4397    #[serde(skip_serializing_if = "Option::is_none")]
4398    pub http_responses: Option<Vec<HttpResponse>>,
4399}
4400
4401// ---------------------------------------------------------------------------
4402// Ports
4403// ---------------------------------------------------------------------------
4404
4405/// Port list (used by status and bind endpoints).
4406#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4407pub struct Ports {
4408    pub ports: Vec<u16>,
4409}
4410
4411// ---------------------------------------------------------------------------
4412// Scenario state
4413// ---------------------------------------------------------------------------
4414
4415/// A scenario and its current state, as returned by the scenario REST
4416/// endpoints (`GET /mockserver/scenario` and `GET /mockserver/scenario/{name}`).
4417#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
4418#[serde(rename_all = "camelCase")]
4419pub struct ScenarioState {
4420    /// The scenario (state-machine) name.
4421    pub scenario_name: String,
4422    /// The scenario's current state.
4423    pub current_state: String,
4424}
4425
4426/// Wrapper for the `GET /mockserver/scenario` list response shape
4427/// (`{"scenarios":[{"scenarioName","currentState"}]}`).
4428#[derive(Debug, Clone, Deserialize)]
4429pub(crate) struct ScenarioList {
4430    #[serde(default)]
4431    pub scenarios: Vec<ScenarioState>,
4432}
4433
4434// ---------------------------------------------------------------------------
4435// Retrieve types
4436// ---------------------------------------------------------------------------
4437
4438/// The type of data to retrieve from MockServer.
4439#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4440pub enum RetrieveType {
4441    /// Recorded inbound requests.
4442    Requests,
4443    /// Active (live) expectations.
4444    ActiveExpectations,
4445    /// Recorded expectations (from proxy mode).
4446    RecordedExpectations,
4447    /// Log messages.
4448    Logs,
4449    /// Request/response pairs.
4450    RequestResponses,
4451}
4452
4453impl RetrieveType {
4454    /// The query parameter value for this type.
4455    pub fn as_str(&self) -> &'static str {
4456        match self {
4457            RetrieveType::Requests => "REQUESTS",
4458            RetrieveType::ActiveExpectations => "ACTIVE_EXPECTATIONS",
4459            RetrieveType::RecordedExpectations => "RECORDED_EXPECTATIONS",
4460            RetrieveType::Logs => "LOGS",
4461            RetrieveType::RequestResponses => "REQUEST_RESPONSES",
4462        }
4463    }
4464}
4465
4466/// The response format for retrieve calls.
4467///
4468/// In addition to JSON and log-entry formats, MockServer can return the
4469/// retrieved expectations as SDK setup code (the builder code that recreates
4470/// the expectations) in a range of languages.
4471#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4472pub enum RetrieveFormat {
4473    Json,
4474    LogEntries,
4475    Java,
4476    JavaScript,
4477    Python,
4478    Go,
4479    CSharp,
4480    Ruby,
4481    Rust,
4482    Php,
4483}
4484
4485impl RetrieveFormat {
4486    /// The query parameter value for this format.
4487    pub fn as_str(&self) -> &'static str {
4488        match self {
4489            RetrieveFormat::Json => "JSON",
4490            RetrieveFormat::LogEntries => "LOG_ENTRIES",
4491            RetrieveFormat::Java => "JAVA",
4492            RetrieveFormat::JavaScript => "JAVASCRIPT",
4493            RetrieveFormat::Python => "PYTHON",
4494            RetrieveFormat::Go => "GO",
4495            RetrieveFormat::CSharp => "CSHARP",
4496            RetrieveFormat::Ruby => "RUBY",
4497            RetrieveFormat::Rust => "RUST",
4498            RetrieveFormat::Php => "PHP",
4499        }
4500    }
4501}
4502
4503/// The type of data to clear from MockServer.
4504#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4505pub enum ClearType {
4506    All,
4507    Log,
4508    Expectations,
4509}
4510
4511impl ClearType {
4512    /// The query parameter value for this type.
4513    pub fn as_str(&self) -> &'static str {
4514        match self {
4515            ClearType::All => "ALL",
4516            ClearType::Log => "LOG",
4517            ClearType::Expectations => "EXPECTATIONS",
4518        }
4519    }
4520}
4521
4522// ---------------------------------------------------------------------------
4523// Pact verification result
4524// ---------------------------------------------------------------------------
4525
4526/// Outcome of a Pact contract verification (`PUT /mockserver/pact/verify`).
4527///
4528/// The server replies `202 ACCEPTED` when every interaction in the contract
4529/// matched an active expectation, or `406 NOT_ACCEPTABLE` when verification
4530/// failed — in both cases the body is the same verification report JSON.
4531#[derive(Debug, Clone, PartialEq, Eq)]
4532pub struct PactVerification {
4533    /// `true` when verification passed (`202`), `false` when it failed (`406`).
4534    pub passed: bool,
4535    /// The verification report JSON returned by the server (verbatim).
4536    pub report: String,
4537}
4538
4539// ---------------------------------------------------------------------------
4540// Operating mode
4541// ---------------------------------------------------------------------------
4542
4543/// High-level operating mode for MockServer (set via `PUT /mockserver/mode`,
4544/// read via `GET /mockserver/mode`).
4545///
4546/// Each mode packages the common record / replay / pass-through workflows into a
4547/// single switch (a convenience over `attemptToProxyIfNoMatchingExpectation`):
4548///
4549/// * [`MockMode::Simulate`] — match expectations and return mocks; unmatched
4550///   requests get a `404`. This is the default (proxy-on-no-match disabled).
4551/// * [`MockMode::Spy`] — match expectations and return mocks, but forward
4552///   unmatched requests to the real upstream so they are served live and recorded
4553///   (proxy-on-no-match enabled).
4554/// * [`MockMode::Capture`] — forward and record; with no expectations defined this
4555///   captures all traffic. Backed by the same proxy flag as [`MockMode::Spy`].
4556#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4557pub enum MockMode {
4558    /// Match expectations; unmatched requests get a `404` (default).
4559    Simulate,
4560    /// Match expectations; unmatched requests forwarded to the upstream and recorded.
4561    Spy,
4562    /// Forward and record all traffic.
4563    Capture,
4564}
4565
4566impl MockMode {
4567    /// The wire value for this mode (the `mode` query parameter / JSON field).
4568    pub fn as_str(&self) -> &'static str {
4569        match self {
4570            MockMode::Simulate => "SIMULATE",
4571            MockMode::Spy => "SPY",
4572            MockMode::Capture => "CAPTURE",
4573        }
4574    }
4575
4576    /// Whether, in this mode, a request matching no expectation is proxied to its
4577    /// upstream (and thereby recorded) rather than answered with a `404`.
4578    pub fn proxy_unmatched_requests(&self) -> bool {
4579        !matches!(self, MockMode::Simulate)
4580    }
4581}
4582
4583impl std::fmt::Display for MockMode {
4584    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4585        f.write_str(self.as_str())
4586    }
4587}
4588
4589impl std::str::FromStr for MockMode {
4590    type Err = String;
4591
4592    /// Parse a mode name case-insensitively (matches the server's
4593    /// `MockMode.parse`). Returns an error message for blank/unknown values.
4594    fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
4595        match value.trim().to_uppercase().as_str() {
4596            "" => Err("mode is required (one of SIMULATE, SPY, CAPTURE)".to_string()),
4597            "SIMULATE" => Ok(MockMode::Simulate),
4598            "SPY" => Ok(MockMode::Spy),
4599            "CAPTURE" => Ok(MockMode::Capture),
4600            other => Err(format!(
4601                "unknown mode '{other}' (expected one of SIMULATE, SPY, CAPTURE)"
4602            )),
4603        }
4604    }
4605}
4606
4607// ---------------------------------------------------------------------------
4608// gRPC descriptor management
4609// ---------------------------------------------------------------------------
4610
4611/// A single gRPC method registered from an uploaded descriptor set.
4612///
4613/// Returned by [`MockServerClient::retrieve_grpc_services`] as part of a
4614/// [`GrpcService`]. Maps to the `methods[]` entries of the
4615/// `PUT /mockserver/grpc/services` wire shape.
4616///
4617/// [`MockServerClient::retrieve_grpc_services`]: crate::MockServerClient::retrieve_grpc_services
4618#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
4619#[serde(rename_all = "camelCase")]
4620pub struct GrpcMethod {
4621    /// The simple method name (e.g. `SayHello`).
4622    pub name: String,
4623
4624    /// Fully-qualified name of the request message type.
4625    pub input_type: String,
4626
4627    /// Fully-qualified name of the response message type.
4628    pub output_type: String,
4629
4630    /// Whether the method uses client-side streaming.
4631    pub client_streaming: bool,
4632
4633    /// Whether the method uses server-side streaming.
4634    pub server_streaming: bool,
4635}
4636
4637/// A gRPC service registered from an uploaded descriptor set.
4638///
4639/// Returned by [`MockServerClient::retrieve_grpc_services`]. Maps to the
4640/// top-level entries of the `PUT /mockserver/grpc/services` wire shape.
4641///
4642/// [`MockServerClient::retrieve_grpc_services`]: crate::MockServerClient::retrieve_grpc_services
4643#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
4644#[serde(rename_all = "camelCase")]
4645pub struct GrpcService {
4646    /// Fully-qualified service name (e.g. `helloworld.Greeter`).
4647    pub name: String,
4648
4649    /// The methods declared by this service.
4650    pub methods: Vec<GrpcMethod>,
4651}
4652
4653// ---------------------------------------------------------------------------
4654// SocketAddress
4655// ---------------------------------------------------------------------------
4656
4657/// A downstream socket address (host / port / scheme) to direct a request at.
4658///
4659/// Maps to MockServer's `SocketAddress` model. Used by load-scenario steps to
4660/// target a specific upstream rather than relying on the `Host` header.
4661#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
4662#[serde(rename_all = "camelCase")]
4663pub struct SocketAddress {
4664    /// The downstream host name or IP.
4665    pub host: String,
4666
4667    /// The downstream port.
4668    pub port: u16,
4669
4670    /// The scheme to connect with — `"HTTP"` or `"HTTPS"`. Defaults to `"HTTP"`
4671    /// on the server when omitted.
4672    #[serde(skip_serializing_if = "Option::is_none")]
4673    pub scheme: Option<String>,
4674}
4675
4676impl SocketAddress {
4677    /// Create a plain HTTP socket address.
4678    pub fn new(host: impl Into<String>, port: u16) -> Self {
4679        Self {
4680            host: host.into(),
4681            port,
4682            scheme: None,
4683        }
4684    }
4685
4686    /// Set the scheme (`"HTTP"` or `"HTTPS"`).
4687    pub fn scheme(mut self, scheme: impl Into<String>) -> Self {
4688        self.scheme = Some(scheme.into());
4689        self
4690    }
4691
4692    /// Convenience: an HTTPS socket address.
4693    pub fn https(host: impl Into<String>, port: u16) -> Self {
4694        Self::new(host, port).scheme("HTTPS")
4695    }
4696}
4697
4698// ---------------------------------------------------------------------------
4699// Load scenario registry (PUT/GET/DELETE /mockserver/loadScenario[/...])
4700// ---------------------------------------------------------------------------
4701
4702/// The interpolation curve used to ramp a value (virtual users or arrival
4703/// rate) from a start setpoint to an end setpoint across a ramp [`LoadStage`].
4704/// Maps to the `RampCurve` schema. Only meaningful for ramp stages; ignored for
4705/// holds and pauses.
4706#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
4707#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
4708pub enum RampCurve {
4709    /// Constant slope.
4710    Linear,
4711    /// Ease-in: slow then fast.
4712    Quadratic,
4713    /// A steeper ease-in.
4714    Exponential,
4715}
4716
4717/// The kind of a [`LoadStage`].
4718///
4719/// - `Vu` — closed model: hold or ramp the number of concurrent virtual users.
4720/// - `Rate` — open model: hold or ramp an arrival rate in iterations/second.
4721/// - `Pause` — drive no load for the duration.
4722#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
4723#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
4724pub enum LoadStageType {
4725    /// Closed model — hold or ramp concurrent virtual users.
4726    Vu,
4727    /// Open model — hold or ramp an arrival rate in iterations/second.
4728    Rate,
4729    /// Drive no load for the duration.
4730    Pause,
4731}
4732
4733/// One stage of a [`LoadProfile`]: a contiguous slice of the run holding or
4734/// ramping a setpoint for `duration_millis`. Stages run in sequence. Maps to the
4735/// `LoadStage` schema.
4736///
4737/// Use the constructors [`LoadStage::vu_hold`], [`LoadStage::vu_ramp`],
4738/// [`LoadStage::rate_hold`], [`LoadStage::rate_ramp`] and [`LoadStage::pause`]
4739/// rather than building the struct directly so only the relevant fields are set
4740/// (and therefore serialized).
4741#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4742#[serde(rename_all = "camelCase")]
4743pub struct LoadStage {
4744    /// The kind of stage — `VU`, `RATE` or `PAUSE`.
4745    #[serde(rename = "type")]
4746    pub stage_type: LoadStageType,
4747
4748    /// How long this stage runs in milliseconds (> 0).
4749    pub duration_millis: u64,
4750
4751    /// Ramp shape (ramp stages only); omitted for holds and pauses.
4752    #[serde(skip_serializing_if = "Option::is_none")]
4753    pub curve: Option<RampCurve>,
4754
4755    /// VU hold: the number of virtual users to hold for the stage.
4756    #[serde(skip_serializing_if = "Option::is_none")]
4757    pub vus: Option<u32>,
4758
4759    /// VU ramp: virtual users at the start of the ramp.
4760    #[serde(skip_serializing_if = "Option::is_none")]
4761    pub start_vus: Option<u32>,
4762
4763    /// VU ramp: virtual users at the end of the ramp.
4764    #[serde(skip_serializing_if = "Option::is_none")]
4765    pub end_vus: Option<u32>,
4766
4767    /// RATE hold: arrival rate to hold, in iterations per second.
4768    #[serde(skip_serializing_if = "Option::is_none")]
4769    pub rate: Option<f64>,
4770
4771    /// RATE ramp: arrival rate at the start of the ramp, in iterations/second.
4772    #[serde(skip_serializing_if = "Option::is_none")]
4773    pub start_rate: Option<f64>,
4774
4775    /// RATE ramp: arrival rate at the end of the ramp, in iterations/second.
4776    #[serde(skip_serializing_if = "Option::is_none")]
4777    pub end_rate: Option<f64>,
4778
4779    /// RATE stage only: optional cap on the auto-scaling virtual-user pool.
4780    #[serde(skip_serializing_if = "Option::is_none")]
4781    pub max_vus: Option<u32>,
4782}
4783
4784impl LoadStage {
4785    fn base(stage_type: LoadStageType, duration_millis: u64) -> Self {
4786        Self {
4787            stage_type,
4788            duration_millis,
4789            curve: None,
4790            vus: None,
4791            start_vus: None,
4792            end_vus: None,
4793            rate: None,
4794            start_rate: None,
4795            end_rate: None,
4796            max_vus: None,
4797        }
4798    }
4799
4800    /// A VU stage holding `vus` virtual users for `duration_millis`.
4801    pub fn vu_hold(vus: u32, duration_millis: u64) -> Self {
4802        let mut stage = Self::base(LoadStageType::Vu, duration_millis);
4803        stage.vus = Some(vus);
4804        stage
4805    }
4806
4807    /// A VU stage ramping from `start_vus` to `end_vus` over `duration_millis`
4808    /// along `curve`.
4809    pub fn vu_ramp(start_vus: u32, end_vus: u32, duration_millis: u64, curve: RampCurve) -> Self {
4810        let mut stage = Self::base(LoadStageType::Vu, duration_millis);
4811        stage.start_vus = Some(start_vus);
4812        stage.end_vus = Some(end_vus);
4813        stage.curve = Some(curve);
4814        stage
4815    }
4816
4817    /// A RATE stage holding `rate` iterations/second for `duration_millis`.
4818    pub fn rate_hold(rate: f64, duration_millis: u64) -> Self {
4819        let mut stage = Self::base(LoadStageType::Rate, duration_millis);
4820        stage.rate = Some(rate);
4821        stage
4822    }
4823
4824    /// A RATE stage ramping from `start_rate` to `end_rate` iterations/second
4825    /// over `duration_millis` along `curve`.
4826    pub fn rate_ramp(
4827        start_rate: f64,
4828        end_rate: f64,
4829        duration_millis: u64,
4830        curve: RampCurve,
4831    ) -> Self {
4832        let mut stage = Self::base(LoadStageType::Rate, duration_millis);
4833        stage.start_rate = Some(start_rate);
4834        stage.end_rate = Some(end_rate);
4835        stage.curve = Some(curve);
4836        stage
4837    }
4838
4839    /// A PAUSE stage that drives no load for `duration_millis`.
4840    pub fn pause(duration_millis: u64) -> Self {
4841        Self::base(LoadStageType::Pause, duration_millis)
4842    }
4843
4844    /// Cap the auto-scaling virtual-user pool for this RATE stage.
4845    pub fn max_vus(mut self, max_vus: u32) -> Self {
4846        self.max_vus = Some(max_vus);
4847        self
4848    }
4849}
4850
4851/// A named load shape that expands server-side into ordinary [`LoadStage`]s.
4852/// Maps to the `LoadShapeType` schema.
4853#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
4854#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
4855pub enum LoadShapeType {
4856    /// Ramp up, hold the peak, ramp back down, with an optional recovery hold.
4857    Spike,
4858    /// A flight of pure-hold steps, each one "step" higher.
4859    Stairs,
4860    /// Ramp 0 to target then hold.
4861    RampHold,
4862}
4863
4864/// What a [`LoadShape`] drives. Maps to the `LoadShapeMetric` schema.
4865///
4866/// - `Vu` — concurrent virtual users (closed model).
4867/// - `Rate` — arrival rate in iterations/second (open model).
4868#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
4869#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
4870pub enum LoadShapeMetric {
4871    /// Concurrent virtual users (closed model).
4872    Vu,
4873    /// Arrival rate in iterations/second (open model).
4874    Rate,
4875}
4876
4877/// A declarative named load shape that expands into ordinary [`LoadStage`]s.
4878/// Maps to the `LoadShape` schema. Only the parameters its `type` needs are
4879/// read; the rest are ignored. Use a shape OR an explicit `stages` list, not
4880/// both.
4881///
4882/// Use the constructors [`LoadShape::spike`], [`LoadShape::stairs`] and
4883/// [`LoadShape::ramp_hold`] so only the relevant fields are set (and therefore
4884/// serialized).
4885#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4886#[serde(rename_all = "camelCase")]
4887pub struct LoadShape {
4888    /// The named shape — `SPIKE`, `STAIRS` or `RAMP_HOLD`.
4889    #[serde(rename = "type")]
4890    pub shape_type: LoadShapeType,
4891
4892    /// What the shape drives — `VU` (default) or `RATE`.
4893    #[serde(skip_serializing_if = "Option::is_none")]
4894    pub metric: Option<LoadShapeMetric>,
4895
4896    /// Ramp interpolation curve used by the shape's ramps.
4897    #[serde(skip_serializing_if = "Option::is_none")]
4898    pub curve: Option<RampCurve>,
4899
4900    /// SPIKE: the level held before and after the spike.
4901    #[serde(skip_serializing_if = "Option::is_none")]
4902    pub baseline: Option<f64>,
4903
4904    /// SPIKE: the level held at the top of the spike.
4905    #[serde(skip_serializing_if = "Option::is_none")]
4906    pub peak: Option<f64>,
4907
4908    /// SPIKE: duration of the baseline to peak ramp.
4909    #[serde(skip_serializing_if = "Option::is_none")]
4910    pub ramp_up_millis: Option<u64>,
4911
4912    /// SPIKE: duration to hold at the peak; RAMP_HOLD: duration to hold at the
4913    /// target.
4914    #[serde(skip_serializing_if = "Option::is_none")]
4915    pub hold_millis: Option<u64>,
4916
4917    /// SPIKE: duration of the peak to baseline ramp.
4918    #[serde(skip_serializing_if = "Option::is_none")]
4919    pub ramp_down_millis: Option<u64>,
4920
4921    /// SPIKE (optional): duration to hold at baseline after the down ramp.
4922    #[serde(skip_serializing_if = "Option::is_none")]
4923    pub recovery_hold_millis: Option<u64>,
4924
4925    /// STAIRS: the level of the first step.
4926    #[serde(skip_serializing_if = "Option::is_none")]
4927    pub start: Option<f64>,
4928
4929    /// STAIRS: how much each step rises above the previous one.
4930    #[serde(skip_serializing_if = "Option::is_none")]
4931    pub step: Option<f64>,
4932
4933    /// STAIRS: the number of steps.
4934    #[serde(skip_serializing_if = "Option::is_none")]
4935    pub steps: Option<u32>,
4936
4937    /// STAIRS: how long each step holds at its level.
4938    #[serde(skip_serializing_if = "Option::is_none")]
4939    pub step_duration_millis: Option<u64>,
4940
4941    /// RAMP_HOLD: the level ramped up to (from 0) and then held.
4942    #[serde(skip_serializing_if = "Option::is_none")]
4943    pub target: Option<f64>,
4944
4945    /// RAMP_HOLD: duration of the 0 to target ramp.
4946    #[serde(skip_serializing_if = "Option::is_none")]
4947    pub ramp_millis: Option<u64>,
4948}
4949
4950impl LoadShape {
4951    fn base(shape_type: LoadShapeType) -> Self {
4952        Self {
4953            shape_type,
4954            metric: None,
4955            curve: None,
4956            baseline: None,
4957            peak: None,
4958            ramp_up_millis: None,
4959            hold_millis: None,
4960            ramp_down_millis: None,
4961            recovery_hold_millis: None,
4962            start: None,
4963            step: None,
4964            steps: None,
4965            step_duration_millis: None,
4966            target: None,
4967            ramp_millis: None,
4968        }
4969    }
4970
4971    /// A SPIKE shape: ramp `baseline` to `peak` over `ramp_up_millis`, hold for
4972    /// `hold_millis`, then ramp back down over `ramp_down_millis`.
4973    pub fn spike(
4974        baseline: f64,
4975        peak: f64,
4976        ramp_up_millis: u64,
4977        hold_millis: u64,
4978        ramp_down_millis: u64,
4979    ) -> Self {
4980        let mut shape = Self::base(LoadShapeType::Spike);
4981        shape.baseline = Some(baseline);
4982        shape.peak = Some(peak);
4983        shape.ramp_up_millis = Some(ramp_up_millis);
4984        shape.hold_millis = Some(hold_millis);
4985        shape.ramp_down_millis = Some(ramp_down_millis);
4986        shape
4987    }
4988
4989    /// A STAIRS shape: `steps` pure-hold steps, the first at `start` and each
4990    /// rising by `step`, every step holding for `step_duration_millis`.
4991    pub fn stairs(start: f64, step: f64, steps: u32, step_duration_millis: u64) -> Self {
4992        let mut shape = Self::base(LoadShapeType::Stairs);
4993        shape.start = Some(start);
4994        shape.step = Some(step);
4995        shape.steps = Some(steps);
4996        shape.step_duration_millis = Some(step_duration_millis);
4997        shape
4998    }
4999
5000    /// A RAMP_HOLD shape: ramp from 0 to `target` over `ramp_millis`, then hold
5001    /// for `hold_millis`.
5002    pub fn ramp_hold(target: f64, ramp_millis: u64, hold_millis: u64) -> Self {
5003        let mut shape = Self::base(LoadShapeType::RampHold);
5004        shape.target = Some(target);
5005        shape.ramp_millis = Some(ramp_millis);
5006        shape.hold_millis = Some(hold_millis);
5007        shape
5008    }
5009
5010    /// Set what the shape drives (`VU` or `RATE`).
5011    pub fn metric(mut self, metric: LoadShapeMetric) -> Self {
5012        self.metric = Some(metric);
5013        self
5014    }
5015
5016    /// Set the ramp interpolation curve.
5017    pub fn curve(mut self, curve: RampCurve) -> Self {
5018        self.curve = Some(curve);
5019        self
5020    }
5021
5022    /// SPIKE only: hold at baseline for `recovery_hold_millis` after the down
5023    /// ramp.
5024    pub fn recovery_hold_millis(mut self, recovery_hold_millis: u64) -> Self {
5025        self.recovery_hold_millis = Some(recovery_hold_millis);
5026        self
5027    }
5028}
5029
5030/// The per-run metric a [`LoadThreshold`] evaluates. Maps to the threshold
5031/// `metric` enum.
5032#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
5033#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
5034pub enum LoadThresholdMetric {
5035    /// 50th-percentile latency in milliseconds.
5036    LatencyP50,
5037    /// 95th-percentile latency in milliseconds.
5038    LatencyP95,
5039    /// 99th-percentile latency in milliseconds.
5040    LatencyP99,
5041    /// 99.9th-percentile latency in milliseconds.
5042    LatencyP999,
5043    /// Failed / requests, as a 0.0-1.0 fraction.
5044    ErrorRate,
5045    /// Throughput in requests/second over the run's elapsed time.
5046    ThroughputRps,
5047}
5048
5049/// How a [`LoadThreshold`]'s observed value is compared to its threshold. Maps
5050/// to the threshold `comparator` enum.
5051#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
5052#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
5053pub enum LoadComparator {
5054    /// observed < threshold.
5055    LessThan,
5056    /// observed <= threshold.
5057    LessThanOrEqual,
5058    /// observed > threshold.
5059    GreaterThan,
5060    /// observed >= threshold.
5061    GreaterThanOrEqual,
5062}
5063
5064/// An in-run pass/fail threshold for a load scenario: a per-run metric compared
5065/// against a value. All thresholds must hold for the run verdict to be PASS
5066/// (logical AND). Maps to the `LoadThreshold` schema.
5067#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
5068#[serde(rename_all = "camelCase")]
5069pub struct LoadThreshold {
5070    /// The per-run metric to evaluate.
5071    pub metric: LoadThresholdMetric,
5072
5073    /// How the observed per-run value is compared to the threshold.
5074    pub comparator: LoadComparator,
5075
5076    /// The threshold value (milliseconds for latency metrics, a 0.0-1.0
5077    /// fraction for `ERROR_RATE`, requests/second for `THROUGHPUT_RPS`).
5078    pub threshold: f64,
5079}
5080
5081impl LoadThreshold {
5082    /// Create a threshold comparing `metric` to `threshold` using `comparator`.
5083    pub fn new(metric: LoadThresholdMetric, comparator: LoadComparator, threshold: f64) -> Self {
5084        Self {
5085            metric,
5086            comparator,
5087            threshold,
5088        }
5089    }
5090}
5091
5092/// How a [`LoadPacing`] target iteration cycle is derived from its value. Maps
5093/// to the pacing `mode` enum.
5094#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
5095#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
5096pub enum LoadPacingMode {
5097    /// No pacing (immediate reschedule).
5098    None,
5099    /// `value` is the target cycle in milliseconds.
5100    ConstantPacing,
5101    /// `value` is the target iterations/second per VU (cycle = 1000 / value ms).
5102    ConstantThroughput,
5103}
5104
5105/// Adaptive iteration pacing (think-time) for a load scenario: a target
5106/// per-virtual-user iteration cycle time. Applies only to the closed-model VU
5107/// loop. Maps to the `LoadPacing` schema.
5108#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
5109#[serde(rename_all = "camelCase")]
5110pub struct LoadPacing {
5111    /// How the target iteration cycle is derived from `value`.
5112    pub mode: LoadPacingMode,
5113
5114    /// For `CONSTANT_PACING` the target cycle in milliseconds; for
5115    /// `CONSTANT_THROUGHPUT` the target iterations/second per VU. Must be > 0
5116    /// when `mode` is not `NONE`.
5117    pub value: f64,
5118}
5119
5120impl LoadPacing {
5121    /// Create a pacing rule with the given mode and value.
5122    pub fn new(mode: LoadPacingMode, value: f64) -> Self {
5123        Self { mode, value }
5124    }
5125
5126    /// `CONSTANT_PACING`: target a per-VU iteration cycle of `cycle_millis`.
5127    pub fn constant_pacing(cycle_millis: f64) -> Self {
5128        Self::new(LoadPacingMode::ConstantPacing, cycle_millis)
5129    }
5130
5131    /// `CONSTANT_THROUGHPUT`: target `iterations_per_second` per VU.
5132    pub fn constant_throughput(iterations_per_second: f64) -> Self {
5133        Self::new(LoadPacingMode::ConstantThroughput, iterations_per_second)
5134    }
5135}
5136
5137/// The format of a [`LoadFeeder`]'s raw `data`. Maps to the feeder `format`
5138/// enum.
5139#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
5140#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
5141pub enum LoadFeederFormat {
5142    /// CSV: first line is the header row.
5143    Csv,
5144    /// JSON: an array of flat objects.
5145    Json,
5146}
5147
5148/// How a [`LoadFeeder`] selects a row each iteration. Maps to the feeder
5149/// `strategy` enum.
5150#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
5151#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
5152pub enum LoadFeederStrategy {
5153    /// Cycle rows and never exhaust (default).
5154    Circular,
5155    /// Pick a uniformly random row each iteration.
5156    Random,
5157    /// Use each row once in order; COMPLETES the run when exhausted.
5158    Sequential,
5159}
5160
5161/// Parameterized test data (a data feeder) for a load scenario: an inline
5162/// dataset from which one row is selected per iteration and exposed to the
5163/// iteration's templates as `$iteration.data.<column>`. Supply EITHER `rows`
5164/// (the primary form) OR `data` + `format`. Maps to the `LoadFeeder` schema.
5165#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
5166#[serde(rename_all = "camelCase")]
5167pub struct LoadFeeder {
5168    /// Inline dataset: a list of column-name to value maps, one per row.
5169    #[serde(skip_serializing_if = "Vec::is_empty")]
5170    pub rows: Vec<HashMap<String, String>>,
5171
5172    /// Optional raw inline dataset parsed server-side into rows per `format`.
5173    #[serde(skip_serializing_if = "Option::is_none")]
5174    pub data: Option<String>,
5175
5176    /// The format of `data` (required when `data` is set).
5177    #[serde(skip_serializing_if = "Option::is_none")]
5178    pub format: Option<LoadFeederFormat>,
5179
5180    /// How a row is chosen each iteration.
5181    #[serde(skip_serializing_if = "Option::is_none")]
5182    pub strategy: Option<LoadFeederStrategy>,
5183}
5184
5185impl LoadFeeder {
5186    /// A feeder from an inline list of column-name to value rows.
5187    pub fn rows(rows: Vec<HashMap<String, String>>) -> Self {
5188        Self {
5189            rows,
5190            ..Self::default()
5191        }
5192    }
5193
5194    /// A feeder from raw inline `data` parsed server-side as `format`.
5195    pub fn data(data: impl Into<String>, format: LoadFeederFormat) -> Self {
5196        Self {
5197            data: Some(data.into()),
5198            format: Some(format),
5199            ..Self::default()
5200        }
5201    }
5202
5203    /// Set the row-selection strategy.
5204    pub fn strategy(mut self, strategy: LoadFeederStrategy) -> Self {
5205        self.strategy = Some(strategy);
5206        self
5207    }
5208}
5209
5210/// Where a [`LoadCapture`] extracts its value from. Maps to the capture
5211/// `source` enum.
5212#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
5213#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
5214pub enum LoadCaptureSource {
5215    /// A JSONPath over the response body.
5216    BodyJsonpath,
5217    /// A response header value.
5218    Header,
5219    /// A regex over the response body string (capture group 1).
5220    BodyRegex,
5221}
5222
5223/// A declarative cross-step capture / correlation rule: extracts a value from a
5224/// step's response and binds it to a variable name a later step in the same
5225/// iteration can reference via `$iteration.captured.<name>`. Best-effort. Maps
5226/// to the `LoadCapture` schema.
5227#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
5228#[serde(rename_all = "camelCase")]
5229pub struct LoadCapture {
5230    /// The variable name later steps reference.
5231    pub name: String,
5232
5233    /// Where to extract from.
5234    pub source: LoadCaptureSource,
5235
5236    /// The JSONPath, header name, or regex driving the extraction.
5237    pub expression: String,
5238
5239    /// Optional fallback value bound when extraction yields nothing.
5240    #[serde(skip_serializing_if = "Option::is_none")]
5241    pub default_value: Option<String>,
5242}
5243
5244impl LoadCapture {
5245    /// Create a capture binding `name` to the value extracted from `source` via
5246    /// `expression`.
5247    pub fn new(
5248        name: impl Into<String>,
5249        source: LoadCaptureSource,
5250        expression: impl Into<String>,
5251    ) -> Self {
5252        Self {
5253            name: name.into(),
5254            source,
5255            expression: expression.into(),
5256            default_value: None,
5257        }
5258    }
5259
5260    /// Set the fallback value bound to the variable on no match.
5261    pub fn default_value(mut self, default_value: impl Into<String>) -> Self {
5262        self.default_value = Some(default_value.into());
5263        self
5264    }
5265}
5266
5267/// How each iteration of a load scenario selects which steps to run. Maps to
5268/// the `stepSelection` enum.
5269#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
5270#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
5271pub enum LoadStepSelection {
5272    /// Run ALL steps in declared order (a multi-step user journey).
5273    Sequential,
5274    /// Run exactly ONE step per iteration chosen at random by weight.
5275    Weighted,
5276}
5277
5278/// The load profile of a load scenario: EITHER an ordered list of [`LoadStage`]s
5279/// run in sequence, OR a single named [`LoadShape`] that expands into stages.
5280/// Maps to the `LoadProfile` schema.
5281///
5282/// Use [`LoadProfile::of`] to build from a list of stages, the convenience
5283/// constructors [`LoadProfile::constant`] / [`LoadProfile::linear`] for a single
5284/// VU stage, or [`LoadProfile::shaped`] for a named shape.
5285#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
5286#[serde(rename_all = "camelCase")]
5287pub struct LoadProfile {
5288    /// Ordered stages run one after another. Omitted (empty) when a `shape` is
5289    /// used.
5290    #[serde(skip_serializing_if = "Vec::is_empty")]
5291    pub stages: Vec<LoadStage>,
5292
5293    /// A named shape that expands server-side into stages. Use a shape OR
5294    /// `stages`, not both.
5295    #[serde(skip_serializing_if = "Option::is_none")]
5296    pub shape: Option<LoadShape>,
5297}
5298
5299impl LoadProfile {
5300    /// A profile from an explicit list of stages.
5301    pub fn of(stages: Vec<LoadStage>) -> Self {
5302        Self {
5303            stages,
5304            shape: None,
5305        }
5306    }
5307
5308    /// A profile from a single named [`LoadShape`].
5309    pub fn shaped(shape: LoadShape) -> Self {
5310        Self {
5311            stages: Vec::new(),
5312            shape: Some(shape),
5313        }
5314    }
5315
5316    /// A single VU stage holding `vus` virtual users for `duration_millis`.
5317    pub fn constant(vus: u32, duration_millis: u64) -> Self {
5318        Self::of(vec![LoadStage::vu_hold(vus, duration_millis)])
5319    }
5320
5321    /// A single linear VU ramp from `start_vus` to `end_vus` over
5322    /// `duration_millis`.
5323    pub fn linear(start_vus: u32, end_vus: u32, duration_millis: u64) -> Self {
5324        Self::of(vec![LoadStage::vu_ramp(
5325            start_vus,
5326            end_vus,
5327            duration_millis,
5328            RampCurve::Linear,
5329        )])
5330    }
5331
5332    /// Append a stage and return the profile.
5333    pub fn add_stage(mut self, stage: LoadStage) -> Self {
5334        self.stages.push(stage);
5335        self
5336    }
5337}
5338
5339/// A single templated request step in a load scenario. Maps to the `LoadStep`
5340/// schema.
5341#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
5342#[serde(rename_all = "camelCase")]
5343pub struct LoadStep {
5344    /// The templated request to fire each iteration.
5345    pub request: HttpRequest,
5346
5347    /// Optional inter-step pause (a [`Delay`]).
5348    #[serde(skip_serializing_if = "Option::is_none")]
5349    pub think_time: Option<Delay>,
5350
5351    /// Optional cross-step capture rules applied to this step's response. Each
5352    /// binds an extracted value to a variable name visible to SUBSEQUENT steps
5353    /// in the same iteration.
5354    #[serde(skip_serializing_if = "Vec::is_empty")]
5355    pub captures: Vec<LoadCapture>,
5356
5357    /// Relative selection weight, used only when the scenario's
5358    /// `stepSelection` is `WEIGHTED`. Must be > 0 when `WEIGHTED`; ignored
5359    /// under the default `SEQUENTIAL` mode.
5360    #[serde(skip_serializing_if = "Option::is_none")]
5361    pub weight: Option<f64>,
5362}
5363
5364impl LoadStep {
5365    /// Create a step from a request matcher/template.
5366    pub fn new(request: HttpRequest) -> Self {
5367        Self {
5368            request,
5369            think_time: None,
5370            captures: Vec::new(),
5371            weight: None,
5372        }
5373    }
5374
5375    /// Set the inter-step pause.
5376    pub fn think_time(mut self, delay: Delay) -> Self {
5377        self.think_time = Some(delay);
5378        self
5379    }
5380
5381    /// Append a cross-step capture rule applied to this step's response.
5382    pub fn capture(mut self, capture: LoadCapture) -> Self {
5383        self.captures.push(capture);
5384        self
5385    }
5386
5387    /// Set the relative selection weight (used only under `WEIGHTED`
5388    /// `stepSelection`).
5389    pub fn weight(mut self, weight: f64) -> Self {
5390        self.weight = Some(weight);
5391        self
5392    }
5393}
5394
5395/// An API-driven load scenario: ordered templated steps driven at a target
5396/// concurrency. Maps to the `LoadScenario` schema (the body of
5397/// `PUT /mockserver/loadScenario`, which registers the scenario in the
5398/// registry without running it). The unique [`name`](LoadScenario::name) is the
5399/// registry key used by `start`/`stop` and the per-scenario `GET`/`DELETE`
5400/// endpoints.
5401#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
5402#[serde(rename_all = "camelCase")]
5403pub struct LoadScenario {
5404    /// Human-readable scenario name.
5405    pub name: String,
5406
5407    /// Template engine for per-iteration rendering — `"VELOCITY"` (default) or
5408    /// `"MUSTACHE"`. (JavaScript is rejected for load steps.)
5409    #[serde(skip_serializing_if = "Option::is_none")]
5410    pub template_type: Option<String>,
5411
5412    /// Optional hard cap on the total number of requests dispatched.
5413    #[serde(skip_serializing_if = "Option::is_none")]
5414    pub max_requests: Option<u64>,
5415
5416    /// Optional delay (milliseconds) applied between a `start` request being
5417    /// accepted and the scenario actually beginning to drive load. Honoured by
5418    /// `PUT /mockserver/loadScenario/start`.
5419    #[serde(skip_serializing_if = "Option::is_none")]
5420    pub start_delay_millis: Option<u64>,
5421
5422    /// Optional in-run pass/fail thresholds; the run carries a PASS verdict iff
5423    /// all hold, FAIL otherwise. Empty/omitted means no verdict is computed.
5424    #[serde(skip_serializing_if = "Vec::is_empty")]
5425    pub thresholds: Vec<LoadThreshold>,
5426
5427    /// When true, a FAIL verdict aborts the run early. Default false (omitted).
5428    #[serde(skip_serializing_if = "std::ops::Not::not")]
5429    pub abort_on_fail: bool,
5430
5431    /// Suppress `abort_on_fail` for the first N milliseconds of the run so noisy
5432    /// startup samples cannot trigger a premature abort.
5433    #[serde(skip_serializing_if = "Option::is_none")]
5434    pub abort_grace_millis: Option<u64>,
5435
5436    /// Optional adaptive iteration pacing (closed-model VU loop only).
5437    #[serde(skip_serializing_if = "Option::is_none")]
5438    pub pacing: Option<LoadPacing>,
5439
5440    /// Optional parameterized test data (a data feeder).
5441    #[serde(skip_serializing_if = "Option::is_none")]
5442    pub feeder: Option<LoadFeeder>,
5443
5444    /// How each iteration selects which steps to run — `SEQUENTIAL` (default,
5445    /// omitted) or `WEIGHTED`.
5446    #[serde(skip_serializing_if = "Option::is_none")]
5447    pub step_selection: Option<LoadStepSelection>,
5448
5449    /// The ramp profile.
5450    pub profile: LoadProfile,
5451
5452    /// Ordered list of request steps fired in sequence each iteration (max 50).
5453    pub steps: Vec<LoadStep>,
5454}
5455
5456impl LoadScenario {
5457    /// Create a scenario with the given name, profile and steps.
5458    pub fn new(name: impl Into<String>, profile: LoadProfile, steps: Vec<LoadStep>) -> Self {
5459        Self {
5460            name: name.into(),
5461            template_type: None,
5462            max_requests: None,
5463            start_delay_millis: None,
5464            thresholds: Vec::new(),
5465            abort_on_fail: false,
5466            abort_grace_millis: None,
5467            pacing: None,
5468            feeder: None,
5469            step_selection: None,
5470            profile,
5471            steps,
5472        }
5473    }
5474
5475    /// Add an in-run pass/fail threshold.
5476    pub fn threshold(mut self, threshold: LoadThreshold) -> Self {
5477        self.thresholds.push(threshold);
5478        self
5479    }
5480
5481    /// Set whether a FAIL verdict aborts the run early.
5482    pub fn abort_on_fail(mut self, abort_on_fail: bool) -> Self {
5483        self.abort_on_fail = abort_on_fail;
5484        self
5485    }
5486
5487    /// Set the abort grace window (milliseconds) for `abort_on_fail`.
5488    pub fn abort_grace_millis(mut self, abort_grace_millis: u64) -> Self {
5489        self.abort_grace_millis = Some(abort_grace_millis);
5490        self
5491    }
5492
5493    /// Set the adaptive iteration pacing.
5494    pub fn pacing(mut self, pacing: LoadPacing) -> Self {
5495        self.pacing = Some(pacing);
5496        self
5497    }
5498
5499    /// Set the parameterized test data feeder.
5500    pub fn feeder(mut self, feeder: LoadFeeder) -> Self {
5501        self.feeder = Some(feeder);
5502        self
5503    }
5504
5505    /// Set how each iteration selects which steps to run.
5506    pub fn step_selection(mut self, step_selection: LoadStepSelection) -> Self {
5507        self.step_selection = Some(step_selection);
5508        self
5509    }
5510
5511    /// Set the template engine (`"VELOCITY"` or `"MUSTACHE"`).
5512    pub fn template_type(mut self, template_type: impl Into<String>) -> Self {
5513        self.template_type = Some(template_type.into());
5514        self
5515    }
5516
5517    /// Set the hard cap on total requests dispatched.
5518    pub fn max_requests(mut self, max_requests: u64) -> Self {
5519        self.max_requests = Some(max_requests);
5520        self
5521    }
5522
5523    /// Set the delay (milliseconds) before the scenario begins driving load
5524    /// once started.
5525    pub fn start_delay_millis(mut self, start_delay_millis: u64) -> Self {
5526        self.start_delay_millis = Some(start_delay_millis);
5527        self
5528    }
5529}
5530
5531// ---------------------------------------------------------------------------
5532// SLO verdicts (PUT /mockserver/verifySLO)
5533// ---------------------------------------------------------------------------
5534
5535/// A single service-level objective over the recorded SLI samples. Maps to the
5536/// `SloObjective` schema.
5537#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
5538#[serde(rename_all = "camelCase")]
5539pub struct SloObjective {
5540    /// The indicator to evaluate — one of `LATENCY_P50`, `LATENCY_P95`,
5541    /// `LATENCY_P99`, `ERROR_RATE`.
5542    pub sli: String,
5543
5544    /// How the observed value is compared to the threshold — one of
5545    /// `LESS_THAN`, `LESS_THAN_OR_EQUAL`, `GREATER_THAN`,
5546    /// `GREATER_THAN_OR_EQUAL`.
5547    pub comparator: String,
5548
5549    /// The objective threshold (milliseconds for latency SLIs, a 0.0–1.0
5550    /// fraction for `ERROR_RATE`).
5551    pub threshold: f64,
5552
5553    /// Which recorded traffic to evaluate — `"FORWARD"` (default) or
5554    /// `"INBOUND"`.
5555    #[serde(skip_serializing_if = "Option::is_none")]
5556    pub scope: Option<String>,
5557}
5558
5559impl SloObjective {
5560    /// Create an objective.
5561    pub fn new(sli: impl Into<String>, comparator: impl Into<String>, threshold: f64) -> Self {
5562        Self {
5563            sli: sli.into(),
5564            comparator: comparator.into(),
5565            threshold,
5566            scope: None,
5567        }
5568    }
5569
5570    /// Set the evaluation scope (`"FORWARD"` or `"INBOUND"`).
5571    pub fn scope(mut self, scope: impl Into<String>) -> Self {
5572        self.scope = Some(scope.into());
5573        self
5574    }
5575}
5576
5577/// The time window of an SLO evaluation. Maps to the `SloCriteria.window`
5578/// object.
5579#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
5580#[serde(rename_all = "camelCase")]
5581pub struct SloWindow {
5582    /// `"LOOKBACK"` (default) or `"EXPLICIT"`.
5583    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
5584    pub window_type: Option<String>,
5585
5586    /// LOOKBACK: window length ending now.
5587    #[serde(skip_serializing_if = "Option::is_none")]
5588    pub lookback_millis: Option<u64>,
5589
5590    /// EXPLICIT: window start in epoch milliseconds.
5591    #[serde(skip_serializing_if = "Option::is_none")]
5592    pub from_epoch_millis: Option<u64>,
5593
5594    /// EXPLICIT: window end in epoch milliseconds.
5595    #[serde(skip_serializing_if = "Option::is_none")]
5596    pub to_epoch_millis: Option<u64>,
5597}
5598
5599impl SloWindow {
5600    /// A LOOKBACK window of `millis` ending now.
5601    pub fn lookback(millis: u64) -> Self {
5602        Self {
5603            window_type: Some("LOOKBACK".to_string()),
5604            lookback_millis: Some(millis),
5605            ..Default::default()
5606        }
5607    }
5608
5609    /// An EXPLICIT window between two epoch-millisecond bounds.
5610    pub fn explicit(from_epoch_millis: u64, to_epoch_millis: u64) -> Self {
5611        Self {
5612            window_type: Some("EXPLICIT".to_string()),
5613            from_epoch_millis: Some(from_epoch_millis),
5614            to_epoch_millis: Some(to_epoch_millis),
5615            ..Default::default()
5616        }
5617    }
5618}
5619
5620/// A named set of service-level objectives over a time window. Maps to the
5621/// `SloCriteria` schema (the body of `PUT /mockserver/verifySLO`).
5622#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
5623#[serde(rename_all = "camelCase")]
5624pub struct SloCriteria {
5625    /// Human-readable criteria name, echoed back in the verdict.
5626    #[serde(skip_serializing_if = "Option::is_none")]
5627    pub name: Option<String>,
5628
5629    /// The time window to evaluate over.
5630    #[serde(skip_serializing_if = "Option::is_none")]
5631    pub window: Option<SloWindow>,
5632
5633    /// Minimum samples required in the window; below this the verdict is
5634    /// INCONCLUSIVE.
5635    #[serde(skip_serializing_if = "Option::is_none")]
5636    pub minimum_sample_count: Option<u64>,
5637
5638    /// Optional list of upstream hosts to restrict the evaluation to.
5639    #[serde(skip_serializing_if = "Option::is_none")]
5640    pub upstream_hosts: Option<Vec<String>>,
5641
5642    /// The objectives (the verdict is the logical AND of all of them).
5643    pub objectives: Vec<SloObjective>,
5644}
5645
5646impl SloCriteria {
5647    /// Create criteria from a set of objectives.
5648    pub fn new(objectives: Vec<SloObjective>) -> Self {
5649        Self {
5650            name: None,
5651            window: None,
5652            minimum_sample_count: None,
5653            upstream_hosts: None,
5654            objectives,
5655        }
5656    }
5657
5658    /// Set the criteria name.
5659    pub fn name(mut self, name: impl Into<String>) -> Self {
5660        self.name = Some(name.into());
5661        self
5662    }
5663
5664    /// Set the evaluation window.
5665    pub fn window(mut self, window: SloWindow) -> Self {
5666        self.window = Some(window);
5667        self
5668    }
5669
5670    /// Set the minimum sample count.
5671    pub fn minimum_sample_count(mut self, count: u64) -> Self {
5672        self.minimum_sample_count = Some(count);
5673        self
5674    }
5675
5676    /// Restrict the evaluation to the given upstream hosts.
5677    pub fn upstream_hosts(mut self, hosts: Vec<String>) -> Self {
5678        self.upstream_hosts = Some(hosts);
5679        self
5680    }
5681}
5682
5683/// The evaluated result of a single objective. Maps to the `SloObjectiveResult`
5684/// schema.
5685#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
5686#[serde(rename_all = "camelCase")]
5687pub struct SloObjectiveResult {
5688    #[serde(skip_serializing_if = "Option::is_none")]
5689    pub sli: Option<String>,
5690    #[serde(skip_serializing_if = "Option::is_none")]
5691    pub comparator: Option<String>,
5692    #[serde(skip_serializing_if = "Option::is_none")]
5693    pub threshold: Option<f64>,
5694    #[serde(skip_serializing_if = "Option::is_none")]
5695    pub observed_value: Option<f64>,
5696    /// `PASS`, `FAIL` or `INCONCLUSIVE`.
5697    #[serde(skip_serializing_if = "Option::is_none")]
5698    pub result: Option<String>,
5699    #[serde(skip_serializing_if = "Option::is_none")]
5700    pub detail: Option<String>,
5701}
5702
5703/// The overall verdict of an SLO evaluation. Maps to the `SloVerdict` schema —
5704/// the response of `PUT /mockserver/verifySLO`.
5705#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
5706#[serde(rename_all = "camelCase")]
5707pub struct SloVerdict {
5708    #[serde(skip_serializing_if = "Option::is_none")]
5709    pub name: Option<String>,
5710    /// `PASS`, `FAIL` or `INCONCLUSIVE`.
5711    #[serde(skip_serializing_if = "Option::is_none")]
5712    pub result: Option<String>,
5713    #[serde(skip_serializing_if = "Option::is_none")]
5714    pub window_from_epoch_millis: Option<u64>,
5715    #[serde(skip_serializing_if = "Option::is_none")]
5716    pub window_to_epoch_millis: Option<u64>,
5717    #[serde(skip_serializing_if = "Option::is_none")]
5718    pub sample_count: Option<u64>,
5719    #[serde(default, skip_serializing_if = "Vec::is_empty")]
5720    pub objective_results: Vec<SloObjectiveResult>,
5721}
5722
5723impl SloVerdict {
5724    /// Whether the overall verdict is `PASS`.
5725    pub fn is_pass(&self) -> bool {
5726        self.result.as_deref() == Some("PASS")
5727    }
5728
5729    /// Whether the overall verdict is `FAIL`.
5730    pub fn is_fail(&self) -> bool {
5731        self.result.as_deref() == Some("FAIL")
5732    }
5733
5734    /// Whether the overall verdict is `INCONCLUSIVE`.
5735    pub fn is_inconclusive(&self) -> bool {
5736        self.result.as_deref() == Some("INCONCLUSIVE")
5737    }
5738}
5739
5740// ---------------------------------------------------------------------------
5741// Preemption (PUT/GET/DELETE /mockserver/preemption)
5742// ---------------------------------------------------------------------------
5743
5744/// Preemption simulation parameters (all fields optional). Maps to the
5745/// `PreemptionRequest` schema (the body of `PUT /mockserver/preemption`).
5746#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
5747#[serde(rename_all = "camelCase")]
5748pub struct PreemptionRequest {
5749    /// How draining is signalled — `"reject503"`, `"goaway"` or `"both"`
5750    /// (default).
5751    #[serde(skip_serializing_if = "Option::is_none")]
5752    pub mode: Option<String>,
5753
5754    /// How long in-flight requests are allowed to drain (clamped server-side).
5755    #[serde(skip_serializing_if = "Option::is_none")]
5756    pub drain_millis: Option<u64>,
5757
5758    /// Auto-uncordon after this many milliseconds (dead-man's switch); `0`
5759    /// (default) means no auto-uncordon.
5760    #[serde(skip_serializing_if = "Option::is_none")]
5761    pub ttl_millis: Option<u64>,
5762
5763    /// HTTP/2 GOAWAY `last_stream_id` to advertise; `-1` (default) lets the
5764    /// server choose.
5765    #[serde(skip_serializing_if = "Option::is_none")]
5766    pub last_stream_id: Option<i64>,
5767}
5768
5769impl PreemptionRequest {
5770    /// An empty request (server defaults: mode "both", default drain, no TTL).
5771    pub fn new() -> Self {
5772        Self::default()
5773    }
5774
5775    /// Set the signalling mode (`"reject503"`, `"goaway"` or `"both"`).
5776    pub fn mode(mut self, mode: impl Into<String>) -> Self {
5777        self.mode = Some(mode.into());
5778        self
5779    }
5780
5781    /// Set the drain window in milliseconds.
5782    pub fn drain_millis(mut self, millis: u64) -> Self {
5783        self.drain_millis = Some(millis);
5784        self
5785    }
5786
5787    /// Set the auto-uncordon TTL in milliseconds.
5788    pub fn ttl_millis(mut self, millis: u64) -> Self {
5789        self.ttl_millis = Some(millis);
5790        self
5791    }
5792
5793    /// Set the HTTP/2 GOAWAY `last_stream_id` to advertise.
5794    pub fn last_stream_id(mut self, id: i64) -> Self {
5795        self.last_stream_id = Some(id);
5796        self
5797    }
5798}
5799
5800/// The current cordon/drain status of the server. Maps to the
5801/// `PreemptionStatus` schema — the response of `PUT`/`GET /mockserver/preemption`.
5802#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
5803#[serde(rename_all = "camelCase")]
5804pub struct PreemptionStatus {
5805    /// `"inactive"`, `"draining"` or `"drained"`.
5806    #[serde(skip_serializing_if = "Option::is_none")]
5807    pub state: Option<String>,
5808
5809    /// Number of requests currently in flight.
5810    #[serde(skip_serializing_if = "Option::is_none")]
5811    pub in_flight: Option<u64>,
5812
5813    /// Milliseconds left in the drain window.
5814    #[serde(skip_serializing_if = "Option::is_none")]
5815    pub drain_remaining_millis: Option<u64>,
5816
5817    /// Active signalling mode (omitted when inactive).
5818    #[serde(skip_serializing_if = "Option::is_none")]
5819    pub mode: Option<String>,
5820}
5821
5822// ---------------------------------------------------------------------------
5823// Service chaos (PUT /mockserver/serviceChaos)
5824// ---------------------------------------------------------------------------
5825
5826/// An HTTP chaos / fault-injection profile for a host or expectation. Maps to
5827/// the `HttpChaosProfile` schema. Captures the commonly-used fields; the model
5828/// carries an `extra` map for any additional server-supported keys.
5829#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
5830#[serde(rename_all = "camelCase")]
5831pub struct HttpChaosProfile {
5832    /// HTTP error status code to return instead of the real response.
5833    #[serde(skip_serializing_if = "Option::is_none")]
5834    pub error_status: Option<u16>,
5835
5836    /// Probability (0.0–1.0) that a request triggers the error.
5837    #[serde(skip_serializing_if = "Option::is_none")]
5838    pub error_probability: Option<f64>,
5839
5840    /// Injected latency (a [`Delay`]).
5841    #[serde(skip_serializing_if = "Option::is_none")]
5842    pub latency: Option<Delay>,
5843
5844    /// When true, drops the TCP connection without responding.
5845    #[serde(skip_serializing_if = "Option::is_none")]
5846    pub connection_drop: Option<bool>,
5847
5848    /// Fixed seed for deterministic probabilistic outcomes.
5849    #[serde(skip_serializing_if = "Option::is_none")]
5850    pub seed: Option<i64>,
5851
5852    /// Literal `Retry-After` header value returned with an injected error.
5853    #[serde(skip_serializing_if = "Option::is_none")]
5854    pub retry_after: Option<String>,
5855
5856    /// Probability (0.0–1.0) of dropping the TCP connection without responding.
5857    #[serde(skip_serializing_if = "Option::is_none")]
5858    pub drop_connection_probability: Option<f64>,
5859
5860    /// Let the first N requests succeed before chaos becomes active.
5861    #[serde(skip_serializing_if = "Option::is_none")]
5862    pub succeed_first: Option<i64>,
5863
5864    /// Number of requests to fail once chaos is active.
5865    #[serde(skip_serializing_if = "Option::is_none")]
5866    pub fail_request_count: Option<i64>,
5867
5868    /// Time-based outage: chaos activates this many ms after first match.
5869    #[serde(skip_serializing_if = "Option::is_none")]
5870    pub outage_after_millis: Option<i64>,
5871
5872    /// Time-based outage: chaos stays active this many ms then self-heals.
5873    #[serde(skip_serializing_if = "Option::is_none")]
5874    pub outage_duration_millis: Option<i64>,
5875
5876    /// Keep only this leading fraction (0.0–1.0) of the response body.
5877    #[serde(skip_serializing_if = "Option::is_none")]
5878    pub truncate_body_at_fraction: Option<f64>,
5879
5880    /// Corrupt the response body so it fails to parse.
5881    #[serde(skip_serializing_if = "Option::is_none")]
5882    pub malformed_body: Option<bool>,
5883
5884    /// Dribble the response body in chunks of this many bytes.
5885    #[serde(skip_serializing_if = "Option::is_none")]
5886    pub slow_response_chunk_size: Option<i64>,
5887
5888    /// Delay between slow-response chunks.
5889    #[serde(skip_serializing_if = "Option::is_none")]
5890    pub slow_response_chunk_delay: Option<Delay>,
5891
5892    /// Shared quota counter key.
5893    #[serde(skip_serializing_if = "Option::is_none")]
5894    pub quota_name: Option<String>,
5895
5896    /// Max requests allowed per quota window.
5897    #[serde(skip_serializing_if = "Option::is_none")]
5898    pub quota_limit: Option<i64>,
5899
5900    /// Quota fixed-window length in milliseconds.
5901    #[serde(skip_serializing_if = "Option::is_none")]
5902    pub quota_window_millis: Option<i64>,
5903
5904    /// Status returned when the quota is exceeded (default 429).
5905    #[serde(skip_serializing_if = "Option::is_none")]
5906    pub quota_error_status: Option<u16>,
5907
5908    /// Ramp error/drop probabilities linearly over this many ms from first match.
5909    #[serde(skip_serializing_if = "Option::is_none")]
5910    pub degradation_ramp_millis: Option<i64>,
5911
5912    /// Rewrite the response body as a GraphQL error envelope.
5913    #[serde(skip_serializing_if = "Option::is_none")]
5914    pub graphql_errors: Option<bool>,
5915
5916    /// Message in `errors[0].message` of the GraphQL error envelope.
5917    #[serde(skip_serializing_if = "Option::is_none")]
5918    pub graphql_error_message: Option<String>,
5919
5920    /// Value for `errors[0].extensions.code`.
5921    #[serde(skip_serializing_if = "Option::is_none")]
5922    pub graphql_error_code: Option<String>,
5923
5924    /// Whether `data` is null (default true) in the GraphQL error envelope.
5925    #[serde(skip_serializing_if = "Option::is_none")]
5926    pub graphql_nullify_data: Option<bool>,
5927
5928    /// Any additional fields the server supports that are not modelled above.
5929    #[serde(flatten)]
5930    pub extra: HashMap<String, serde_json::Value>,
5931}
5932
5933impl HttpChaosProfile {
5934    /// Create an empty chaos profile.
5935    pub fn new() -> Self {
5936        Self::default()
5937    }
5938
5939    /// Set the error status code returned on fault.
5940    pub fn error_status(mut self, status: u16) -> Self {
5941        self.error_status = Some(status);
5942        self
5943    }
5944
5945    /// Set the probability (0.0–1.0) of triggering the error.
5946    pub fn error_probability(mut self, probability: f64) -> Self {
5947        self.error_probability = Some(probability);
5948        self
5949    }
5950
5951    /// Set the injected latency.
5952    pub fn latency(mut self, latency: Delay) -> Self {
5953        self.latency = Some(latency);
5954        self
5955    }
5956
5957    /// Drop the TCP connection without responding.
5958    pub fn connection_drop(mut self, drop: bool) -> Self {
5959        self.connection_drop = Some(drop);
5960        self
5961    }
5962
5963    /// Set the deterministic seed.
5964    pub fn seed(mut self, seed: i64) -> Self {
5965        self.seed = Some(seed);
5966        self
5967    }
5968}
5969
5970// ---------------------------------------------------------------------------
5971// Chaos experiment (PUT /mockserver/chaosExperiment)
5972// ---------------------------------------------------------------------------
5973
5974/// A single stage of a chaos experiment. Maps to a `ChaosExperiment.stages[]`
5975/// entry.
5976#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
5977#[serde(rename_all = "camelCase")]
5978pub struct ChaosStage {
5979    /// How long this stage runs before advancing (max 86_400_000 = 24h).
5980    pub duration_millis: u64,
5981
5982    /// Map of host -> chaos profile to apply during this stage.
5983    pub profiles: HashMap<String, HttpChaosProfile>,
5984}
5985
5986impl ChaosStage {
5987    /// Create a stage running for `duration_millis`.
5988    pub fn new(duration_millis: u64) -> Self {
5989        Self {
5990            duration_millis,
5991            profiles: HashMap::new(),
5992        }
5993    }
5994
5995    /// Add a host -> chaos profile to apply during the stage.
5996    pub fn profile(mut self, host: impl Into<String>, profile: HttpChaosProfile) -> Self {
5997        self.profiles.insert(host.into(), profile);
5998        self
5999    }
6000}
6001
6002/// A scheduled multi-stage chaos experiment definition. Maps to the
6003/// `ChaosExperiment` schema (the body of `PUT /mockserver/chaosExperiment`).
6004#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
6005#[serde(rename_all = "camelCase")]
6006pub struct ChaosExperiment {
6007    /// Human-readable experiment name.
6008    #[serde(skip_serializing_if = "Option::is_none")]
6009    pub name: Option<String>,
6010
6011    /// Whether to loop back to stage 0 after the last stage completes (default
6012    /// false). Serialized as `loop` on the wire.
6013    #[serde(rename = "loop", skip_serializing_if = "Option::is_none")]
6014    pub loop_back: Option<bool>,
6015
6016    /// The ordered sequence of stages.
6017    pub stages: Vec<ChaosStage>,
6018}
6019
6020impl ChaosExperiment {
6021    /// Create an experiment from an ordered list of stages.
6022    pub fn new(stages: Vec<ChaosStage>) -> Self {
6023        Self {
6024            name: None,
6025            loop_back: None,
6026            stages,
6027        }
6028    }
6029
6030    /// Set the experiment name.
6031    pub fn name(mut self, name: impl Into<String>) -> Self {
6032        self.name = Some(name.into());
6033        self
6034    }
6035
6036    /// Set whether the experiment loops back to the first stage.
6037    pub fn loop_back(mut self, loop_back: bool) -> Self {
6038        self.loop_back = Some(loop_back);
6039        self
6040    }
6041}
6042
6043// ---------------------------------------------------------------------------
6044// Tests
6045// ---------------------------------------------------------------------------
6046
6047#[cfg(test)]
6048mod tests {
6049    use super::*;
6050
6051    #[test]
6052    fn test_grpc_services_deserialize_from_server_wire_shape() {
6053        // Mirrors the JSON array produced by `PUT /mockserver/grpc/services`
6054        // in mockserver-core HttpState.java (camelCase keys, full type names).
6055        let wire = r#"[
6056            {
6057                "name": "helloworld.Greeter",
6058                "methods": [
6059                    {
6060                        "name": "SayHello",
6061                        "inputType": "helloworld.HelloRequest",
6062                        "outputType": "helloworld.HelloReply",
6063                        "clientStreaming": false,
6064                        "serverStreaming": false
6065                    },
6066                    {
6067                        "name": "LotsOfReplies",
6068                        "inputType": "helloworld.HelloRequest",
6069                        "outputType": "helloworld.HelloReply",
6070                        "clientStreaming": false,
6071                        "serverStreaming": true
6072                    }
6073                ]
6074            }
6075        ]"#;
6076
6077        let services: Vec<GrpcService> = serde_json::from_str(wire).unwrap();
6078        assert_eq!(services.len(), 1);
6079        let svc = &services[0];
6080        assert_eq!(svc.name, "helloworld.Greeter");
6081        assert_eq!(svc.methods.len(), 2);
6082
6083        let unary = &svc.methods[0];
6084        assert_eq!(unary.name, "SayHello");
6085        assert_eq!(unary.input_type, "helloworld.HelloRequest");
6086        assert_eq!(unary.output_type, "helloworld.HelloReply");
6087        assert!(!unary.client_streaming);
6088        assert!(!unary.server_streaming);
6089
6090        let server_stream = &svc.methods[1];
6091        assert_eq!(server_stream.name, "LotsOfReplies");
6092        assert!(!server_stream.client_streaming);
6093        assert!(server_stream.server_streaming);
6094    }
6095
6096    #[test]
6097    fn test_grpc_method_serializes_with_camel_case_keys() {
6098        let method = GrpcMethod {
6099            name: "BidiChat".into(),
6100            input_type: "chat.Message".into(),
6101            output_type: "chat.Message".into(),
6102            client_streaming: true,
6103            server_streaming: true,
6104        };
6105        let value = serde_json::to_value(&method).unwrap();
6106        assert_eq!(value["name"], "BidiChat");
6107        assert_eq!(value["inputType"], "chat.Message");
6108        assert_eq!(value["outputType"], "chat.Message");
6109        assert_eq!(value["clientStreaming"], true);
6110        assert_eq!(value["serverStreaming"], true);
6111    }
6112
6113    #[test]
6114    fn test_grpc_services_empty_array() {
6115        let services: Vec<GrpcService> = serde_json::from_str("[]").unwrap();
6116        assert!(services.is_empty());
6117    }
6118
6119    #[test]
6120    fn test_grpc_service_round_trips() {
6121        let original = GrpcService {
6122            name: "helloworld.Greeter".into(),
6123            methods: vec![GrpcMethod {
6124                name: "SayHello".into(),
6125                input_type: "helloworld.HelloRequest".into(),
6126                output_type: "helloworld.HelloReply".into(),
6127                client_streaming: false,
6128                server_streaming: false,
6129            }],
6130        };
6131        let json = serde_json::to_string(&original).unwrap();
6132        let parsed: GrpcService = serde_json::from_str(&json).unwrap();
6133        assert_eq!(original, parsed);
6134    }
6135}