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// ---------------------------------------------------------------------------
11// HttpRequest
12// ---------------------------------------------------------------------------
13
14/// Matcher for an HTTP request. Uses builder methods for fluent construction.
15///
16/// # Example
17/// ```
18/// use mockserver_client::HttpRequest;
19///
20/// let request = HttpRequest::new()
21///     .method("POST")
22///     .path("/api/users")
23///     .header("Content-Type", "application/json")
24///     .query_param("page", "1")
25///     .body("{}");
26/// ```
27#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
28#[serde(rename_all = "camelCase")]
29pub struct HttpRequest {
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub method: Option<String>,
32
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub path: Option<String>,
35
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub query_string_parameters: Option<HashMap<String, Vec<String>>>,
38
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub headers: Option<HashMap<String, Vec<String>>>,
41
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub body: Option<Body>,
44
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub socket_address: Option<SocketAddress>,
47}
48
49impl HttpRequest {
50    /// Create a new empty request matcher.
51    pub fn new() -> Self {
52        Self::default()
53    }
54
55    /// Set the downstream socket address to connect to.
56    ///
57    /// Used by load-scenario steps (and forwarded/proxied requests) to direct
58    /// the rendered request at a specific host/port/scheme rather than relying
59    /// on the request's `Host` header.
60    pub fn socket_address(mut self, socket_address: SocketAddress) -> Self {
61        self.socket_address = Some(socket_address);
62        self
63    }
64
65    /// Set the HTTP method to match.
66    pub fn method(mut self, method: impl Into<String>) -> Self {
67        self.method = Some(method.into());
68        self
69    }
70
71    /// Set the path to match.
72    pub fn path(mut self, path: impl Into<String>) -> Self {
73        self.path = Some(path.into());
74        self
75    }
76
77    /// Add a query string parameter (multiple values per key supported).
78    pub fn query_param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
79        let params = self
80            .query_string_parameters
81            .get_or_insert_with(HashMap::new);
82        params.entry(key.into()).or_default().push(value.into());
83        self
84    }
85
86    /// Add a header (multiple values per key supported).
87    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
88        let headers = self.headers.get_or_insert_with(HashMap::new);
89        headers.entry(key.into()).or_default().push(value.into());
90        self
91    }
92
93    /// Set a plain string body matcher.
94    pub fn body(mut self, body: impl Into<String>) -> Self {
95        self.body = Some(Body::Plain(body.into()));
96        self
97    }
98
99    /// Set a typed JSON body matcher.
100    pub fn json_body(mut self, json: serde_json::Value) -> Self {
101        self.body = Some(Body::Typed {
102            body_type: "JSON".to_string(),
103            json: json.to_string(),
104        });
105        self
106    }
107
108    /// Set a file body (type "FILE") with optional content type and template type.
109    ///
110    /// Use [`Body::file`] for richer construction if you need content type or
111    /// template type set.
112    pub fn file_body(mut self, file_path: impl Into<String>) -> Self {
113        self.body = Some(Body::File {
114            file_path: file_path.into(),
115            content_type: None,
116            template_type: None,
117        });
118        self
119    }
120
121    /// Set a pre-built [`Body`] value (use with [`Body::file`] for FILE bodies).
122    pub fn body_value(mut self, body: Body) -> Self {
123        self.body = Some(body);
124        self
125    }
126}
127
128// ---------------------------------------------------------------------------
129// Body
130// ---------------------------------------------------------------------------
131
132/// Request/response body — either a plain string, a typed object, or a file reference.
133#[derive(Debug, Clone, PartialEq)]
134pub enum Body {
135    /// A plain string body.
136    Plain(String),
137    /// A typed body (e.g., JSON).
138    Typed { body_type: String, json: String },
139    /// A file body (`type: "FILE"`), with optional template evaluation.
140    File {
141        file_path: String,
142        content_type: Option<String>,
143        template_type: Option<String>,
144    },
145}
146
147impl Body {
148    /// Create a FILE body referencing a path on the server filesystem.
149    ///
150    /// # Example
151    /// ```
152    /// use mockserver_client::Body;
153    ///
154    /// let body = Body::file("/data/response.json")
155    ///     .with_content_type("application/json")
156    ///     .with_template_type("VELOCITY");
157    /// ```
158    pub fn file(file_path: impl Into<String>) -> Self {
159        Body::File {
160            file_path: file_path.into(),
161            content_type: None,
162            template_type: None,
163        }
164    }
165
166    /// Set the content type on a FILE body. No-op on other variants.
167    pub fn with_content_type(mut self, content_type: impl Into<String>) -> Self {
168        if let Body::File {
169            content_type: ref mut ct,
170            ..
171        } = self
172        {
173            *ct = Some(content_type.into());
174        }
175        self
176    }
177
178    /// Set the template type (e.g., "VELOCITY", "MUSTACHE") on a FILE body.
179    /// No-op on other variants.
180    pub fn with_template_type(mut self, template_type: impl Into<String>) -> Self {
181        if let Body::File {
182            template_type: ref mut tt,
183            ..
184        } = self
185        {
186            *tt = Some(template_type.into());
187        }
188        self
189    }
190}
191
192impl Serialize for Body {
193    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
194    where
195        S: serde::Serializer,
196    {
197        match self {
198            Body::Plain(s) => serializer.serialize_str(s),
199            Body::Typed { body_type, json } => {
200                use serde::ser::SerializeMap;
201                let mut map = serializer.serialize_map(Some(2))?;
202                map.serialize_entry("type", body_type)?;
203                map.serialize_entry("json", json)?;
204                map.end()
205            }
206            Body::File {
207                file_path,
208                content_type,
209                template_type,
210            } => {
211                use serde::ser::SerializeMap;
212                let count = 2
213                    + content_type.as_ref().map_or(0, |_| 1)
214                    + template_type.as_ref().map_or(0, |_| 1);
215                let mut map = serializer.serialize_map(Some(count))?;
216                map.serialize_entry("type", "FILE")?;
217                map.serialize_entry("filePath", file_path)?;
218                if let Some(ct) = content_type {
219                    map.serialize_entry("contentType", ct)?;
220                }
221                if let Some(tt) = template_type {
222                    map.serialize_entry("templateType", tt)?;
223                }
224                map.end()
225            }
226        }
227    }
228}
229
230impl<'de> Deserialize<'de> for Body {
231    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
232    where
233        D: serde::Deserializer<'de>,
234    {
235        use serde_json::Value;
236        let v = Value::deserialize(deserializer)?;
237        match v {
238            Value::String(s) => Ok(Body::Plain(s)),
239            Value::Object(map) => {
240                let body_type = map
241                    .get("type")
242                    .and_then(|v| v.as_str())
243                    .unwrap_or("JSON")
244                    .to_string();
245                if body_type == "FILE" {
246                    let file_path = map
247                        .get("filePath")
248                        .and_then(|v| v.as_str())
249                        .unwrap_or("")
250                        .to_string();
251                    let content_type = map
252                        .get("contentType")
253                        .and_then(|v| v.as_str())
254                        .map(|s| s.to_string());
255                    let template_type = map
256                        .get("templateType")
257                        .and_then(|v| v.as_str())
258                        .map(|s| s.to_string());
259                    Ok(Body::File {
260                        file_path,
261                        content_type,
262                        template_type,
263                    })
264                } else {
265                    let json = map
266                        .get("json")
267                        .and_then(|v| v.as_str())
268                        .unwrap_or("")
269                        .to_string();
270                    Ok(Body::Typed { body_type, json })
271                }
272            }
273            _ => Ok(Body::Plain(v.to_string())),
274        }
275    }
276}
277
278// ---------------------------------------------------------------------------
279// HttpResponse
280// ---------------------------------------------------------------------------
281
282/// Builder for an HTTP response action.
283///
284/// # Example
285/// ```
286/// use mockserver_client::HttpResponse;
287///
288/// let response = HttpResponse::new()
289///     .status_code(201)
290///     .header("Location", "/api/users/42")
291///     .body("{\"id\": 42}");
292/// ```
293#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
294#[serde(rename_all = "camelCase")]
295pub struct HttpResponse {
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub status_code: Option<u16>,
298
299    #[serde(skip_serializing_if = "Option::is_none")]
300    pub headers: Option<HashMap<String, Vec<String>>>,
301
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub body: Option<String>,
304
305    #[serde(skip_serializing_if = "Option::is_none")]
306    pub delay: Option<Delay>,
307}
308
309impl HttpResponse {
310    /// Create a new empty response.
311    pub fn new() -> Self {
312        Self::default()
313    }
314
315    /// Set the HTTP status code.
316    pub fn status_code(mut self, code: u16) -> Self {
317        self.status_code = Some(code);
318        self
319    }
320
321    /// Add a response header.
322    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
323        let headers = self.headers.get_or_insert_with(HashMap::new);
324        headers.entry(key.into()).or_default().push(value.into());
325        self
326    }
327
328    /// Set the response body as a string.
329    pub fn body(mut self, body: impl Into<String>) -> Self {
330        self.body = Some(body.into());
331        self
332    }
333
334    /// Set a response delay.
335    pub fn delay(mut self, delay: Delay) -> Self {
336        self.delay = Some(delay);
337        self
338    }
339}
340
341// ---------------------------------------------------------------------------
342// HttpTemplate (response or forward)
343// ---------------------------------------------------------------------------
344
345/// Template action — evaluate a response or forward template (Velocity, Mustache, etc.).
346///
347/// Used as `httpResponseTemplate` or `httpForwardTemplate` in an expectation.
348///
349/// # Example
350/// ```
351/// use mockserver_client::HttpTemplate;
352///
353/// let tmpl = HttpTemplate::new("VELOCITY", "{ \"statusCode\": 200 }")
354///     .template_file("/path/to/template.vm");
355/// ```
356#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
357#[serde(rename_all = "camelCase")]
358pub struct HttpTemplate {
359    #[serde(skip_serializing_if = "Option::is_none")]
360    pub template_type: Option<String>,
361
362    #[serde(skip_serializing_if = "Option::is_none")]
363    pub template: Option<String>,
364
365    #[serde(skip_serializing_if = "Option::is_none")]
366    pub template_file: Option<String>,
367}
368
369impl HttpTemplate {
370    /// Create a template action with the given type and inline template body.
371    pub fn new(template_type: impl Into<String>, template: impl Into<String>) -> Self {
372        Self {
373            template_type: Some(template_type.into()),
374            template: Some(template.into()),
375            template_file: None,
376        }
377    }
378
379    /// Create a template action that loads from a file path.
380    pub fn from_file(template_type: impl Into<String>, file_path: impl Into<String>) -> Self {
381        Self {
382            template_type: Some(template_type.into()),
383            template: None,
384            template_file: Some(file_path.into()),
385        }
386    }
387
388    /// Set the template type (e.g., "VELOCITY", "MUSTACHE").
389    pub fn template_type(mut self, template_type: impl Into<String>) -> Self {
390        self.template_type = Some(template_type.into());
391        self
392    }
393
394    /// Set the inline template body.
395    pub fn template(mut self, template: impl Into<String>) -> Self {
396        self.template = Some(template.into());
397        self
398    }
399
400    /// Set the template file path (alternative to inline template).
401    pub fn template_file(mut self, file_path: impl Into<String>) -> Self {
402        self.template_file = Some(file_path.into());
403        self
404    }
405}
406
407// ---------------------------------------------------------------------------
408// HttpForward
409// ---------------------------------------------------------------------------
410
411/// Forward action — proxy the matched request to another host.
412///
413/// # Example
414/// ```
415/// use mockserver_client::HttpForward;
416///
417/// let forward = HttpForward::new("backend.local", 8080);
418/// ```
419#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
420#[serde(rename_all = "camelCase")]
421pub struct HttpForward {
422    pub host: String,
423
424    #[serde(skip_serializing_if = "Option::is_none")]
425    pub port: Option<u16>,
426
427    #[serde(skip_serializing_if = "Option::is_none")]
428    pub scheme: Option<String>,
429}
430
431impl HttpForward {
432    /// Create a forward action to the given host and port.
433    pub fn new(host: impl Into<String>, port: u16) -> Self {
434        Self {
435            host: host.into(),
436            port: Some(port),
437            scheme: None,
438        }
439    }
440
441    /// Set the scheme (HTTP or HTTPS).
442    pub fn scheme(mut self, scheme: impl Into<String>) -> Self {
443        self.scheme = Some(scheme.into());
444        self
445    }
446}
447
448// ---------------------------------------------------------------------------
449// HttpClassCallback
450// ---------------------------------------------------------------------------
451
452/// Class callback action — delegates the response (or forward) to a server-side
453/// class that implements MockServer's callback interface.
454///
455/// This is a purely declarative (REST-only) callback: no WebSocket is involved.
456/// The named class must be on the MockServer server's classpath. Serialized as
457/// `httpResponseClassCallback` or `httpForwardClassCallback` in an expectation.
458///
459/// # Example
460/// ```
461/// use mockserver_client::HttpClassCallback;
462///
463/// let cb = HttpClassCallback::new("com.example.MyCallback").primary(true);
464/// ```
465#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
466#[serde(rename_all = "camelCase")]
467pub struct HttpClassCallback {
468    pub callback_class: String,
469
470    #[serde(skip_serializing_if = "Option::is_none")]
471    pub delay: Option<Delay>,
472
473    #[serde(skip_serializing_if = "Option::is_none")]
474    pub primary: Option<bool>,
475}
476
477impl HttpClassCallback {
478    /// Create a class callback referencing the fully-qualified class name of a
479    /// server-side callback implementation.
480    pub fn new(callback_class: impl Into<String>) -> Self {
481        Self {
482            callback_class: callback_class.into(),
483            delay: None,
484            primary: None,
485        }
486    }
487
488    /// Set a delay applied before the callback runs.
489    pub fn delay(mut self, delay: Delay) -> Self {
490        self.delay = Some(delay);
491        self
492    }
493
494    /// Mark this callback as primary (kept on the primary event-loop thread).
495    pub fn primary(mut self, primary: bool) -> Self {
496        self.primary = Some(primary);
497        self
498    }
499}
500
501// ---------------------------------------------------------------------------
502// HttpObjectCallback
503// ---------------------------------------------------------------------------
504
505/// Object (closure) callback action — delegates the response (or forward) to a
506/// client-side closure invoked over the callback WebSocket.
507///
508/// The `client_id` is the id assigned by MockServer when the client opens the
509/// callback WebSocket (`/_mockserver_callback_websocket`). When a request
510/// matches, the server pushes it over that socket and the client's registered
511/// closure produces the response. Serialized as `httpResponseObjectCallback` or
512/// `httpForwardObjectCallback` in an expectation.
513///
514/// Most users do not construct this directly — use
515/// [`MockServerClient::mock_with_callback`](crate::MockServerClient::mock_with_callback),
516/// which opens the shared WebSocket, registers the closure, and wires up the
517/// `client_id` automatically.
518#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
519#[serde(rename_all = "camelCase")]
520pub struct HttpObjectCallback {
521    pub client_id: String,
522
523    #[serde(skip_serializing_if = "Option::is_none")]
524    pub response_callback: Option<bool>,
525
526    #[serde(skip_serializing_if = "Option::is_none")]
527    pub delay: Option<Delay>,
528
529    #[serde(skip_serializing_if = "Option::is_none")]
530    pub primary: Option<bool>,
531}
532
533impl HttpObjectCallback {
534    /// Create an object callback bound to the given callback-WebSocket client id.
535    pub fn new(client_id: impl Into<String>) -> Self {
536        Self {
537            client_id: client_id.into(),
538            response_callback: None,
539            delay: None,
540            primary: None,
541        }
542    }
543
544    /// Set whether the callback also receives the response (forward + response form).
545    pub fn response_callback(mut self, response_callback: bool) -> Self {
546        self.response_callback = Some(response_callback);
547        self
548    }
549
550    /// Set a delay applied before the callback runs.
551    pub fn delay(mut self, delay: Delay) -> Self {
552        self.delay = Some(delay);
553        self
554    }
555
556    /// Mark this callback as primary (kept on the primary event-loop thread).
557    pub fn primary(mut self, primary: bool) -> Self {
558        self.primary = Some(primary);
559        self
560    }
561}
562
563// ---------------------------------------------------------------------------
564// HttpError
565// ---------------------------------------------------------------------------
566
567/// Error action — return a connection-level error to the caller.
568#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
569#[serde(rename_all = "camelCase")]
570pub struct HttpError {
571    #[serde(skip_serializing_if = "Option::is_none")]
572    pub drop_connection: Option<bool>,
573
574    #[serde(skip_serializing_if = "Option::is_none")]
575    pub response_bytes: Option<String>,
576}
577
578impl HttpError {
579    /// Create a new error action.
580    pub fn new() -> Self {
581        Self::default()
582    }
583
584    /// Drop the connection without a response.
585    pub fn drop_connection(mut self, drop: bool) -> Self {
586        self.drop_connection = Some(drop);
587        self
588    }
589
590    /// Send arbitrary bytes then close.
591    pub fn response_bytes(mut self, bytes: impl Into<String>) -> Self {
592        self.response_bytes = Some(bytes.into());
593        self
594    }
595}
596
597// ---------------------------------------------------------------------------
598// HttpSseResponse (Server-Sent Events)
599// ---------------------------------------------------------------------------
600
601/// A single Server-Sent Event in an [`HttpSseResponse`].
602///
603/// Maps to the `events[]` entries of the `httpSseResponse` wire shape.
604#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
605#[serde(rename_all = "camelCase")]
606pub struct SseEvent {
607    #[serde(skip_serializing_if = "Option::is_none")]
608    pub event: Option<String>,
609
610    #[serde(skip_serializing_if = "Option::is_none")]
611    pub data: Option<String>,
612
613    #[serde(skip_serializing_if = "Option::is_none")]
614    pub id: Option<String>,
615
616    #[serde(skip_serializing_if = "Option::is_none")]
617    pub retry: Option<u32>,
618
619    #[serde(skip_serializing_if = "Option::is_none")]
620    pub delay: Option<Delay>,
621}
622
623impl SseEvent {
624    /// Create a new empty SSE event.
625    pub fn new() -> Self {
626        Self::default()
627    }
628
629    /// Set the `event:` field (event type/name).
630    pub fn event(mut self, event: impl Into<String>) -> Self {
631        self.event = Some(event.into());
632        self
633    }
634
635    /// Set the `data:` payload.
636    pub fn data(mut self, data: impl Into<String>) -> Self {
637        self.data = Some(data.into());
638        self
639    }
640
641    /// Set the `id:` field.
642    pub fn id(mut self, id: impl Into<String>) -> Self {
643        self.id = Some(id.into());
644        self
645    }
646
647    /// Set the `retry:` reconnection time in milliseconds.
648    pub fn retry(mut self, retry: u32) -> Self {
649        self.retry = Some(retry);
650        self
651    }
652
653    /// Set a delay before this event is emitted.
654    pub fn delay(mut self, delay: Delay) -> Self {
655        self.delay = Some(delay);
656        self
657    }
658}
659
660/// Builder for a Server-Sent Events (SSE) streaming response action.
661///
662/// Serialized as the `httpSseResponse` action in an expectation.
663///
664/// # Example
665/// ```
666/// use mockserver_client::{HttpSseResponse, SseEvent};
667///
668/// let sse = HttpSseResponse::new()
669///     .status_code(200)
670///     .header("Content-Type", "text/event-stream")
671///     .event(SseEvent::new().event("message").data("hello").id("1"))
672///     .close_connection(true);
673/// ```
674#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
675#[serde(rename_all = "camelCase")]
676pub struct HttpSseResponse {
677    #[serde(skip_serializing_if = "Option::is_none")]
678    pub status_code: Option<u16>,
679
680    #[serde(skip_serializing_if = "Option::is_none")]
681    pub headers: Option<HashMap<String, Vec<String>>>,
682
683    #[serde(skip_serializing_if = "Option::is_none")]
684    pub events: Option<Vec<SseEvent>>,
685
686    #[serde(skip_serializing_if = "Option::is_none")]
687    pub close_connection: Option<bool>,
688
689    #[serde(skip_serializing_if = "Option::is_none")]
690    pub delay: Option<Delay>,
691}
692
693impl HttpSseResponse {
694    /// Create a new empty SSE response.
695    pub fn new() -> Self {
696        Self::default()
697    }
698
699    /// Set the HTTP status code.
700    pub fn status_code(mut self, code: u16) -> Self {
701        self.status_code = Some(code);
702        self
703    }
704
705    /// Add a response header.
706    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
707        let headers = self.headers.get_or_insert_with(HashMap::new);
708        headers.entry(key.into()).or_default().push(value.into());
709        self
710    }
711
712    /// Append an SSE event to the stream.
713    pub fn event(mut self, event: SseEvent) -> Self {
714        self.events.get_or_insert_with(Vec::new).push(event);
715        self
716    }
717
718    /// Replace all SSE events.
719    pub fn events(mut self, events: Vec<SseEvent>) -> Self {
720        self.events = Some(events);
721        self
722    }
723
724    /// Whether to close the connection after emitting all events.
725    pub fn close_connection(mut self, close: bool) -> Self {
726        self.close_connection = Some(close);
727        self
728    }
729
730    /// Set a delay before the response starts.
731    pub fn delay(mut self, delay: Delay) -> Self {
732        self.delay = Some(delay);
733        self
734    }
735}
736
737// ---------------------------------------------------------------------------
738// HttpWebSocketResponse
739// ---------------------------------------------------------------------------
740
741/// A single WebSocket message in an [`HttpWebSocketResponse`].
742///
743/// Either `text` or `binary` should be set. Binary data is base64-encoded
744/// on the wire (the schema declares `binary` as `format: byte`).
745#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
746#[serde(rename_all = "camelCase")]
747pub struct WebSocketMessage {
748    #[serde(skip_serializing_if = "Option::is_none")]
749    pub text: Option<String>,
750
751    #[serde(skip_serializing_if = "Option::is_none")]
752    pub binary: Option<String>,
753
754    #[serde(skip_serializing_if = "Option::is_none")]
755    pub delay: Option<Delay>,
756}
757
758impl WebSocketMessage {
759    /// Create a text WebSocket message.
760    pub fn text(text: impl Into<String>) -> Self {
761        Self {
762            text: Some(text.into()),
763            binary: None,
764            delay: None,
765        }
766    }
767
768    /// Create a binary WebSocket message from raw bytes (base64-encoded on the wire).
769    pub fn binary(data: impl AsRef<[u8]>) -> Self {
770        Self {
771            text: None,
772            binary: Some(BASE64.encode(data.as_ref())),
773            delay: None,
774        }
775    }
776
777    /// Create a binary WebSocket message from an already base64-encoded string.
778    pub fn binary_base64(base64: impl Into<String>) -> Self {
779        Self {
780            text: None,
781            binary: Some(base64.into()),
782            delay: None,
783        }
784    }
785
786    /// Set a delay before this message is sent.
787    pub fn delay(mut self, delay: Delay) -> Self {
788        self.delay = Some(delay);
789        self
790    }
791}
792
793/// Builder for a WebSocket streaming response action.
794///
795/// Serialized as the `httpWebSocketResponse` action in an expectation.
796///
797/// # Example
798/// ```
799/// use mockserver_client::{HttpWebSocketResponse, WebSocketMessage};
800///
801/// let ws = HttpWebSocketResponse::new()
802///     .subprotocol("chat")
803///     .message(WebSocketMessage::text("hello"))
804///     .message(WebSocketMessage::binary([0x01, 0x02]))
805///     .close_connection(true);
806/// ```
807#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
808#[serde(rename_all = "camelCase")]
809pub struct HttpWebSocketResponse {
810    #[serde(skip_serializing_if = "Option::is_none")]
811    pub subprotocol: Option<String>,
812
813    #[serde(skip_serializing_if = "Option::is_none")]
814    pub messages: Option<Vec<WebSocketMessage>>,
815
816    #[serde(skip_serializing_if = "Option::is_none")]
817    pub close_connection: Option<bool>,
818
819    #[serde(skip_serializing_if = "Option::is_none")]
820    pub delay: Option<Delay>,
821}
822
823impl HttpWebSocketResponse {
824    /// Create a new empty WebSocket response.
825    pub fn new() -> Self {
826        Self::default()
827    }
828
829    /// Set the negotiated subprotocol.
830    pub fn subprotocol(mut self, subprotocol: impl Into<String>) -> Self {
831        self.subprotocol = Some(subprotocol.into());
832        self
833    }
834
835    /// Append a WebSocket message to send.
836    pub fn message(mut self, message: WebSocketMessage) -> Self {
837        self.messages.get_or_insert_with(Vec::new).push(message);
838        self
839    }
840
841    /// Replace all WebSocket messages.
842    pub fn messages(mut self, messages: Vec<WebSocketMessage>) -> Self {
843        self.messages = Some(messages);
844        self
845    }
846
847    /// Whether to close the connection after emitting all messages.
848    pub fn close_connection(mut self, close: bool) -> Self {
849        self.close_connection = Some(close);
850        self
851    }
852
853    /// Set a delay before the response starts.
854    pub fn delay(mut self, delay: Delay) -> Self {
855        self.delay = Some(delay);
856        self
857    }
858}
859
860// ---------------------------------------------------------------------------
861// DnsResponse
862// ---------------------------------------------------------------------------
863
864/// A single DNS resource record in a [`DnsResponse`].
865#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
866#[serde(rename_all = "camelCase")]
867pub struct DnsRecord {
868    #[serde(skip_serializing_if = "Option::is_none")]
869    pub name: Option<String>,
870
871    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
872    pub record_type: Option<String>,
873
874    #[serde(skip_serializing_if = "Option::is_none")]
875    pub dns_class: Option<String>,
876
877    #[serde(skip_serializing_if = "Option::is_none")]
878    pub ttl: Option<u32>,
879
880    #[serde(skip_serializing_if = "Option::is_none")]
881    pub value: Option<String>,
882
883    #[serde(skip_serializing_if = "Option::is_none")]
884    pub priority: Option<u32>,
885
886    #[serde(skip_serializing_if = "Option::is_none")]
887    pub weight: Option<u32>,
888
889    #[serde(skip_serializing_if = "Option::is_none")]
890    pub port: Option<u16>,
891}
892
893impl DnsRecord {
894    /// Create a new empty DNS record.
895    pub fn new() -> Self {
896        Self::default()
897    }
898
899    /// Create an `A` (IPv4 address) record.
900    pub fn a(name: impl Into<String>, ip: impl Into<String>) -> Self {
901        Self::new().name(name).record_type("A").value(ip)
902    }
903
904    /// Create an `AAAA` (IPv6 address) record.
905    pub fn aaaa(name: impl Into<String>, ip: impl Into<String>) -> Self {
906        Self::new().name(name).record_type("AAAA").value(ip)
907    }
908
909    /// Create a `CNAME` record.
910    pub fn cname(name: impl Into<String>, target: impl Into<String>) -> Self {
911        Self::new().name(name).record_type("CNAME").value(target)
912    }
913
914    /// Create a `TXT` record.
915    pub fn txt(name: impl Into<String>, text: impl Into<String>) -> Self {
916        Self::new().name(name).record_type("TXT").value(text)
917    }
918
919    /// Set the record name.
920    pub fn name(mut self, name: impl Into<String>) -> Self {
921        self.name = Some(name.into());
922        self
923    }
924
925    /// Set the record type (e.g. "A", "AAAA", "CNAME", "MX", "SRV", "TXT", "PTR").
926    pub fn record_type(mut self, record_type: impl Into<String>) -> Self {
927        self.record_type = Some(record_type.into());
928        self
929    }
930
931    /// Set the DNS class (e.g. "IN", "CH", "HS", "ANY").
932    pub fn dns_class(mut self, dns_class: impl Into<String>) -> Self {
933        self.dns_class = Some(dns_class.into());
934        self
935    }
936
937    /// Set the time-to-live in seconds.
938    pub fn ttl(mut self, ttl: u32) -> Self {
939        self.ttl = Some(ttl);
940        self
941    }
942
943    /// Set the record value (address, target, text, etc.).
944    pub fn value(mut self, value: impl Into<String>) -> Self {
945        self.value = Some(value.into());
946        self
947    }
948
949    /// Set the priority (MX/SRV).
950    pub fn priority(mut self, priority: u32) -> Self {
951        self.priority = Some(priority);
952        self
953    }
954
955    /// Set the weight (SRV).
956    pub fn weight(mut self, weight: u32) -> Self {
957        self.weight = Some(weight);
958        self
959    }
960
961    /// Set the port (SRV).
962    pub fn port(mut self, port: u16) -> Self {
963        self.port = Some(port);
964        self
965    }
966}
967
968/// Builder for a DNS response action.
969///
970/// Serialized as the `dnsResponse` action in an expectation.
971///
972/// # Example
973/// ```
974/// use mockserver_client::{DnsResponse, DnsRecord};
975///
976/// let dns = DnsResponse::new()
977///     .response_code("NOERROR")
978///     .answer_record(DnsRecord::a("example.com", "1.2.3.4").ttl(300));
979/// ```
980#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
981#[serde(rename_all = "camelCase")]
982pub struct DnsResponse {
983    #[serde(skip_serializing_if = "Option::is_none")]
984    pub answer_records: Option<Vec<DnsRecord>>,
985
986    #[serde(skip_serializing_if = "Option::is_none")]
987    pub authority_records: Option<Vec<DnsRecord>>,
988
989    #[serde(skip_serializing_if = "Option::is_none")]
990    pub additional_records: Option<Vec<DnsRecord>>,
991
992    #[serde(skip_serializing_if = "Option::is_none")]
993    pub response_code: Option<String>,
994
995    #[serde(skip_serializing_if = "Option::is_none")]
996    pub delay: Option<Delay>,
997}
998
999impl DnsResponse {
1000    /// Create a new empty DNS response.
1001    pub fn new() -> Self {
1002        Self::default()
1003    }
1004
1005    /// Append an answer-section record.
1006    pub fn answer_record(mut self, record: DnsRecord) -> Self {
1007        self.answer_records
1008            .get_or_insert_with(Vec::new)
1009            .push(record);
1010        self
1011    }
1012
1013    /// Replace all answer-section records.
1014    pub fn answer_records(mut self, records: Vec<DnsRecord>) -> Self {
1015        self.answer_records = Some(records);
1016        self
1017    }
1018
1019    /// Append an authority-section record.
1020    pub fn authority_record(mut self, record: DnsRecord) -> Self {
1021        self.authority_records
1022            .get_or_insert_with(Vec::new)
1023            .push(record);
1024        self
1025    }
1026
1027    /// Append an additional-section record.
1028    pub fn additional_record(mut self, record: DnsRecord) -> Self {
1029        self.additional_records
1030            .get_or_insert_with(Vec::new)
1031            .push(record);
1032        self
1033    }
1034
1035    /// Set the DNS response code (e.g. "NOERROR", "NXDOMAIN", "SERVFAIL").
1036    pub fn response_code(mut self, code: impl Into<String>) -> Self {
1037        self.response_code = Some(code.into());
1038        self
1039    }
1040
1041    /// Set a delay before the response is returned.
1042    pub fn delay(mut self, delay: Delay) -> Self {
1043        self.delay = Some(delay);
1044        self
1045    }
1046}
1047
1048// ---------------------------------------------------------------------------
1049// BinaryResponse
1050// ---------------------------------------------------------------------------
1051
1052/// Builder for a raw binary response action.
1053///
1054/// Serialized as the `binaryResponse` action in an expectation. The binary
1055/// payload is base64-encoded on the wire (the schema declares `binaryData`
1056/// as a string).
1057///
1058/// # Example
1059/// ```
1060/// use mockserver_client::BinaryResponse;
1061///
1062/// let resp = BinaryResponse::from_bytes([0xDE, 0xAD, 0xBE, 0xEF]);
1063/// ```
1064#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1065#[serde(rename_all = "camelCase")]
1066pub struct BinaryResponse {
1067    #[serde(skip_serializing_if = "Option::is_none")]
1068    pub binary_data: Option<String>,
1069
1070    #[serde(skip_serializing_if = "Option::is_none")]
1071    pub delay: Option<Delay>,
1072}
1073
1074impl BinaryResponse {
1075    /// Create a new empty binary response.
1076    pub fn new() -> Self {
1077        Self::default()
1078    }
1079
1080    /// Create a binary response from raw bytes (base64-encoded on the wire).
1081    pub fn from_bytes(data: impl AsRef<[u8]>) -> Self {
1082        Self {
1083            binary_data: Some(BASE64.encode(data.as_ref())),
1084            delay: None,
1085        }
1086    }
1087
1088    /// Create a binary response from an already base64-encoded string.
1089    pub fn from_base64(base64: impl Into<String>) -> Self {
1090        Self {
1091            binary_data: Some(base64.into()),
1092            delay: None,
1093        }
1094    }
1095
1096    /// Set the binary payload from raw bytes (base64-encoded on the wire).
1097    pub fn binary_data(mut self, data: impl AsRef<[u8]>) -> Self {
1098        self.binary_data = Some(BASE64.encode(data.as_ref()));
1099        self
1100    }
1101
1102    /// Set a delay before the response is returned.
1103    pub fn delay(mut self, delay: Delay) -> Self {
1104        self.delay = Some(delay);
1105        self
1106    }
1107}
1108
1109// ---------------------------------------------------------------------------
1110// GrpcStreamResponse
1111// ---------------------------------------------------------------------------
1112
1113/// A single gRPC stream message in a [`GrpcStreamResponse`].
1114#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1115#[serde(rename_all = "camelCase")]
1116pub struct GrpcStreamMessage {
1117    #[serde(skip_serializing_if = "Option::is_none")]
1118    pub json: Option<String>,
1119
1120    #[serde(skip_serializing_if = "Option::is_none")]
1121    pub delay: Option<Delay>,
1122}
1123
1124impl GrpcStreamMessage {
1125    /// Create a gRPC stream message from a JSON-encoded protobuf message string.
1126    pub fn json(json: impl Into<String>) -> Self {
1127        Self {
1128            json: Some(json.into()),
1129            delay: None,
1130        }
1131    }
1132
1133    /// Set a delay before this message is sent.
1134    pub fn delay(mut self, delay: Delay) -> Self {
1135        self.delay = Some(delay);
1136        self
1137    }
1138}
1139
1140/// Builder for a gRPC streaming response action.
1141///
1142/// Serialized as the `grpcStreamResponse` action in an expectation.
1143///
1144/// # Example
1145/// ```
1146/// use mockserver_client::{GrpcStreamResponse, GrpcStreamMessage};
1147///
1148/// let grpc = GrpcStreamResponse::new()
1149///     .status_name("OK")
1150///     .message(GrpcStreamMessage::json("{\"id\":1}"))
1151///     .close_connection(true);
1152/// ```
1153#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1154#[serde(rename_all = "camelCase")]
1155pub struct GrpcStreamResponse {
1156    #[serde(skip_serializing_if = "Option::is_none")]
1157    pub status_name: Option<String>,
1158
1159    #[serde(skip_serializing_if = "Option::is_none")]
1160    pub status_message: Option<String>,
1161
1162    #[serde(skip_serializing_if = "Option::is_none")]
1163    pub headers: Option<HashMap<String, Vec<String>>>,
1164
1165    #[serde(skip_serializing_if = "Option::is_none")]
1166    pub messages: Option<Vec<GrpcStreamMessage>>,
1167
1168    #[serde(skip_serializing_if = "Option::is_none")]
1169    pub close_connection: Option<bool>,
1170
1171    #[serde(skip_serializing_if = "Option::is_none")]
1172    pub delay: Option<Delay>,
1173}
1174
1175impl GrpcStreamResponse {
1176    /// Create a new empty gRPC stream response.
1177    pub fn new() -> Self {
1178        Self::default()
1179    }
1180
1181    /// Set the gRPC status name (e.g. "OK", "NOT_FOUND").
1182    pub fn status_name(mut self, status_name: impl Into<String>) -> Self {
1183        self.status_name = Some(status_name.into());
1184        self
1185    }
1186
1187    /// Set the gRPC status message.
1188    pub fn status_message(mut self, status_message: impl Into<String>) -> Self {
1189        self.status_message = Some(status_message.into());
1190        self
1191    }
1192
1193    /// Add a response header (gRPC metadata).
1194    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
1195        let headers = self.headers.get_or_insert_with(HashMap::new);
1196        headers.entry(key.into()).or_default().push(value.into());
1197        self
1198    }
1199
1200    /// Append a gRPC stream message.
1201    pub fn message(mut self, message: GrpcStreamMessage) -> Self {
1202        self.messages.get_or_insert_with(Vec::new).push(message);
1203        self
1204    }
1205
1206    /// Replace all gRPC stream messages.
1207    pub fn messages(mut self, messages: Vec<GrpcStreamMessage>) -> Self {
1208        self.messages = Some(messages);
1209        self
1210    }
1211
1212    /// Whether to close the stream after emitting all messages.
1213    pub fn close_connection(mut self, close: bool) -> Self {
1214        self.close_connection = Some(close);
1215        self
1216    }
1217
1218    /// Set a delay before the response starts.
1219    pub fn delay(mut self, delay: Delay) -> Self {
1220        self.delay = Some(delay);
1221        self
1222    }
1223}
1224
1225// ---------------------------------------------------------------------------
1226// OpenApiExpectation
1227// ---------------------------------------------------------------------------
1228
1229/// An OpenAPI specification import — registers matchers and example responses
1230/// for the operations in an OpenAPI/Swagger spec.
1231///
1232/// Sent via `PUT /mockserver/openapi`. The spec may be a URL, a filesystem
1233/// path (`file://...`), a classpath resource, or an inline JSON/YAML payload.
1234///
1235/// # Example
1236/// ```
1237/// use mockserver_client::OpenApiExpectation;
1238///
1239/// let expectation = OpenApiExpectation::new(
1240///     "https://example.com/petstore.yaml",
1241/// )
1242/// .operation("listPets", "200")
1243/// .operation("showPetById", "200");
1244/// ```
1245#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1246#[serde(rename_all = "camelCase")]
1247pub struct OpenApiExpectation {
1248    pub spec_url_or_payload: String,
1249
1250    #[serde(skip_serializing_if = "Option::is_none")]
1251    pub operations_and_responses: Option<HashMap<String, String>>,
1252
1253    #[serde(skip_serializing_if = "Option::is_none")]
1254    pub context_path_prefix: Option<String>,
1255}
1256
1257impl OpenApiExpectation {
1258    /// Create an OpenAPI import from a spec URL, file path, classpath resource,
1259    /// or inline JSON/YAML payload.
1260    pub fn new(spec_url_or_payload: impl Into<String>) -> Self {
1261        Self {
1262            spec_url_or_payload: spec_url_or_payload.into(),
1263            operations_and_responses: None,
1264            context_path_prefix: None,
1265        }
1266    }
1267
1268    /// Map an `operationId` to the status code (or example name) to respond with.
1269    ///
1270    /// When no operations are specified, MockServer creates example responses
1271    /// for every operation in the spec.
1272    pub fn operation(
1273        mut self,
1274        operation_id: impl Into<String>,
1275        status_code: impl Into<String>,
1276    ) -> Self {
1277        self.operations_and_responses
1278            .get_or_insert_with(HashMap::new)
1279            .insert(operation_id.into(), status_code.into());
1280        self
1281    }
1282
1283    /// Replace the full operations-to-responses map.
1284    pub fn operations_and_responses(mut self, map: HashMap<String, String>) -> Self {
1285        self.operations_and_responses = Some(map);
1286        self
1287    }
1288
1289    /// Set a context-path prefix to prepend to every generated matcher path.
1290    pub fn context_path_prefix(mut self, prefix: impl Into<String>) -> Self {
1291        self.context_path_prefix = Some(prefix.into());
1292        self
1293    }
1294}
1295
1296// ---------------------------------------------------------------------------
1297// Delay
1298// ---------------------------------------------------------------------------
1299
1300/// A time delay (e.g., for response delays).
1301#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1302#[serde(rename_all = "camelCase")]
1303pub struct Delay {
1304    pub time_unit: String,
1305    pub value: u64,
1306}
1307
1308impl Delay {
1309    /// Create a delay in milliseconds.
1310    pub fn milliseconds(value: u64) -> Self {
1311        Self {
1312            time_unit: "MILLISECONDS".to_string(),
1313            value,
1314        }
1315    }
1316
1317    /// Create a delay in seconds.
1318    pub fn seconds(value: u64) -> Self {
1319        Self {
1320            time_unit: "SECONDS".to_string(),
1321            value,
1322        }
1323    }
1324}
1325
1326// ---------------------------------------------------------------------------
1327// Times
1328// ---------------------------------------------------------------------------
1329
1330/// How many times an expectation should be matched.
1331#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1332#[serde(rename_all = "camelCase")]
1333pub struct Times {
1334    #[serde(skip_serializing_if = "Option::is_none")]
1335    pub remaining_times: Option<u32>,
1336
1337    #[serde(default)]
1338    pub unlimited: bool,
1339}
1340
1341impl Times {
1342    /// Match unlimited times.
1343    pub fn unlimited() -> Self {
1344        Self {
1345            remaining_times: None,
1346            unlimited: true,
1347        }
1348    }
1349
1350    /// Match exactly `n` times.
1351    pub fn exactly(n: u32) -> Self {
1352        Self {
1353            remaining_times: Some(n),
1354            unlimited: false,
1355        }
1356    }
1357
1358    /// Match once.
1359    pub fn once() -> Self {
1360        Self::exactly(1)
1361    }
1362}
1363
1364// ---------------------------------------------------------------------------
1365// TimeToLive
1366// ---------------------------------------------------------------------------
1367
1368/// How long an expectation remains active.
1369#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1370#[serde(rename_all = "camelCase")]
1371pub struct TimeToLive {
1372    #[serde(skip_serializing_if = "Option::is_none")]
1373    pub time_unit: Option<String>,
1374
1375    #[serde(skip_serializing_if = "Option::is_none")]
1376    pub time_to_live: Option<u64>,
1377
1378    #[serde(default)]
1379    pub unlimited: bool,
1380}
1381
1382impl TimeToLive {
1383    /// Unlimited TTL (never expires).
1384    pub fn unlimited() -> Self {
1385        Self {
1386            time_unit: None,
1387            time_to_live: None,
1388            unlimited: true,
1389        }
1390    }
1391
1392    /// Expire after the given number of seconds.
1393    pub fn seconds(seconds: u64) -> Self {
1394        Self {
1395            time_unit: Some("SECONDS".to_string()),
1396            time_to_live: Some(seconds),
1397            unlimited: false,
1398        }
1399    }
1400
1401    /// Expire after the given number of milliseconds.
1402    pub fn milliseconds(millis: u64) -> Self {
1403        Self {
1404            time_unit: Some("MILLISECONDS".to_string()),
1405            time_to_live: Some(millis),
1406            unlimited: false,
1407        }
1408    }
1409}
1410
1411// ---------------------------------------------------------------------------
1412// VerificationTimes
1413// ---------------------------------------------------------------------------
1414
1415/// Verification constraints — how many times a request must have been received.
1416///
1417/// On the wire both `atLeast` and `atMost` are ALWAYS sent, using `-1` to mean
1418/// "unbounded". The MockServer server deserializes these into primitive `int`
1419/// fields, so an omitted bound defaults to `0` server-side — which would turn
1420/// `at_least(n)` into an impossible `between(n, 0)` constraint. Emitting the
1421/// explicit `-1` sentinel (matching the Java client) avoids that.
1422#[derive(Debug, Clone, Deserialize, PartialEq)]
1423#[serde(rename_all = "camelCase")]
1424pub struct VerificationTimes {
1425    pub at_least: Option<u32>,
1426    pub at_most: Option<u32>,
1427}
1428
1429impl Serialize for VerificationTimes {
1430    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1431    where
1432        S: serde::Serializer,
1433    {
1434        use serde::ser::SerializeStruct;
1435        let mut state = serializer.serialize_struct("VerificationTimes", 2)?;
1436        state.serialize_field("atLeast", &self.at_least.map_or(-1_i64, i64::from))?;
1437        state.serialize_field("atMost", &self.at_most.map_or(-1_i64, i64::from))?;
1438        state.end()
1439    }
1440}
1441
1442impl VerificationTimes {
1443    /// Require at least `n` matching requests.
1444    pub fn at_least(n: u32) -> Self {
1445        Self {
1446            at_least: Some(n),
1447            at_most: None,
1448        }
1449    }
1450
1451    /// Require at most `n` matching requests.
1452    pub fn at_most(n: u32) -> Self {
1453        Self {
1454            at_least: None,
1455            at_most: Some(n),
1456        }
1457    }
1458
1459    /// Require exactly `n` matching requests.
1460    pub fn exactly(n: u32) -> Self {
1461        Self {
1462            at_least: Some(n),
1463            at_most: Some(n),
1464        }
1465    }
1466
1467    /// Require between `min` and `max` matching requests (inclusive).
1468    pub fn between(min: u32, max: u32) -> Self {
1469        Self {
1470            at_least: Some(min),
1471            at_most: Some(max),
1472        }
1473    }
1474}
1475
1476// ---------------------------------------------------------------------------
1477// Stateful scenarios
1478// ---------------------------------------------------------------------------
1479
1480/// How MockServer selects which of an expectation's multiple `http_responses`
1481/// to return on each match. Maps to the `responseMode` field.
1482///
1483/// - `Sequential` (default) — cycle through the responses in order.
1484/// - `Random` — pick a response uniformly at random.
1485/// - `Weighted` — pick a response weighted by the index-aligned
1486///   [`Expectation::response_weights`].
1487/// - `Switch` — return the same response for [`Expectation::switch_after`]
1488///   matches before advancing to the next.
1489#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1490#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
1491pub enum ResponseMode {
1492    /// Cycle through the responses in order (default).
1493    Sequential,
1494    /// Pick a response uniformly at random.
1495    Random,
1496    /// Pick a response weighted by [`Expectation::response_weights`].
1497    Weighted,
1498    /// Return each response for [`Expectation::switch_after`] matches before advancing.
1499    Switch,
1500}
1501
1502/// The protocol event that triggers a [`CrossProtocolScenario`] state
1503/// transition. Maps to the `trigger` field.
1504#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1505#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
1506pub enum CrossProtocolTrigger {
1507    /// A DNS query is observed.
1508    DnsQuery,
1509    /// A WebSocket connection is established.
1510    WebsocketConnect,
1511    /// A gRPC request is observed.
1512    GrpcRequest,
1513    /// An HTTP request is observed.
1514    HttpRequest,
1515}
1516
1517/// A cross-protocol scenario correlation: when a protocol event matching
1518/// [`trigger`](Self::trigger) (and optionally [`match_pattern`](Self::match_pattern))
1519/// is observed, the named scenario is advanced to [`target_state`](Self::target_state).
1520///
1521/// Maps to entries of the `crossProtocolScenarios` array.
1522///
1523/// # Example
1524/// ```
1525/// use mockserver_client::{CrossProtocolScenario, CrossProtocolTrigger};
1526///
1527/// let scenario = CrossProtocolScenario::new(
1528///     CrossProtocolTrigger::DnsQuery,
1529///     "Deploy",
1530///     "DnsObserved",
1531/// )
1532/// .match_pattern("api.example.com");
1533/// ```
1534#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1535#[serde(rename_all = "camelCase")]
1536pub struct CrossProtocolScenario {
1537    pub trigger: CrossProtocolTrigger,
1538
1539    #[serde(skip_serializing_if = "Option::is_none")]
1540    pub match_pattern: Option<String>,
1541
1542    pub scenario_name: String,
1543
1544    pub target_state: String,
1545}
1546
1547impl CrossProtocolScenario {
1548    /// Create a cross-protocol scenario for the given trigger that advances
1549    /// `scenario_name` to `target_state` when an event fires.
1550    pub fn new(
1551        trigger: CrossProtocolTrigger,
1552        scenario_name: impl Into<String>,
1553        target_state: impl Into<String>,
1554    ) -> Self {
1555        Self {
1556            trigger,
1557            match_pattern: None,
1558            scenario_name: scenario_name.into(),
1559            target_state: target_state.into(),
1560        }
1561    }
1562
1563    /// Set the substring filter on the event identifier (omit to match all).
1564    pub fn match_pattern(mut self, pattern: impl Into<String>) -> Self {
1565        self.match_pattern = Some(pattern.into());
1566        self
1567    }
1568}
1569
1570// ---------------------------------------------------------------------------
1571// Expectation
1572// ---------------------------------------------------------------------------
1573
1574/// A full expectation combining a request matcher with an action.
1575#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1576#[serde(rename_all = "camelCase")]
1577pub struct Expectation {
1578    #[serde(skip_serializing_if = "Option::is_none")]
1579    pub id: Option<String>,
1580
1581    #[serde(skip_serializing_if = "Option::is_none")]
1582    pub priority: Option<i32>,
1583
1584    pub http_request: HttpRequest,
1585
1586    #[serde(skip_serializing_if = "Option::is_none")]
1587    pub http_response: Option<HttpResponse>,
1588
1589    #[serde(skip_serializing_if = "Option::is_none")]
1590    pub http_forward: Option<HttpForward>,
1591
1592    #[serde(skip_serializing_if = "Option::is_none")]
1593    pub http_response_template: Option<HttpTemplate>,
1594
1595    #[serde(skip_serializing_if = "Option::is_none")]
1596    pub http_forward_template: Option<HttpTemplate>,
1597
1598    #[serde(skip_serializing_if = "Option::is_none")]
1599    pub http_error: Option<HttpError>,
1600
1601    /// Class callback that produces the response (serialized as `httpResponseClassCallback`).
1602    #[serde(skip_serializing_if = "Option::is_none")]
1603    pub http_response_class_callback: Option<HttpClassCallback>,
1604
1605    /// Class callback that produces the request to forward (serialized as `httpForwardClassCallback`).
1606    #[serde(skip_serializing_if = "Option::is_none")]
1607    pub http_forward_class_callback: Option<HttpClassCallback>,
1608
1609    /// Object/closure callback that produces the response (serialized as `httpResponseObjectCallback`).
1610    #[serde(skip_serializing_if = "Option::is_none")]
1611    pub http_response_object_callback: Option<HttpObjectCallback>,
1612
1613    /// Object/closure callback that produces the request to forward (serialized as `httpForwardObjectCallback`).
1614    #[serde(skip_serializing_if = "Option::is_none")]
1615    pub http_forward_object_callback: Option<HttpObjectCallback>,
1616
1617    #[serde(skip_serializing_if = "Option::is_none")]
1618    pub http_sse_response: Option<HttpSseResponse>,
1619
1620    #[serde(skip_serializing_if = "Option::is_none")]
1621    pub http_web_socket_response: Option<HttpWebSocketResponse>,
1622
1623    #[serde(skip_serializing_if = "Option::is_none")]
1624    pub dns_response: Option<DnsResponse>,
1625
1626    #[serde(skip_serializing_if = "Option::is_none")]
1627    pub binary_response: Option<BinaryResponse>,
1628
1629    #[serde(skip_serializing_if = "Option::is_none")]
1630    pub grpc_stream_response: Option<GrpcStreamResponse>,
1631
1632    #[serde(skip_serializing_if = "Option::is_none")]
1633    pub times: Option<Times>,
1634
1635    #[serde(skip_serializing_if = "Option::is_none")]
1636    pub time_to_live: Option<TimeToLive>,
1637
1638    /// Name of the state-machine this expectation participates in.
1639    #[serde(skip_serializing_if = "Option::is_none")]
1640    pub scenario_name: Option<String>,
1641
1642    /// State the scenario must be in for this expectation to match.
1643    #[serde(skip_serializing_if = "Option::is_none")]
1644    pub scenario_state: Option<String>,
1645
1646    /// State the scenario transitions to after this expectation matches.
1647    #[serde(skip_serializing_if = "Option::is_none")]
1648    pub new_scenario_state: Option<String>,
1649
1650    /// Multiple responses; takes priority over the singular [`Expectation::http_response`].
1651    #[serde(skip_serializing_if = "Option::is_none")]
1652    pub http_responses: Option<Vec<HttpResponse>>,
1653
1654    /// How a response is selected from [`Expectation::http_responses`].
1655    #[serde(skip_serializing_if = "Option::is_none")]
1656    pub response_mode: Option<ResponseMode>,
1657
1658    /// Index-aligned relative weights for [`ResponseMode::Weighted`].
1659    #[serde(skip_serializing_if = "Option::is_none")]
1660    pub response_weights: Option<Vec<i32>>,
1661
1662    /// Requests per response block before advancing under [`ResponseMode::Switch`] (default 1).
1663    #[serde(skip_serializing_if = "Option::is_none")]
1664    pub switch_after: Option<i32>,
1665
1666    /// Cross-protocol scenario correlations that advance scenario state on protocol events.
1667    #[serde(skip_serializing_if = "Option::is_none")]
1668    pub cross_protocol_scenarios: Option<Vec<CrossProtocolScenario>>,
1669}
1670
1671impl Expectation {
1672    /// Create a new expectation with the given request matcher.
1673    pub fn new(request: HttpRequest) -> Self {
1674        Self {
1675            http_request: request,
1676            ..Default::default()
1677        }
1678    }
1679
1680    /// Set the expectation ID (for upsert semantics).
1681    pub fn id(mut self, id: impl Into<String>) -> Self {
1682        self.id = Some(id.into());
1683        self
1684    }
1685
1686    /// Set the priority (higher = matched first).
1687    pub fn priority(mut self, priority: i32) -> Self {
1688        self.priority = Some(priority);
1689        self
1690    }
1691
1692    /// Set a response action.
1693    pub fn respond(mut self, response: HttpResponse) -> Self {
1694        self.http_response = Some(response);
1695        self
1696    }
1697
1698    /// Set a forward action.
1699    pub fn forward(mut self, forward: HttpForward) -> Self {
1700        self.http_forward = Some(forward);
1701        self
1702    }
1703
1704    /// Set a response template action.
1705    pub fn respond_template(mut self, template: HttpTemplate) -> Self {
1706        self.http_response_template = Some(template);
1707        self
1708    }
1709
1710    /// Set a forward template action.
1711    pub fn forward_template(mut self, template: HttpTemplate) -> Self {
1712        self.http_forward_template = Some(template);
1713        self
1714    }
1715
1716    /// Set an error action.
1717    pub fn error(mut self, error: HttpError) -> Self {
1718        self.http_error = Some(error);
1719        self
1720    }
1721
1722    /// Respond via a server-side class callback (`httpResponseClassCallback`).
1723    ///
1724    /// The named class must implement MockServer's callback interface and be on
1725    /// the server's classpath. Convenience over building an [`HttpClassCallback`]
1726    /// directly; use [`respond_class_callback`](Self::respond_class_callback) for
1727    /// the full builder (delay, primary).
1728    pub fn respond_with_class_callback(mut self, callback_class: impl Into<String>) -> Self {
1729        self.http_response_class_callback = Some(HttpClassCallback::new(callback_class));
1730        self
1731    }
1732
1733    /// Respond via a pre-built [`HttpClassCallback`] (`httpResponseClassCallback`).
1734    pub fn respond_class_callback(mut self, callback: HttpClassCallback) -> Self {
1735        self.http_response_class_callback = Some(callback);
1736        self
1737    }
1738
1739    /// Forward via a server-side class callback (`httpForwardClassCallback`).
1740    pub fn forward_with_class_callback(mut self, callback_class: impl Into<String>) -> Self {
1741        self.http_forward_class_callback = Some(HttpClassCallback::new(callback_class));
1742        self
1743    }
1744
1745    /// Forward via a pre-built [`HttpClassCallback`] (`httpForwardClassCallback`).
1746    pub fn forward_class_callback(mut self, callback: HttpClassCallback) -> Self {
1747        self.http_forward_class_callback = Some(callback);
1748        self
1749    }
1750
1751    /// Respond via an object/closure callback (`httpResponseObjectCallback`).
1752    ///
1753    /// Most users should call
1754    /// [`MockServerClient::mock_with_callback`](crate::MockServerClient::mock_with_callback)
1755    /// instead, which opens the callback WebSocket, registers the closure, and
1756    /// fills in the `client_id` automatically.
1757    pub fn respond_object_callback(mut self, callback: HttpObjectCallback) -> Self {
1758        self.http_response_object_callback = Some(callback);
1759        self
1760    }
1761
1762    /// Forward via an object/closure callback (`httpForwardObjectCallback`).
1763    pub fn forward_object_callback(mut self, callback: HttpObjectCallback) -> Self {
1764        self.http_forward_object_callback = Some(callback);
1765        self
1766    }
1767
1768    /// Set a Server-Sent Events (SSE) response action.
1769    pub fn respond_sse(mut self, sse: HttpSseResponse) -> Self {
1770        self.http_sse_response = Some(sse);
1771        self
1772    }
1773
1774    /// Set a WebSocket response action.
1775    pub fn respond_web_socket(mut self, ws: HttpWebSocketResponse) -> Self {
1776        self.http_web_socket_response = Some(ws);
1777        self
1778    }
1779
1780    /// Set a DNS response action.
1781    pub fn respond_dns(mut self, dns: DnsResponse) -> Self {
1782        self.dns_response = Some(dns);
1783        self
1784    }
1785
1786    /// Set a raw binary response action.
1787    pub fn respond_binary(mut self, binary: BinaryResponse) -> Self {
1788        self.binary_response = Some(binary);
1789        self
1790    }
1791
1792    /// Set a gRPC streaming response action.
1793    pub fn respond_grpc_stream(mut self, grpc: GrpcStreamResponse) -> Self {
1794        self.grpc_stream_response = Some(grpc);
1795        self
1796    }
1797
1798    /// Set the number of times this expectation matches.
1799    pub fn times(mut self, times: Times) -> Self {
1800        self.times = Some(times);
1801        self
1802    }
1803
1804    /// Set the time-to-live.
1805    pub fn time_to_live(mut self, ttl: TimeToLive) -> Self {
1806        self.time_to_live = Some(ttl);
1807        self
1808    }
1809
1810    /// Set the scenario (state-machine) name this expectation participates in.
1811    pub fn scenario_name(mut self, name: impl Into<String>) -> Self {
1812        self.scenario_name = Some(name.into());
1813        self
1814    }
1815
1816    /// Set the state the scenario must be in for this expectation to match.
1817    pub fn scenario_state(mut self, state: impl Into<String>) -> Self {
1818        self.scenario_state = Some(state.into());
1819        self
1820    }
1821
1822    /// Set the state the scenario transitions to after this expectation matches.
1823    pub fn new_scenario_state(mut self, state: impl Into<String>) -> Self {
1824        self.new_scenario_state = Some(state.into());
1825        self
1826    }
1827
1828    /// Append a response to the multiple-responses list (`http_responses`).
1829    ///
1830    /// When set, `http_responses` takes priority over the singular
1831    /// [`respond`](Self::respond) action.
1832    pub fn respond_with(mut self, response: HttpResponse) -> Self {
1833        self.http_responses
1834            .get_or_insert_with(Vec::new)
1835            .push(response);
1836        self
1837    }
1838
1839    /// Replace all multiple responses (`http_responses`).
1840    pub fn http_responses(mut self, responses: Vec<HttpResponse>) -> Self {
1841        self.http_responses = Some(responses);
1842        self
1843    }
1844
1845    /// Set how a response is selected from `http_responses`.
1846    pub fn response_mode(mut self, mode: ResponseMode) -> Self {
1847        self.response_mode = Some(mode);
1848        self
1849    }
1850
1851    /// Set the index-aligned relative weights for [`ResponseMode::Weighted`].
1852    pub fn response_weights(mut self, weights: Vec<i32>) -> Self {
1853        self.response_weights = Some(weights);
1854        self
1855    }
1856
1857    /// Set the number of requests per response block before advancing under
1858    /// [`ResponseMode::Switch`].
1859    pub fn switch_after(mut self, switch_after: i32) -> Self {
1860        self.switch_after = Some(switch_after);
1861        self
1862    }
1863
1864    /// Append a [`CrossProtocolScenario`] correlation.
1865    pub fn cross_protocol_scenario(mut self, scenario: CrossProtocolScenario) -> Self {
1866        self.cross_protocol_scenarios
1867            .get_or_insert_with(Vec::new)
1868            .push(scenario);
1869        self
1870    }
1871
1872    /// Replace all cross-protocol scenario correlations.
1873    pub fn cross_protocol_scenarios(mut self, scenarios: Vec<CrossProtocolScenario>) -> Self {
1874        self.cross_protocol_scenarios = Some(scenarios);
1875        self
1876    }
1877}
1878
1879// ---------------------------------------------------------------------------
1880// Verification
1881// ---------------------------------------------------------------------------
1882
1883/// A verification request sent to MockServer.
1884///
1885/// At least one of `http_request` or `http_response` must be set.
1886/// `http_response` uses the same [`HttpResponse`] type as expectations —
1887/// the server matches against the recorded response's status code, headers,
1888/// and body.
1889#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1890#[serde(rename_all = "camelCase")]
1891pub struct Verification {
1892    #[serde(skip_serializing_if = "Option::is_none")]
1893    pub http_request: Option<HttpRequest>,
1894
1895    #[serde(skip_serializing_if = "Option::is_none")]
1896    pub http_response: Option<HttpResponse>,
1897
1898    #[serde(skip_serializing_if = "Option::is_none")]
1899    pub times: Option<VerificationTimes>,
1900
1901    #[serde(skip_serializing_if = "Option::is_none")]
1902    pub maximum_number_of_request_to_return_in_verification_failure: Option<u32>,
1903}
1904
1905/// A verification sequence request.
1906///
1907/// `http_responses` is index-aligned with `http_requests` — each entry
1908/// constrains the response that must have been returned for the
1909/// corresponding request.
1910#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1911#[serde(rename_all = "camelCase")]
1912pub struct VerificationSequence {
1913    #[serde(skip_serializing_if = "Option::is_none")]
1914    pub http_requests: Option<Vec<HttpRequest>>,
1915
1916    #[serde(skip_serializing_if = "Option::is_none")]
1917    pub http_responses: Option<Vec<HttpResponse>>,
1918}
1919
1920// ---------------------------------------------------------------------------
1921// Ports
1922// ---------------------------------------------------------------------------
1923
1924/// Port list (used by status and bind endpoints).
1925#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1926pub struct Ports {
1927    pub ports: Vec<u16>,
1928}
1929
1930// ---------------------------------------------------------------------------
1931// Scenario state
1932// ---------------------------------------------------------------------------
1933
1934/// A scenario and its current state, as returned by the scenario REST
1935/// endpoints (`GET /mockserver/scenario` and `GET /mockserver/scenario/{name}`).
1936#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1937#[serde(rename_all = "camelCase")]
1938pub struct ScenarioState {
1939    /// The scenario (state-machine) name.
1940    pub scenario_name: String,
1941    /// The scenario's current state.
1942    pub current_state: String,
1943}
1944
1945/// Wrapper for the `GET /mockserver/scenario` list response shape
1946/// (`{"scenarios":[{"scenarioName","currentState"}]}`).
1947#[derive(Debug, Clone, Deserialize)]
1948pub(crate) struct ScenarioList {
1949    #[serde(default)]
1950    pub scenarios: Vec<ScenarioState>,
1951}
1952
1953// ---------------------------------------------------------------------------
1954// Retrieve types
1955// ---------------------------------------------------------------------------
1956
1957/// The type of data to retrieve from MockServer.
1958#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1959pub enum RetrieveType {
1960    /// Recorded inbound requests.
1961    Requests,
1962    /// Active (live) expectations.
1963    ActiveExpectations,
1964    /// Recorded expectations (from proxy mode).
1965    RecordedExpectations,
1966    /// Log messages.
1967    Logs,
1968    /// Request/response pairs.
1969    RequestResponses,
1970}
1971
1972impl RetrieveType {
1973    /// The query parameter value for this type.
1974    pub fn as_str(&self) -> &'static str {
1975        match self {
1976            RetrieveType::Requests => "REQUESTS",
1977            RetrieveType::ActiveExpectations => "ACTIVE_EXPECTATIONS",
1978            RetrieveType::RecordedExpectations => "RECORDED_EXPECTATIONS",
1979            RetrieveType::Logs => "LOGS",
1980            RetrieveType::RequestResponses => "REQUEST_RESPONSES",
1981        }
1982    }
1983}
1984
1985/// The response format for retrieve calls.
1986///
1987/// In addition to JSON and log-entry formats, MockServer can return the
1988/// retrieved expectations as SDK setup code (the builder code that recreates
1989/// the expectations) in a range of languages.
1990#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1991pub enum RetrieveFormat {
1992    Json,
1993    LogEntries,
1994    Java,
1995    JavaScript,
1996    Python,
1997    Go,
1998    CSharp,
1999    Ruby,
2000    Rust,
2001    Php,
2002}
2003
2004impl RetrieveFormat {
2005    /// The query parameter value for this format.
2006    pub fn as_str(&self) -> &'static str {
2007        match self {
2008            RetrieveFormat::Json => "JSON",
2009            RetrieveFormat::LogEntries => "LOG_ENTRIES",
2010            RetrieveFormat::Java => "JAVA",
2011            RetrieveFormat::JavaScript => "JAVASCRIPT",
2012            RetrieveFormat::Python => "PYTHON",
2013            RetrieveFormat::Go => "GO",
2014            RetrieveFormat::CSharp => "CSHARP",
2015            RetrieveFormat::Ruby => "RUBY",
2016            RetrieveFormat::Rust => "RUST",
2017            RetrieveFormat::Php => "PHP",
2018        }
2019    }
2020}
2021
2022/// The type of data to clear from MockServer.
2023#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2024pub enum ClearType {
2025    All,
2026    Log,
2027    Expectations,
2028}
2029
2030impl ClearType {
2031    /// The query parameter value for this type.
2032    pub fn as_str(&self) -> &'static str {
2033        match self {
2034            ClearType::All => "ALL",
2035            ClearType::Log => "LOG",
2036            ClearType::Expectations => "EXPECTATIONS",
2037        }
2038    }
2039}
2040
2041// ---------------------------------------------------------------------------
2042// gRPC descriptor management
2043// ---------------------------------------------------------------------------
2044
2045/// A single gRPC method registered from an uploaded descriptor set.
2046///
2047/// Returned by [`MockServerClient::retrieve_grpc_services`] as part of a
2048/// [`GrpcService`]. Maps to the `methods[]` entries of the
2049/// `PUT /mockserver/grpc/services` wire shape.
2050///
2051/// [`MockServerClient::retrieve_grpc_services`]: crate::MockServerClient::retrieve_grpc_services
2052#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
2053#[serde(rename_all = "camelCase")]
2054pub struct GrpcMethod {
2055    /// The simple method name (e.g. `SayHello`).
2056    pub name: String,
2057
2058    /// Fully-qualified name of the request message type.
2059    pub input_type: String,
2060
2061    /// Fully-qualified name of the response message type.
2062    pub output_type: String,
2063
2064    /// Whether the method uses client-side streaming.
2065    pub client_streaming: bool,
2066
2067    /// Whether the method uses server-side streaming.
2068    pub server_streaming: bool,
2069}
2070
2071/// A gRPC service registered from an uploaded descriptor set.
2072///
2073/// Returned by [`MockServerClient::retrieve_grpc_services`]. Maps to the
2074/// top-level entries of the `PUT /mockserver/grpc/services` wire shape.
2075///
2076/// [`MockServerClient::retrieve_grpc_services`]: crate::MockServerClient::retrieve_grpc_services
2077#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
2078#[serde(rename_all = "camelCase")]
2079pub struct GrpcService {
2080    /// Fully-qualified service name (e.g. `helloworld.Greeter`).
2081    pub name: String,
2082
2083    /// The methods declared by this service.
2084    pub methods: Vec<GrpcMethod>,
2085}
2086
2087// ---------------------------------------------------------------------------
2088// SocketAddress
2089// ---------------------------------------------------------------------------
2090
2091/// A downstream socket address (host / port / scheme) to direct a request at.
2092///
2093/// Maps to MockServer's `SocketAddress` model. Used by load-scenario steps to
2094/// target a specific upstream rather than relying on the `Host` header.
2095#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
2096#[serde(rename_all = "camelCase")]
2097pub struct SocketAddress {
2098    /// The downstream host name or IP.
2099    pub host: String,
2100
2101    /// The downstream port.
2102    pub port: u16,
2103
2104    /// The scheme to connect with — `"HTTP"` or `"HTTPS"`. Defaults to `"HTTP"`
2105    /// on the server when omitted.
2106    #[serde(skip_serializing_if = "Option::is_none")]
2107    pub scheme: Option<String>,
2108}
2109
2110impl SocketAddress {
2111    /// Create a plain HTTP socket address.
2112    pub fn new(host: impl Into<String>, port: u16) -> Self {
2113        Self {
2114            host: host.into(),
2115            port,
2116            scheme: None,
2117        }
2118    }
2119
2120    /// Set the scheme (`"HTTP"` or `"HTTPS"`).
2121    pub fn scheme(mut self, scheme: impl Into<String>) -> Self {
2122        self.scheme = Some(scheme.into());
2123        self
2124    }
2125
2126    /// Convenience: an HTTPS socket address.
2127    pub fn https(host: impl Into<String>, port: u16) -> Self {
2128        Self::new(host, port).scheme("HTTPS")
2129    }
2130}
2131
2132// ---------------------------------------------------------------------------
2133// Load scenario registry (PUT/GET/DELETE /mockserver/loadScenario[/...])
2134// ---------------------------------------------------------------------------
2135
2136/// The interpolation curve used to ramp a value (virtual users or arrival
2137/// rate) from a start setpoint to an end setpoint across a ramp [`LoadStage`].
2138/// Maps to the `RampCurve` schema. Only meaningful for ramp stages; ignored for
2139/// holds and pauses.
2140#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2141#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
2142pub enum RampCurve {
2143    /// Constant slope.
2144    Linear,
2145    /// Ease-in: slow then fast.
2146    Quadratic,
2147    /// A steeper ease-in.
2148    Exponential,
2149}
2150
2151/// The kind of a [`LoadStage`].
2152///
2153/// - `Vu` — closed model: hold or ramp the number of concurrent virtual users.
2154/// - `Rate` — open model: hold or ramp an arrival rate in iterations/second.
2155/// - `Pause` — drive no load for the duration.
2156#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2157#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
2158pub enum LoadStageType {
2159    /// Closed model — hold or ramp concurrent virtual users.
2160    Vu,
2161    /// Open model — hold or ramp an arrival rate in iterations/second.
2162    Rate,
2163    /// Drive no load for the duration.
2164    Pause,
2165}
2166
2167/// One stage of a [`LoadProfile`]: a contiguous slice of the run holding or
2168/// ramping a setpoint for `duration_millis`. Stages run in sequence. Maps to the
2169/// `LoadStage` schema.
2170///
2171/// Use the constructors [`LoadStage::vu_hold`], [`LoadStage::vu_ramp`],
2172/// [`LoadStage::rate_hold`], [`LoadStage::rate_ramp`] and [`LoadStage::pause`]
2173/// rather than building the struct directly so only the relevant fields are set
2174/// (and therefore serialized).
2175#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2176#[serde(rename_all = "camelCase")]
2177pub struct LoadStage {
2178    /// The kind of stage — `VU`, `RATE` or `PAUSE`.
2179    #[serde(rename = "type")]
2180    pub stage_type: LoadStageType,
2181
2182    /// How long this stage runs in milliseconds (> 0).
2183    pub duration_millis: u64,
2184
2185    /// Ramp shape (ramp stages only); omitted for holds and pauses.
2186    #[serde(skip_serializing_if = "Option::is_none")]
2187    pub curve: Option<RampCurve>,
2188
2189    /// VU hold: the number of virtual users to hold for the stage.
2190    #[serde(skip_serializing_if = "Option::is_none")]
2191    pub vus: Option<u32>,
2192
2193    /// VU ramp: virtual users at the start of the ramp.
2194    #[serde(skip_serializing_if = "Option::is_none")]
2195    pub start_vus: Option<u32>,
2196
2197    /// VU ramp: virtual users at the end of the ramp.
2198    #[serde(skip_serializing_if = "Option::is_none")]
2199    pub end_vus: Option<u32>,
2200
2201    /// RATE hold: arrival rate to hold, in iterations per second.
2202    #[serde(skip_serializing_if = "Option::is_none")]
2203    pub rate: Option<f64>,
2204
2205    /// RATE ramp: arrival rate at the start of the ramp, in iterations/second.
2206    #[serde(skip_serializing_if = "Option::is_none")]
2207    pub start_rate: Option<f64>,
2208
2209    /// RATE ramp: arrival rate at the end of the ramp, in iterations/second.
2210    #[serde(skip_serializing_if = "Option::is_none")]
2211    pub end_rate: Option<f64>,
2212
2213    /// RATE stage only: optional cap on the auto-scaling virtual-user pool.
2214    #[serde(skip_serializing_if = "Option::is_none")]
2215    pub max_vus: Option<u32>,
2216}
2217
2218impl LoadStage {
2219    fn base(stage_type: LoadStageType, duration_millis: u64) -> Self {
2220        Self {
2221            stage_type,
2222            duration_millis,
2223            curve: None,
2224            vus: None,
2225            start_vus: None,
2226            end_vus: None,
2227            rate: None,
2228            start_rate: None,
2229            end_rate: None,
2230            max_vus: None,
2231        }
2232    }
2233
2234    /// A VU stage holding `vus` virtual users for `duration_millis`.
2235    pub fn vu_hold(vus: u32, duration_millis: u64) -> Self {
2236        let mut stage = Self::base(LoadStageType::Vu, duration_millis);
2237        stage.vus = Some(vus);
2238        stage
2239    }
2240
2241    /// A VU stage ramping from `start_vus` to `end_vus` over `duration_millis`
2242    /// along `curve`.
2243    pub fn vu_ramp(start_vus: u32, end_vus: u32, duration_millis: u64, curve: RampCurve) -> Self {
2244        let mut stage = Self::base(LoadStageType::Vu, duration_millis);
2245        stage.start_vus = Some(start_vus);
2246        stage.end_vus = Some(end_vus);
2247        stage.curve = Some(curve);
2248        stage
2249    }
2250
2251    /// A RATE stage holding `rate` iterations/second for `duration_millis`.
2252    pub fn rate_hold(rate: f64, duration_millis: u64) -> Self {
2253        let mut stage = Self::base(LoadStageType::Rate, duration_millis);
2254        stage.rate = Some(rate);
2255        stage
2256    }
2257
2258    /// A RATE stage ramping from `start_rate` to `end_rate` iterations/second
2259    /// over `duration_millis` along `curve`.
2260    pub fn rate_ramp(
2261        start_rate: f64,
2262        end_rate: f64,
2263        duration_millis: u64,
2264        curve: RampCurve,
2265    ) -> Self {
2266        let mut stage = Self::base(LoadStageType::Rate, duration_millis);
2267        stage.start_rate = Some(start_rate);
2268        stage.end_rate = Some(end_rate);
2269        stage.curve = Some(curve);
2270        stage
2271    }
2272
2273    /// A PAUSE stage that drives no load for `duration_millis`.
2274    pub fn pause(duration_millis: u64) -> Self {
2275        Self::base(LoadStageType::Pause, duration_millis)
2276    }
2277
2278    /// Cap the auto-scaling virtual-user pool for this RATE stage.
2279    pub fn max_vus(mut self, max_vus: u32) -> Self {
2280        self.max_vus = Some(max_vus);
2281        self
2282    }
2283}
2284
2285/// The load profile of a load scenario: an ordered list of [`LoadStage`]s run
2286/// in sequence. Maps to the `LoadProfile` schema.
2287///
2288/// Use [`LoadProfile::of`] to build from a list of stages, or the convenience
2289/// constructors [`LoadProfile::constant`] / [`LoadProfile::linear`] for a single
2290/// VU stage.
2291#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2292#[serde(rename_all = "camelCase")]
2293pub struct LoadProfile {
2294    /// Ordered stages run one after another.
2295    pub stages: Vec<LoadStage>,
2296}
2297
2298impl LoadProfile {
2299    /// A profile from an explicit list of stages.
2300    pub fn of(stages: Vec<LoadStage>) -> Self {
2301        Self { stages }
2302    }
2303
2304    /// A single VU stage holding `vus` virtual users for `duration_millis`.
2305    pub fn constant(vus: u32, duration_millis: u64) -> Self {
2306        Self::of(vec![LoadStage::vu_hold(vus, duration_millis)])
2307    }
2308
2309    /// A single linear VU ramp from `start_vus` to `end_vus` over
2310    /// `duration_millis`.
2311    pub fn linear(start_vus: u32, end_vus: u32, duration_millis: u64) -> Self {
2312        Self::of(vec![LoadStage::vu_ramp(
2313            start_vus,
2314            end_vus,
2315            duration_millis,
2316            RampCurve::Linear,
2317        )])
2318    }
2319
2320    /// Append a stage and return the profile.
2321    pub fn add_stage(mut self, stage: LoadStage) -> Self {
2322        self.stages.push(stage);
2323        self
2324    }
2325}
2326
2327/// A single templated request step in a load scenario. Maps to the `LoadStep`
2328/// schema.
2329#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2330#[serde(rename_all = "camelCase")]
2331pub struct LoadStep {
2332    /// The templated request to fire each iteration.
2333    pub request: HttpRequest,
2334
2335    /// Optional inter-step pause (a [`Delay`]).
2336    #[serde(skip_serializing_if = "Option::is_none")]
2337    pub think_time: Option<Delay>,
2338}
2339
2340impl LoadStep {
2341    /// Create a step from a request matcher/template.
2342    pub fn new(request: HttpRequest) -> Self {
2343        Self {
2344            request,
2345            think_time: None,
2346        }
2347    }
2348
2349    /// Set the inter-step pause.
2350    pub fn think_time(mut self, delay: Delay) -> Self {
2351        self.think_time = Some(delay);
2352        self
2353    }
2354}
2355
2356/// An API-driven load scenario: ordered templated steps driven at a target
2357/// concurrency. Maps to the `LoadScenario` schema (the body of
2358/// `PUT /mockserver/loadScenario`, which registers the scenario in the
2359/// registry without running it). The unique [`name`](LoadScenario::name) is the
2360/// registry key used by `start`/`stop` and the per-scenario `GET`/`DELETE`
2361/// endpoints.
2362#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2363#[serde(rename_all = "camelCase")]
2364pub struct LoadScenario {
2365    /// Human-readable scenario name.
2366    pub name: String,
2367
2368    /// Template engine for per-iteration rendering — `"VELOCITY"` (default) or
2369    /// `"MUSTACHE"`. (JavaScript is rejected for load steps.)
2370    #[serde(skip_serializing_if = "Option::is_none")]
2371    pub template_type: Option<String>,
2372
2373    /// Optional hard cap on the total number of requests dispatched.
2374    #[serde(skip_serializing_if = "Option::is_none")]
2375    pub max_requests: Option<u64>,
2376
2377    /// Optional delay (milliseconds) applied between a `start` request being
2378    /// accepted and the scenario actually beginning to drive load. Honoured by
2379    /// `PUT /mockserver/loadScenario/start`.
2380    #[serde(skip_serializing_if = "Option::is_none")]
2381    pub start_delay_millis: Option<u64>,
2382
2383    /// The ramp profile.
2384    pub profile: LoadProfile,
2385
2386    /// Ordered list of request steps fired in sequence each iteration (max 50).
2387    pub steps: Vec<LoadStep>,
2388}
2389
2390impl LoadScenario {
2391    /// Create a scenario with the given name, profile and steps.
2392    pub fn new(name: impl Into<String>, profile: LoadProfile, steps: Vec<LoadStep>) -> Self {
2393        Self {
2394            name: name.into(),
2395            template_type: None,
2396            max_requests: None,
2397            start_delay_millis: None,
2398            profile,
2399            steps,
2400        }
2401    }
2402
2403    /// Set the template engine (`"VELOCITY"` or `"MUSTACHE"`).
2404    pub fn template_type(mut self, template_type: impl Into<String>) -> Self {
2405        self.template_type = Some(template_type.into());
2406        self
2407    }
2408
2409    /// Set the hard cap on total requests dispatched.
2410    pub fn max_requests(mut self, max_requests: u64) -> Self {
2411        self.max_requests = Some(max_requests);
2412        self
2413    }
2414
2415    /// Set the delay (milliseconds) before the scenario begins driving load
2416    /// once started.
2417    pub fn start_delay_millis(mut self, start_delay_millis: u64) -> Self {
2418        self.start_delay_millis = Some(start_delay_millis);
2419        self
2420    }
2421}
2422
2423// ---------------------------------------------------------------------------
2424// SLO verdicts (PUT /mockserver/verifySLO)
2425// ---------------------------------------------------------------------------
2426
2427/// A single service-level objective over the recorded SLI samples. Maps to the
2428/// `SloObjective` schema.
2429#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2430#[serde(rename_all = "camelCase")]
2431pub struct SloObjective {
2432    /// The indicator to evaluate — one of `LATENCY_P50`, `LATENCY_P95`,
2433    /// `LATENCY_P99`, `ERROR_RATE`.
2434    pub sli: String,
2435
2436    /// How the observed value is compared to the threshold — one of
2437    /// `LESS_THAN`, `LESS_THAN_OR_EQUAL`, `GREATER_THAN`,
2438    /// `GREATER_THAN_OR_EQUAL`.
2439    pub comparator: String,
2440
2441    /// The objective threshold (milliseconds for latency SLIs, a 0.0–1.0
2442    /// fraction for `ERROR_RATE`).
2443    pub threshold: f64,
2444
2445    /// Which recorded traffic to evaluate — `"FORWARD"` (default) or
2446    /// `"INBOUND"`.
2447    #[serde(skip_serializing_if = "Option::is_none")]
2448    pub scope: Option<String>,
2449}
2450
2451impl SloObjective {
2452    /// Create an objective.
2453    pub fn new(sli: impl Into<String>, comparator: impl Into<String>, threshold: f64) -> Self {
2454        Self {
2455            sli: sli.into(),
2456            comparator: comparator.into(),
2457            threshold,
2458            scope: None,
2459        }
2460    }
2461
2462    /// Set the evaluation scope (`"FORWARD"` or `"INBOUND"`).
2463    pub fn scope(mut self, scope: impl Into<String>) -> Self {
2464        self.scope = Some(scope.into());
2465        self
2466    }
2467}
2468
2469/// The time window of an SLO evaluation. Maps to the `SloCriteria.window`
2470/// object.
2471#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
2472#[serde(rename_all = "camelCase")]
2473pub struct SloWindow {
2474    /// `"LOOKBACK"` (default) or `"EXPLICIT"`.
2475    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
2476    pub window_type: Option<String>,
2477
2478    /// LOOKBACK: window length ending now.
2479    #[serde(skip_serializing_if = "Option::is_none")]
2480    pub lookback_millis: Option<u64>,
2481
2482    /// EXPLICIT: window start in epoch milliseconds.
2483    #[serde(skip_serializing_if = "Option::is_none")]
2484    pub from_epoch_millis: Option<u64>,
2485
2486    /// EXPLICIT: window end in epoch milliseconds.
2487    #[serde(skip_serializing_if = "Option::is_none")]
2488    pub to_epoch_millis: Option<u64>,
2489}
2490
2491impl SloWindow {
2492    /// A LOOKBACK window of `millis` ending now.
2493    pub fn lookback(millis: u64) -> Self {
2494        Self {
2495            window_type: Some("LOOKBACK".to_string()),
2496            lookback_millis: Some(millis),
2497            ..Default::default()
2498        }
2499    }
2500
2501    /// An EXPLICIT window between two epoch-millisecond bounds.
2502    pub fn explicit(from_epoch_millis: u64, to_epoch_millis: u64) -> Self {
2503        Self {
2504            window_type: Some("EXPLICIT".to_string()),
2505            from_epoch_millis: Some(from_epoch_millis),
2506            to_epoch_millis: Some(to_epoch_millis),
2507            ..Default::default()
2508        }
2509    }
2510}
2511
2512/// A named set of service-level objectives over a time window. Maps to the
2513/// `SloCriteria` schema (the body of `PUT /mockserver/verifySLO`).
2514#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2515#[serde(rename_all = "camelCase")]
2516pub struct SloCriteria {
2517    /// Human-readable criteria name, echoed back in the verdict.
2518    #[serde(skip_serializing_if = "Option::is_none")]
2519    pub name: Option<String>,
2520
2521    /// The time window to evaluate over.
2522    #[serde(skip_serializing_if = "Option::is_none")]
2523    pub window: Option<SloWindow>,
2524
2525    /// Minimum samples required in the window; below this the verdict is
2526    /// INCONCLUSIVE.
2527    #[serde(skip_serializing_if = "Option::is_none")]
2528    pub minimum_sample_count: Option<u64>,
2529
2530    /// Optional list of upstream hosts to restrict the evaluation to.
2531    #[serde(skip_serializing_if = "Option::is_none")]
2532    pub upstream_hosts: Option<Vec<String>>,
2533
2534    /// The objectives (the verdict is the logical AND of all of them).
2535    pub objectives: Vec<SloObjective>,
2536}
2537
2538impl SloCriteria {
2539    /// Create criteria from a set of objectives.
2540    pub fn new(objectives: Vec<SloObjective>) -> Self {
2541        Self {
2542            name: None,
2543            window: None,
2544            minimum_sample_count: None,
2545            upstream_hosts: None,
2546            objectives,
2547        }
2548    }
2549
2550    /// Set the criteria name.
2551    pub fn name(mut self, name: impl Into<String>) -> Self {
2552        self.name = Some(name.into());
2553        self
2554    }
2555
2556    /// Set the evaluation window.
2557    pub fn window(mut self, window: SloWindow) -> Self {
2558        self.window = Some(window);
2559        self
2560    }
2561
2562    /// Set the minimum sample count.
2563    pub fn minimum_sample_count(mut self, count: u64) -> Self {
2564        self.minimum_sample_count = Some(count);
2565        self
2566    }
2567
2568    /// Restrict the evaluation to the given upstream hosts.
2569    pub fn upstream_hosts(mut self, hosts: Vec<String>) -> Self {
2570        self.upstream_hosts = Some(hosts);
2571        self
2572    }
2573}
2574
2575/// The evaluated result of a single objective. Maps to the `SloObjectiveResult`
2576/// schema.
2577#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2578#[serde(rename_all = "camelCase")]
2579pub struct SloObjectiveResult {
2580    #[serde(skip_serializing_if = "Option::is_none")]
2581    pub sli: Option<String>,
2582    #[serde(skip_serializing_if = "Option::is_none")]
2583    pub comparator: Option<String>,
2584    #[serde(skip_serializing_if = "Option::is_none")]
2585    pub threshold: Option<f64>,
2586    #[serde(skip_serializing_if = "Option::is_none")]
2587    pub observed_value: Option<f64>,
2588    /// `PASS`, `FAIL` or `INCONCLUSIVE`.
2589    #[serde(skip_serializing_if = "Option::is_none")]
2590    pub result: Option<String>,
2591    #[serde(skip_serializing_if = "Option::is_none")]
2592    pub detail: Option<String>,
2593}
2594
2595/// The overall verdict of an SLO evaluation. Maps to the `SloVerdict` schema —
2596/// the response of `PUT /mockserver/verifySLO`.
2597#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2598#[serde(rename_all = "camelCase")]
2599pub struct SloVerdict {
2600    #[serde(skip_serializing_if = "Option::is_none")]
2601    pub name: Option<String>,
2602    /// `PASS`, `FAIL` or `INCONCLUSIVE`.
2603    #[serde(skip_serializing_if = "Option::is_none")]
2604    pub result: Option<String>,
2605    #[serde(skip_serializing_if = "Option::is_none")]
2606    pub window_from_epoch_millis: Option<u64>,
2607    #[serde(skip_serializing_if = "Option::is_none")]
2608    pub window_to_epoch_millis: Option<u64>,
2609    #[serde(skip_serializing_if = "Option::is_none")]
2610    pub sample_count: Option<u64>,
2611    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2612    pub objective_results: Vec<SloObjectiveResult>,
2613}
2614
2615impl SloVerdict {
2616    /// Whether the overall verdict is `PASS`.
2617    pub fn is_pass(&self) -> bool {
2618        self.result.as_deref() == Some("PASS")
2619    }
2620
2621    /// Whether the overall verdict is `FAIL`.
2622    pub fn is_fail(&self) -> bool {
2623        self.result.as_deref() == Some("FAIL")
2624    }
2625
2626    /// Whether the overall verdict is `INCONCLUSIVE`.
2627    pub fn is_inconclusive(&self) -> bool {
2628        self.result.as_deref() == Some("INCONCLUSIVE")
2629    }
2630}
2631
2632// ---------------------------------------------------------------------------
2633// Preemption (PUT/GET/DELETE /mockserver/preemption)
2634// ---------------------------------------------------------------------------
2635
2636/// Preemption simulation parameters (all fields optional). Maps to the
2637/// `PreemptionRequest` schema (the body of `PUT /mockserver/preemption`).
2638#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
2639#[serde(rename_all = "camelCase")]
2640pub struct PreemptionRequest {
2641    /// How draining is signalled — `"reject503"`, `"goaway"` or `"both"`
2642    /// (default).
2643    #[serde(skip_serializing_if = "Option::is_none")]
2644    pub mode: Option<String>,
2645
2646    /// How long in-flight requests are allowed to drain (clamped server-side).
2647    #[serde(skip_serializing_if = "Option::is_none")]
2648    pub drain_millis: Option<u64>,
2649
2650    /// Auto-uncordon after this many milliseconds (dead-man's switch); `0`
2651    /// (default) means no auto-uncordon.
2652    #[serde(skip_serializing_if = "Option::is_none")]
2653    pub ttl_millis: Option<u64>,
2654
2655    /// HTTP/2 GOAWAY `last_stream_id` to advertise; `-1` (default) lets the
2656    /// server choose.
2657    #[serde(skip_serializing_if = "Option::is_none")]
2658    pub last_stream_id: Option<i64>,
2659}
2660
2661impl PreemptionRequest {
2662    /// An empty request (server defaults: mode "both", default drain, no TTL).
2663    pub fn new() -> Self {
2664        Self::default()
2665    }
2666
2667    /// Set the signalling mode (`"reject503"`, `"goaway"` or `"both"`).
2668    pub fn mode(mut self, mode: impl Into<String>) -> Self {
2669        self.mode = Some(mode.into());
2670        self
2671    }
2672
2673    /// Set the drain window in milliseconds.
2674    pub fn drain_millis(mut self, millis: u64) -> Self {
2675        self.drain_millis = Some(millis);
2676        self
2677    }
2678
2679    /// Set the auto-uncordon TTL in milliseconds.
2680    pub fn ttl_millis(mut self, millis: u64) -> Self {
2681        self.ttl_millis = Some(millis);
2682        self
2683    }
2684
2685    /// Set the HTTP/2 GOAWAY `last_stream_id` to advertise.
2686    pub fn last_stream_id(mut self, id: i64) -> Self {
2687        self.last_stream_id = Some(id);
2688        self
2689    }
2690}
2691
2692/// The current cordon/drain status of the server. Maps to the
2693/// `PreemptionStatus` schema — the response of `PUT`/`GET /mockserver/preemption`.
2694#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
2695#[serde(rename_all = "camelCase")]
2696pub struct PreemptionStatus {
2697    /// `"inactive"`, `"draining"` or `"drained"`.
2698    #[serde(skip_serializing_if = "Option::is_none")]
2699    pub state: Option<String>,
2700
2701    /// Number of requests currently in flight.
2702    #[serde(skip_serializing_if = "Option::is_none")]
2703    pub in_flight: Option<u64>,
2704
2705    /// Milliseconds left in the drain window.
2706    #[serde(skip_serializing_if = "Option::is_none")]
2707    pub drain_remaining_millis: Option<u64>,
2708
2709    /// Active signalling mode (omitted when inactive).
2710    #[serde(skip_serializing_if = "Option::is_none")]
2711    pub mode: Option<String>,
2712}
2713
2714// ---------------------------------------------------------------------------
2715// Service chaos (PUT /mockserver/serviceChaos)
2716// ---------------------------------------------------------------------------
2717
2718/// An HTTP chaos / fault-injection profile for a host or expectation. Maps to
2719/// the `HttpChaosProfile` schema. Captures the commonly-used fields; the model
2720/// carries an `extra` map for any additional server-supported keys.
2721#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2722#[serde(rename_all = "camelCase")]
2723pub struct HttpChaosProfile {
2724    /// HTTP error status code to return instead of the real response.
2725    #[serde(skip_serializing_if = "Option::is_none")]
2726    pub error_status: Option<u16>,
2727
2728    /// Probability (0.0–1.0) that a request triggers the error.
2729    #[serde(skip_serializing_if = "Option::is_none")]
2730    pub error_probability: Option<f64>,
2731
2732    /// Injected latency (a [`Delay`]).
2733    #[serde(skip_serializing_if = "Option::is_none")]
2734    pub latency: Option<Delay>,
2735
2736    /// When true, drops the TCP connection without responding.
2737    #[serde(skip_serializing_if = "Option::is_none")]
2738    pub connection_drop: Option<bool>,
2739
2740    /// Fixed seed for deterministic probabilistic outcomes.
2741    #[serde(skip_serializing_if = "Option::is_none")]
2742    pub seed: Option<i64>,
2743
2744    /// Any additional fields the server supports that are not modelled above.
2745    #[serde(flatten)]
2746    pub extra: HashMap<String, serde_json::Value>,
2747}
2748
2749impl HttpChaosProfile {
2750    /// Create an empty chaos profile.
2751    pub fn new() -> Self {
2752        Self::default()
2753    }
2754
2755    /// Set the error status code returned on fault.
2756    pub fn error_status(mut self, status: u16) -> Self {
2757        self.error_status = Some(status);
2758        self
2759    }
2760
2761    /// Set the probability (0.0–1.0) of triggering the error.
2762    pub fn error_probability(mut self, probability: f64) -> Self {
2763        self.error_probability = Some(probability);
2764        self
2765    }
2766
2767    /// Set the injected latency.
2768    pub fn latency(mut self, latency: Delay) -> Self {
2769        self.latency = Some(latency);
2770        self
2771    }
2772
2773    /// Drop the TCP connection without responding.
2774    pub fn connection_drop(mut self, drop: bool) -> Self {
2775        self.connection_drop = Some(drop);
2776        self
2777    }
2778
2779    /// Set the deterministic seed.
2780    pub fn seed(mut self, seed: i64) -> Self {
2781        self.seed = Some(seed);
2782        self
2783    }
2784}
2785
2786// ---------------------------------------------------------------------------
2787// Chaos experiment (PUT /mockserver/chaosExperiment)
2788// ---------------------------------------------------------------------------
2789
2790/// A single stage of a chaos experiment. Maps to a `ChaosExperiment.stages[]`
2791/// entry.
2792#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2793#[serde(rename_all = "camelCase")]
2794pub struct ChaosStage {
2795    /// How long this stage runs before advancing (max 86_400_000 = 24h).
2796    pub duration_millis: u64,
2797
2798    /// Map of host -> chaos profile to apply during this stage.
2799    pub profiles: HashMap<String, HttpChaosProfile>,
2800}
2801
2802impl ChaosStage {
2803    /// Create a stage running for `duration_millis`.
2804    pub fn new(duration_millis: u64) -> Self {
2805        Self {
2806            duration_millis,
2807            profiles: HashMap::new(),
2808        }
2809    }
2810
2811    /// Add a host -> chaos profile to apply during the stage.
2812    pub fn profile(mut self, host: impl Into<String>, profile: HttpChaosProfile) -> Self {
2813        self.profiles.insert(host.into(), profile);
2814        self
2815    }
2816}
2817
2818/// A scheduled multi-stage chaos experiment definition. Maps to the
2819/// `ChaosExperiment` schema (the body of `PUT /mockserver/chaosExperiment`).
2820#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2821#[serde(rename_all = "camelCase")]
2822pub struct ChaosExperiment {
2823    /// Human-readable experiment name.
2824    #[serde(skip_serializing_if = "Option::is_none")]
2825    pub name: Option<String>,
2826
2827    /// Whether to loop back to stage 0 after the last stage completes (default
2828    /// false). Serialized as `loop` on the wire.
2829    #[serde(rename = "loop", skip_serializing_if = "Option::is_none")]
2830    pub loop_back: Option<bool>,
2831
2832    /// The ordered sequence of stages.
2833    pub stages: Vec<ChaosStage>,
2834}
2835
2836impl ChaosExperiment {
2837    /// Create an experiment from an ordered list of stages.
2838    pub fn new(stages: Vec<ChaosStage>) -> Self {
2839        Self {
2840            name: None,
2841            loop_back: None,
2842            stages,
2843        }
2844    }
2845
2846    /// Set the experiment name.
2847    pub fn name(mut self, name: impl Into<String>) -> Self {
2848        self.name = Some(name.into());
2849        self
2850    }
2851
2852    /// Set whether the experiment loops back to the first stage.
2853    pub fn loop_back(mut self, loop_back: bool) -> Self {
2854        self.loop_back = Some(loop_back);
2855        self
2856    }
2857}
2858
2859// ---------------------------------------------------------------------------
2860// Tests
2861// ---------------------------------------------------------------------------
2862
2863#[cfg(test)]
2864mod tests {
2865    use super::*;
2866
2867    #[test]
2868    fn test_grpc_services_deserialize_from_server_wire_shape() {
2869        // Mirrors the JSON array produced by `PUT /mockserver/grpc/services`
2870        // in mockserver-core HttpState.java (camelCase keys, full type names).
2871        let wire = r#"[
2872            {
2873                "name": "helloworld.Greeter",
2874                "methods": [
2875                    {
2876                        "name": "SayHello",
2877                        "inputType": "helloworld.HelloRequest",
2878                        "outputType": "helloworld.HelloReply",
2879                        "clientStreaming": false,
2880                        "serverStreaming": false
2881                    },
2882                    {
2883                        "name": "LotsOfReplies",
2884                        "inputType": "helloworld.HelloRequest",
2885                        "outputType": "helloworld.HelloReply",
2886                        "clientStreaming": false,
2887                        "serverStreaming": true
2888                    }
2889                ]
2890            }
2891        ]"#;
2892
2893        let services: Vec<GrpcService> = serde_json::from_str(wire).unwrap();
2894        assert_eq!(services.len(), 1);
2895        let svc = &services[0];
2896        assert_eq!(svc.name, "helloworld.Greeter");
2897        assert_eq!(svc.methods.len(), 2);
2898
2899        let unary = &svc.methods[0];
2900        assert_eq!(unary.name, "SayHello");
2901        assert_eq!(unary.input_type, "helloworld.HelloRequest");
2902        assert_eq!(unary.output_type, "helloworld.HelloReply");
2903        assert!(!unary.client_streaming);
2904        assert!(!unary.server_streaming);
2905
2906        let server_stream = &svc.methods[1];
2907        assert_eq!(server_stream.name, "LotsOfReplies");
2908        assert!(!server_stream.client_streaming);
2909        assert!(server_stream.server_streaming);
2910    }
2911
2912    #[test]
2913    fn test_grpc_method_serializes_with_camel_case_keys() {
2914        let method = GrpcMethod {
2915            name: "BidiChat".into(),
2916            input_type: "chat.Message".into(),
2917            output_type: "chat.Message".into(),
2918            client_streaming: true,
2919            server_streaming: true,
2920        };
2921        let value = serde_json::to_value(&method).unwrap();
2922        assert_eq!(value["name"], "BidiChat");
2923        assert_eq!(value["inputType"], "chat.Message");
2924        assert_eq!(value["outputType"], "chat.Message");
2925        assert_eq!(value["clientStreaming"], true);
2926        assert_eq!(value["serverStreaming"], true);
2927    }
2928
2929    #[test]
2930    fn test_grpc_services_empty_array() {
2931        let services: Vec<GrpcService> = serde_json::from_str("[]").unwrap();
2932        assert!(services.is_empty());
2933    }
2934
2935    #[test]
2936    fn test_grpc_service_round_trips() {
2937        let original = GrpcService {
2938            name: "helloworld.Greeter".into(),
2939            methods: vec![GrpcMethod {
2940                name: "SayHello".into(),
2941                input_type: "helloworld.HelloRequest".into(),
2942                output_type: "helloworld.HelloReply".into(),
2943                client_streaming: false,
2944                server_streaming: false,
2945            }],
2946        };
2947        let json = serde_json::to_string(&original).unwrap();
2948        let parsed: GrpcService = serde_json::from_str(&json).unwrap();
2949        assert_eq!(original, parsed);
2950    }
2951}