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