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// Pact verification result
2043// ---------------------------------------------------------------------------
2044
2045/// Outcome of a Pact contract verification (`PUT /mockserver/pact/verify`).
2046///
2047/// The server replies `202 ACCEPTED` when every interaction in the contract
2048/// matched an active expectation, or `406 NOT_ACCEPTABLE` when verification
2049/// failed — in both cases the body is the same verification report JSON.
2050#[derive(Debug, Clone, PartialEq, Eq)]
2051pub struct PactVerification {
2052    /// `true` when verification passed (`202`), `false` when it failed (`406`).
2053    pub passed: bool,
2054    /// The verification report JSON returned by the server (verbatim).
2055    pub report: String,
2056}
2057
2058// ---------------------------------------------------------------------------
2059// Operating mode
2060// ---------------------------------------------------------------------------
2061
2062/// High-level operating mode for MockServer (set via `PUT /mockserver/mode`,
2063/// read via `GET /mockserver/mode`).
2064///
2065/// Each mode packages the common record / replay / pass-through workflows into a
2066/// single switch (a convenience over `attemptToProxyIfNoMatchingExpectation`):
2067///
2068/// * [`MockMode::Simulate`] — match expectations and return mocks; unmatched
2069///   requests get a `404`. This is the default (proxy-on-no-match disabled).
2070/// * [`MockMode::Spy`] — match expectations and return mocks, but forward
2071///   unmatched requests to the real upstream so they are served live and recorded
2072///   (proxy-on-no-match enabled).
2073/// * [`MockMode::Capture`] — forward and record; with no expectations defined this
2074///   captures all traffic. Backed by the same proxy flag as [`MockMode::Spy`].
2075#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2076pub enum MockMode {
2077    /// Match expectations; unmatched requests get a `404` (default).
2078    Simulate,
2079    /// Match expectations; unmatched requests forwarded to the upstream and recorded.
2080    Spy,
2081    /// Forward and record all traffic.
2082    Capture,
2083}
2084
2085impl MockMode {
2086    /// The wire value for this mode (the `mode` query parameter / JSON field).
2087    pub fn as_str(&self) -> &'static str {
2088        match self {
2089            MockMode::Simulate => "SIMULATE",
2090            MockMode::Spy => "SPY",
2091            MockMode::Capture => "CAPTURE",
2092        }
2093    }
2094
2095    /// Whether, in this mode, a request matching no expectation is proxied to its
2096    /// upstream (and thereby recorded) rather than answered with a `404`.
2097    pub fn proxy_unmatched_requests(&self) -> bool {
2098        !matches!(self, MockMode::Simulate)
2099    }
2100}
2101
2102impl std::fmt::Display for MockMode {
2103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2104        f.write_str(self.as_str())
2105    }
2106}
2107
2108impl std::str::FromStr for MockMode {
2109    type Err = String;
2110
2111    /// Parse a mode name case-insensitively (matches the server's
2112    /// `MockMode.parse`). Returns an error message for blank/unknown values.
2113    fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
2114        match value.trim().to_uppercase().as_str() {
2115            "" => Err("mode is required (one of SIMULATE, SPY, CAPTURE)".to_string()),
2116            "SIMULATE" => Ok(MockMode::Simulate),
2117            "SPY" => Ok(MockMode::Spy),
2118            "CAPTURE" => Ok(MockMode::Capture),
2119            other => Err(format!(
2120                "unknown mode '{other}' (expected one of SIMULATE, SPY, CAPTURE)"
2121            )),
2122        }
2123    }
2124}
2125
2126// ---------------------------------------------------------------------------
2127// gRPC descriptor management
2128// ---------------------------------------------------------------------------
2129
2130/// A single gRPC method registered from an uploaded descriptor set.
2131///
2132/// Returned by [`MockServerClient::retrieve_grpc_services`] as part of a
2133/// [`GrpcService`]. Maps to the `methods[]` entries of the
2134/// `PUT /mockserver/grpc/services` wire shape.
2135///
2136/// [`MockServerClient::retrieve_grpc_services`]: crate::MockServerClient::retrieve_grpc_services
2137#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
2138#[serde(rename_all = "camelCase")]
2139pub struct GrpcMethod {
2140    /// The simple method name (e.g. `SayHello`).
2141    pub name: String,
2142
2143    /// Fully-qualified name of the request message type.
2144    pub input_type: String,
2145
2146    /// Fully-qualified name of the response message type.
2147    pub output_type: String,
2148
2149    /// Whether the method uses client-side streaming.
2150    pub client_streaming: bool,
2151
2152    /// Whether the method uses server-side streaming.
2153    pub server_streaming: bool,
2154}
2155
2156/// A gRPC service registered from an uploaded descriptor set.
2157///
2158/// Returned by [`MockServerClient::retrieve_grpc_services`]. Maps to the
2159/// top-level entries of the `PUT /mockserver/grpc/services` wire shape.
2160///
2161/// [`MockServerClient::retrieve_grpc_services`]: crate::MockServerClient::retrieve_grpc_services
2162#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
2163#[serde(rename_all = "camelCase")]
2164pub struct GrpcService {
2165    /// Fully-qualified service name (e.g. `helloworld.Greeter`).
2166    pub name: String,
2167
2168    /// The methods declared by this service.
2169    pub methods: Vec<GrpcMethod>,
2170}
2171
2172// ---------------------------------------------------------------------------
2173// SocketAddress
2174// ---------------------------------------------------------------------------
2175
2176/// A downstream socket address (host / port / scheme) to direct a request at.
2177///
2178/// Maps to MockServer's `SocketAddress` model. Used by load-scenario steps to
2179/// target a specific upstream rather than relying on the `Host` header.
2180#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
2181#[serde(rename_all = "camelCase")]
2182pub struct SocketAddress {
2183    /// The downstream host name or IP.
2184    pub host: String,
2185
2186    /// The downstream port.
2187    pub port: u16,
2188
2189    /// The scheme to connect with — `"HTTP"` or `"HTTPS"`. Defaults to `"HTTP"`
2190    /// on the server when omitted.
2191    #[serde(skip_serializing_if = "Option::is_none")]
2192    pub scheme: Option<String>,
2193}
2194
2195impl SocketAddress {
2196    /// Create a plain HTTP socket address.
2197    pub fn new(host: impl Into<String>, port: u16) -> Self {
2198        Self {
2199            host: host.into(),
2200            port,
2201            scheme: None,
2202        }
2203    }
2204
2205    /// Set the scheme (`"HTTP"` or `"HTTPS"`).
2206    pub fn scheme(mut self, scheme: impl Into<String>) -> Self {
2207        self.scheme = Some(scheme.into());
2208        self
2209    }
2210
2211    /// Convenience: an HTTPS socket address.
2212    pub fn https(host: impl Into<String>, port: u16) -> Self {
2213        Self::new(host, port).scheme("HTTPS")
2214    }
2215}
2216
2217// ---------------------------------------------------------------------------
2218// Load scenario registry (PUT/GET/DELETE /mockserver/loadScenario[/...])
2219// ---------------------------------------------------------------------------
2220
2221/// The interpolation curve used to ramp a value (virtual users or arrival
2222/// rate) from a start setpoint to an end setpoint across a ramp [`LoadStage`].
2223/// Maps to the `RampCurve` schema. Only meaningful for ramp stages; ignored for
2224/// holds and pauses.
2225#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2226#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
2227pub enum RampCurve {
2228    /// Constant slope.
2229    Linear,
2230    /// Ease-in: slow then fast.
2231    Quadratic,
2232    /// A steeper ease-in.
2233    Exponential,
2234}
2235
2236/// The kind of a [`LoadStage`].
2237///
2238/// - `Vu` — closed model: hold or ramp the number of concurrent virtual users.
2239/// - `Rate` — open model: hold or ramp an arrival rate in iterations/second.
2240/// - `Pause` — drive no load for the duration.
2241#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2242#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
2243pub enum LoadStageType {
2244    /// Closed model — hold or ramp concurrent virtual users.
2245    Vu,
2246    /// Open model — hold or ramp an arrival rate in iterations/second.
2247    Rate,
2248    /// Drive no load for the duration.
2249    Pause,
2250}
2251
2252/// One stage of a [`LoadProfile`]: a contiguous slice of the run holding or
2253/// ramping a setpoint for `duration_millis`. Stages run in sequence. Maps to the
2254/// `LoadStage` schema.
2255///
2256/// Use the constructors [`LoadStage::vu_hold`], [`LoadStage::vu_ramp`],
2257/// [`LoadStage::rate_hold`], [`LoadStage::rate_ramp`] and [`LoadStage::pause`]
2258/// rather than building the struct directly so only the relevant fields are set
2259/// (and therefore serialized).
2260#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2261#[serde(rename_all = "camelCase")]
2262pub struct LoadStage {
2263    /// The kind of stage — `VU`, `RATE` or `PAUSE`.
2264    #[serde(rename = "type")]
2265    pub stage_type: LoadStageType,
2266
2267    /// How long this stage runs in milliseconds (> 0).
2268    pub duration_millis: u64,
2269
2270    /// Ramp shape (ramp stages only); omitted for holds and pauses.
2271    #[serde(skip_serializing_if = "Option::is_none")]
2272    pub curve: Option<RampCurve>,
2273
2274    /// VU hold: the number of virtual users to hold for the stage.
2275    #[serde(skip_serializing_if = "Option::is_none")]
2276    pub vus: Option<u32>,
2277
2278    /// VU ramp: virtual users at the start of the ramp.
2279    #[serde(skip_serializing_if = "Option::is_none")]
2280    pub start_vus: Option<u32>,
2281
2282    /// VU ramp: virtual users at the end of the ramp.
2283    #[serde(skip_serializing_if = "Option::is_none")]
2284    pub end_vus: Option<u32>,
2285
2286    /// RATE hold: arrival rate to hold, in iterations per second.
2287    #[serde(skip_serializing_if = "Option::is_none")]
2288    pub rate: Option<f64>,
2289
2290    /// RATE ramp: arrival rate at the start of the ramp, in iterations/second.
2291    #[serde(skip_serializing_if = "Option::is_none")]
2292    pub start_rate: Option<f64>,
2293
2294    /// RATE ramp: arrival rate at the end of the ramp, in iterations/second.
2295    #[serde(skip_serializing_if = "Option::is_none")]
2296    pub end_rate: Option<f64>,
2297
2298    /// RATE stage only: optional cap on the auto-scaling virtual-user pool.
2299    #[serde(skip_serializing_if = "Option::is_none")]
2300    pub max_vus: Option<u32>,
2301}
2302
2303impl LoadStage {
2304    fn base(stage_type: LoadStageType, duration_millis: u64) -> Self {
2305        Self {
2306            stage_type,
2307            duration_millis,
2308            curve: None,
2309            vus: None,
2310            start_vus: None,
2311            end_vus: None,
2312            rate: None,
2313            start_rate: None,
2314            end_rate: None,
2315            max_vus: None,
2316        }
2317    }
2318
2319    /// A VU stage holding `vus` virtual users for `duration_millis`.
2320    pub fn vu_hold(vus: u32, duration_millis: u64) -> Self {
2321        let mut stage = Self::base(LoadStageType::Vu, duration_millis);
2322        stage.vus = Some(vus);
2323        stage
2324    }
2325
2326    /// A VU stage ramping from `start_vus` to `end_vus` over `duration_millis`
2327    /// along `curve`.
2328    pub fn vu_ramp(start_vus: u32, end_vus: u32, duration_millis: u64, curve: RampCurve) -> Self {
2329        let mut stage = Self::base(LoadStageType::Vu, duration_millis);
2330        stage.start_vus = Some(start_vus);
2331        stage.end_vus = Some(end_vus);
2332        stage.curve = Some(curve);
2333        stage
2334    }
2335
2336    /// A RATE stage holding `rate` iterations/second for `duration_millis`.
2337    pub fn rate_hold(rate: f64, duration_millis: u64) -> Self {
2338        let mut stage = Self::base(LoadStageType::Rate, duration_millis);
2339        stage.rate = Some(rate);
2340        stage
2341    }
2342
2343    /// A RATE stage ramping from `start_rate` to `end_rate` iterations/second
2344    /// over `duration_millis` along `curve`.
2345    pub fn rate_ramp(
2346        start_rate: f64,
2347        end_rate: f64,
2348        duration_millis: u64,
2349        curve: RampCurve,
2350    ) -> Self {
2351        let mut stage = Self::base(LoadStageType::Rate, duration_millis);
2352        stage.start_rate = Some(start_rate);
2353        stage.end_rate = Some(end_rate);
2354        stage.curve = Some(curve);
2355        stage
2356    }
2357
2358    /// A PAUSE stage that drives no load for `duration_millis`.
2359    pub fn pause(duration_millis: u64) -> Self {
2360        Self::base(LoadStageType::Pause, duration_millis)
2361    }
2362
2363    /// Cap the auto-scaling virtual-user pool for this RATE stage.
2364    pub fn max_vus(mut self, max_vus: u32) -> Self {
2365        self.max_vus = Some(max_vus);
2366        self
2367    }
2368}
2369
2370/// A named load shape that expands server-side into ordinary [`LoadStage`]s.
2371/// Maps to the `LoadShapeType` schema.
2372#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2373#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
2374pub enum LoadShapeType {
2375    /// Ramp up, hold the peak, ramp back down, with an optional recovery hold.
2376    Spike,
2377    /// A flight of pure-hold steps, each one "step" higher.
2378    Stairs,
2379    /// Ramp 0 to target then hold.
2380    RampHold,
2381}
2382
2383/// What a [`LoadShape`] drives. Maps to the `LoadShapeMetric` schema.
2384///
2385/// - `Vu` — concurrent virtual users (closed model).
2386/// - `Rate` — arrival rate in iterations/second (open model).
2387#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2388#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
2389pub enum LoadShapeMetric {
2390    /// Concurrent virtual users (closed model).
2391    Vu,
2392    /// Arrival rate in iterations/second (open model).
2393    Rate,
2394}
2395
2396/// A declarative named load shape that expands into ordinary [`LoadStage`]s.
2397/// Maps to the `LoadShape` schema. Only the parameters its `type` needs are
2398/// read; the rest are ignored. Use a shape OR an explicit `stages` list, not
2399/// both.
2400///
2401/// Use the constructors [`LoadShape::spike`], [`LoadShape::stairs`] and
2402/// [`LoadShape::ramp_hold`] so only the relevant fields are set (and therefore
2403/// serialized).
2404#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2405#[serde(rename_all = "camelCase")]
2406pub struct LoadShape {
2407    /// The named shape — `SPIKE`, `STAIRS` or `RAMP_HOLD`.
2408    #[serde(rename = "type")]
2409    pub shape_type: LoadShapeType,
2410
2411    /// What the shape drives — `VU` (default) or `RATE`.
2412    #[serde(skip_serializing_if = "Option::is_none")]
2413    pub metric: Option<LoadShapeMetric>,
2414
2415    /// Ramp interpolation curve used by the shape's ramps.
2416    #[serde(skip_serializing_if = "Option::is_none")]
2417    pub curve: Option<RampCurve>,
2418
2419    /// SPIKE: the level held before and after the spike.
2420    #[serde(skip_serializing_if = "Option::is_none")]
2421    pub baseline: Option<f64>,
2422
2423    /// SPIKE: the level held at the top of the spike.
2424    #[serde(skip_serializing_if = "Option::is_none")]
2425    pub peak: Option<f64>,
2426
2427    /// SPIKE: duration of the baseline to peak ramp.
2428    #[serde(skip_serializing_if = "Option::is_none")]
2429    pub ramp_up_millis: Option<u64>,
2430
2431    /// SPIKE: duration to hold at the peak; RAMP_HOLD: duration to hold at the
2432    /// target.
2433    #[serde(skip_serializing_if = "Option::is_none")]
2434    pub hold_millis: Option<u64>,
2435
2436    /// SPIKE: duration of the peak to baseline ramp.
2437    #[serde(skip_serializing_if = "Option::is_none")]
2438    pub ramp_down_millis: Option<u64>,
2439
2440    /// SPIKE (optional): duration to hold at baseline after the down ramp.
2441    #[serde(skip_serializing_if = "Option::is_none")]
2442    pub recovery_hold_millis: Option<u64>,
2443
2444    /// STAIRS: the level of the first step.
2445    #[serde(skip_serializing_if = "Option::is_none")]
2446    pub start: Option<f64>,
2447
2448    /// STAIRS: how much each step rises above the previous one.
2449    #[serde(skip_serializing_if = "Option::is_none")]
2450    pub step: Option<f64>,
2451
2452    /// STAIRS: the number of steps.
2453    #[serde(skip_serializing_if = "Option::is_none")]
2454    pub steps: Option<u32>,
2455
2456    /// STAIRS: how long each step holds at its level.
2457    #[serde(skip_serializing_if = "Option::is_none")]
2458    pub step_duration_millis: Option<u64>,
2459
2460    /// RAMP_HOLD: the level ramped up to (from 0) and then held.
2461    #[serde(skip_serializing_if = "Option::is_none")]
2462    pub target: Option<f64>,
2463
2464    /// RAMP_HOLD: duration of the 0 to target ramp.
2465    #[serde(skip_serializing_if = "Option::is_none")]
2466    pub ramp_millis: Option<u64>,
2467}
2468
2469impl LoadShape {
2470    fn base(shape_type: LoadShapeType) -> Self {
2471        Self {
2472            shape_type,
2473            metric: None,
2474            curve: None,
2475            baseline: None,
2476            peak: None,
2477            ramp_up_millis: None,
2478            hold_millis: None,
2479            ramp_down_millis: None,
2480            recovery_hold_millis: None,
2481            start: None,
2482            step: None,
2483            steps: None,
2484            step_duration_millis: None,
2485            target: None,
2486            ramp_millis: None,
2487        }
2488    }
2489
2490    /// A SPIKE shape: ramp `baseline` to `peak` over `ramp_up_millis`, hold for
2491    /// `hold_millis`, then ramp back down over `ramp_down_millis`.
2492    pub fn spike(
2493        baseline: f64,
2494        peak: f64,
2495        ramp_up_millis: u64,
2496        hold_millis: u64,
2497        ramp_down_millis: u64,
2498    ) -> Self {
2499        let mut shape = Self::base(LoadShapeType::Spike);
2500        shape.baseline = Some(baseline);
2501        shape.peak = Some(peak);
2502        shape.ramp_up_millis = Some(ramp_up_millis);
2503        shape.hold_millis = Some(hold_millis);
2504        shape.ramp_down_millis = Some(ramp_down_millis);
2505        shape
2506    }
2507
2508    /// A STAIRS shape: `steps` pure-hold steps, the first at `start` and each
2509    /// rising by `step`, every step holding for `step_duration_millis`.
2510    pub fn stairs(start: f64, step: f64, steps: u32, step_duration_millis: u64) -> Self {
2511        let mut shape = Self::base(LoadShapeType::Stairs);
2512        shape.start = Some(start);
2513        shape.step = Some(step);
2514        shape.steps = Some(steps);
2515        shape.step_duration_millis = Some(step_duration_millis);
2516        shape
2517    }
2518
2519    /// A RAMP_HOLD shape: ramp from 0 to `target` over `ramp_millis`, then hold
2520    /// for `hold_millis`.
2521    pub fn ramp_hold(target: f64, ramp_millis: u64, hold_millis: u64) -> Self {
2522        let mut shape = Self::base(LoadShapeType::RampHold);
2523        shape.target = Some(target);
2524        shape.ramp_millis = Some(ramp_millis);
2525        shape.hold_millis = Some(hold_millis);
2526        shape
2527    }
2528
2529    /// Set what the shape drives (`VU` or `RATE`).
2530    pub fn metric(mut self, metric: LoadShapeMetric) -> Self {
2531        self.metric = Some(metric);
2532        self
2533    }
2534
2535    /// Set the ramp interpolation curve.
2536    pub fn curve(mut self, curve: RampCurve) -> Self {
2537        self.curve = Some(curve);
2538        self
2539    }
2540
2541    /// SPIKE only: hold at baseline for `recovery_hold_millis` after the down
2542    /// ramp.
2543    pub fn recovery_hold_millis(mut self, recovery_hold_millis: u64) -> Self {
2544        self.recovery_hold_millis = Some(recovery_hold_millis);
2545        self
2546    }
2547}
2548
2549/// The per-run metric a [`LoadThreshold`] evaluates. Maps to the threshold
2550/// `metric` enum.
2551#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2552#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
2553pub enum LoadThresholdMetric {
2554    /// 50th-percentile latency in milliseconds.
2555    LatencyP50,
2556    /// 95th-percentile latency in milliseconds.
2557    LatencyP95,
2558    /// 99th-percentile latency in milliseconds.
2559    LatencyP99,
2560    /// 99.9th-percentile latency in milliseconds.
2561    LatencyP999,
2562    /// Failed / requests, as a 0.0-1.0 fraction.
2563    ErrorRate,
2564    /// Throughput in requests/second over the run's elapsed time.
2565    ThroughputRps,
2566}
2567
2568/// How a [`LoadThreshold`]'s observed value is compared to its threshold. Maps
2569/// to the threshold `comparator` enum.
2570#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2571#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
2572pub enum LoadComparator {
2573    /// observed < threshold.
2574    LessThan,
2575    /// observed <= threshold.
2576    LessThanOrEqual,
2577    /// observed > threshold.
2578    GreaterThan,
2579    /// observed >= threshold.
2580    GreaterThanOrEqual,
2581}
2582
2583/// An in-run pass/fail threshold for a load scenario: a per-run metric compared
2584/// against a value. All thresholds must hold for the run verdict to be PASS
2585/// (logical AND). Maps to the `LoadThreshold` schema.
2586#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2587#[serde(rename_all = "camelCase")]
2588pub struct LoadThreshold {
2589    /// The per-run metric to evaluate.
2590    pub metric: LoadThresholdMetric,
2591
2592    /// How the observed per-run value is compared to the threshold.
2593    pub comparator: LoadComparator,
2594
2595    /// The threshold value (milliseconds for latency metrics, a 0.0-1.0
2596    /// fraction for `ERROR_RATE`, requests/second for `THROUGHPUT_RPS`).
2597    pub threshold: f64,
2598}
2599
2600impl LoadThreshold {
2601    /// Create a threshold comparing `metric` to `threshold` using `comparator`.
2602    pub fn new(metric: LoadThresholdMetric, comparator: LoadComparator, threshold: f64) -> Self {
2603        Self {
2604            metric,
2605            comparator,
2606            threshold,
2607        }
2608    }
2609}
2610
2611/// How a [`LoadPacing`] target iteration cycle is derived from its value. Maps
2612/// to the pacing `mode` enum.
2613#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2614#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
2615pub enum LoadPacingMode {
2616    /// No pacing (immediate reschedule).
2617    None,
2618    /// `value` is the target cycle in milliseconds.
2619    ConstantPacing,
2620    /// `value` is the target iterations/second per VU (cycle = 1000 / value ms).
2621    ConstantThroughput,
2622}
2623
2624/// Adaptive iteration pacing (think-time) for a load scenario: a target
2625/// per-virtual-user iteration cycle time. Applies only to the closed-model VU
2626/// loop. Maps to the `LoadPacing` schema.
2627#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2628#[serde(rename_all = "camelCase")]
2629pub struct LoadPacing {
2630    /// How the target iteration cycle is derived from `value`.
2631    pub mode: LoadPacingMode,
2632
2633    /// For `CONSTANT_PACING` the target cycle in milliseconds; for
2634    /// `CONSTANT_THROUGHPUT` the target iterations/second per VU. Must be > 0
2635    /// when `mode` is not `NONE`.
2636    pub value: f64,
2637}
2638
2639impl LoadPacing {
2640    /// Create a pacing rule with the given mode and value.
2641    pub fn new(mode: LoadPacingMode, value: f64) -> Self {
2642        Self { mode, value }
2643    }
2644
2645    /// `CONSTANT_PACING`: target a per-VU iteration cycle of `cycle_millis`.
2646    pub fn constant_pacing(cycle_millis: f64) -> Self {
2647        Self::new(LoadPacingMode::ConstantPacing, cycle_millis)
2648    }
2649
2650    /// `CONSTANT_THROUGHPUT`: target `iterations_per_second` per VU.
2651    pub fn constant_throughput(iterations_per_second: f64) -> Self {
2652        Self::new(LoadPacingMode::ConstantThroughput, iterations_per_second)
2653    }
2654}
2655
2656/// The format of a [`LoadFeeder`]'s raw `data`. Maps to the feeder `format`
2657/// enum.
2658#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2659#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
2660pub enum LoadFeederFormat {
2661    /// CSV: first line is the header row.
2662    Csv,
2663    /// JSON: an array of flat objects.
2664    Json,
2665}
2666
2667/// How a [`LoadFeeder`] selects a row each iteration. Maps to the feeder
2668/// `strategy` enum.
2669#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2670#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
2671pub enum LoadFeederStrategy {
2672    /// Cycle rows and never exhaust (default).
2673    Circular,
2674    /// Pick a uniformly random row each iteration.
2675    Random,
2676    /// Use each row once in order; COMPLETES the run when exhausted.
2677    Sequential,
2678}
2679
2680/// Parameterized test data (a data feeder) for a load scenario: an inline
2681/// dataset from which one row is selected per iteration and exposed to the
2682/// iteration's templates as `$iteration.data.<column>`. Supply EITHER `rows`
2683/// (the primary form) OR `data` + `format`. Maps to the `LoadFeeder` schema.
2684#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2685#[serde(rename_all = "camelCase")]
2686pub struct LoadFeeder {
2687    /// Inline dataset: a list of column-name to value maps, one per row.
2688    #[serde(skip_serializing_if = "Vec::is_empty")]
2689    pub rows: Vec<HashMap<String, String>>,
2690
2691    /// Optional raw inline dataset parsed server-side into rows per `format`.
2692    #[serde(skip_serializing_if = "Option::is_none")]
2693    pub data: Option<String>,
2694
2695    /// The format of `data` (required when `data` is set).
2696    #[serde(skip_serializing_if = "Option::is_none")]
2697    pub format: Option<LoadFeederFormat>,
2698
2699    /// How a row is chosen each iteration.
2700    #[serde(skip_serializing_if = "Option::is_none")]
2701    pub strategy: Option<LoadFeederStrategy>,
2702}
2703
2704impl LoadFeeder {
2705    /// A feeder from an inline list of column-name to value rows.
2706    pub fn rows(rows: Vec<HashMap<String, String>>) -> Self {
2707        Self {
2708            rows,
2709            ..Self::default()
2710        }
2711    }
2712
2713    /// A feeder from raw inline `data` parsed server-side as `format`.
2714    pub fn data(data: impl Into<String>, format: LoadFeederFormat) -> Self {
2715        Self {
2716            data: Some(data.into()),
2717            format: Some(format),
2718            ..Self::default()
2719        }
2720    }
2721
2722    /// Set the row-selection strategy.
2723    pub fn strategy(mut self, strategy: LoadFeederStrategy) -> Self {
2724        self.strategy = Some(strategy);
2725        self
2726    }
2727}
2728
2729/// Where a [`LoadCapture`] extracts its value from. Maps to the capture
2730/// `source` enum.
2731#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2732#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
2733pub enum LoadCaptureSource {
2734    /// A JSONPath over the response body.
2735    BodyJsonpath,
2736    /// A response header value.
2737    Header,
2738    /// A regex over the response body string (capture group 1).
2739    BodyRegex,
2740}
2741
2742/// A declarative cross-step capture / correlation rule: extracts a value from a
2743/// step's response and binds it to a variable name a later step in the same
2744/// iteration can reference via `$iteration.captured.<name>`. Best-effort. Maps
2745/// to the `LoadCapture` schema.
2746#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2747#[serde(rename_all = "camelCase")]
2748pub struct LoadCapture {
2749    /// The variable name later steps reference.
2750    pub name: String,
2751
2752    /// Where to extract from.
2753    pub source: LoadCaptureSource,
2754
2755    /// The JSONPath, header name, or regex driving the extraction.
2756    pub expression: String,
2757
2758    /// Optional fallback value bound when extraction yields nothing.
2759    #[serde(skip_serializing_if = "Option::is_none")]
2760    pub default_value: Option<String>,
2761}
2762
2763impl LoadCapture {
2764    /// Create a capture binding `name` to the value extracted from `source` via
2765    /// `expression`.
2766    pub fn new(
2767        name: impl Into<String>,
2768        source: LoadCaptureSource,
2769        expression: impl Into<String>,
2770    ) -> Self {
2771        Self {
2772            name: name.into(),
2773            source,
2774            expression: expression.into(),
2775            default_value: None,
2776        }
2777    }
2778
2779    /// Set the fallback value bound to the variable on no match.
2780    pub fn default_value(mut self, default_value: impl Into<String>) -> Self {
2781        self.default_value = Some(default_value.into());
2782        self
2783    }
2784}
2785
2786/// How each iteration of a load scenario selects which steps to run. Maps to
2787/// the `stepSelection` enum.
2788#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2789#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
2790pub enum LoadStepSelection {
2791    /// Run ALL steps in declared order (a multi-step user journey).
2792    Sequential,
2793    /// Run exactly ONE step per iteration chosen at random by weight.
2794    Weighted,
2795}
2796
2797/// The load profile of a load scenario: EITHER an ordered list of [`LoadStage`]s
2798/// run in sequence, OR a single named [`LoadShape`] that expands into stages.
2799/// Maps to the `LoadProfile` schema.
2800///
2801/// Use [`LoadProfile::of`] to build from a list of stages, the convenience
2802/// constructors [`LoadProfile::constant`] / [`LoadProfile::linear`] for a single
2803/// VU stage, or [`LoadProfile::shaped`] for a named shape.
2804#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2805#[serde(rename_all = "camelCase")]
2806pub struct LoadProfile {
2807    /// Ordered stages run one after another. Omitted (empty) when a `shape` is
2808    /// used.
2809    #[serde(skip_serializing_if = "Vec::is_empty")]
2810    pub stages: Vec<LoadStage>,
2811
2812    /// A named shape that expands server-side into stages. Use a shape OR
2813    /// `stages`, not both.
2814    #[serde(skip_serializing_if = "Option::is_none")]
2815    pub shape: Option<LoadShape>,
2816}
2817
2818impl LoadProfile {
2819    /// A profile from an explicit list of stages.
2820    pub fn of(stages: Vec<LoadStage>) -> Self {
2821        Self {
2822            stages,
2823            shape: None,
2824        }
2825    }
2826
2827    /// A profile from a single named [`LoadShape`].
2828    pub fn shaped(shape: LoadShape) -> Self {
2829        Self {
2830            stages: Vec::new(),
2831            shape: Some(shape),
2832        }
2833    }
2834
2835    /// A single VU stage holding `vus` virtual users for `duration_millis`.
2836    pub fn constant(vus: u32, duration_millis: u64) -> Self {
2837        Self::of(vec![LoadStage::vu_hold(vus, duration_millis)])
2838    }
2839
2840    /// A single linear VU ramp from `start_vus` to `end_vus` over
2841    /// `duration_millis`.
2842    pub fn linear(start_vus: u32, end_vus: u32, duration_millis: u64) -> Self {
2843        Self::of(vec![LoadStage::vu_ramp(
2844            start_vus,
2845            end_vus,
2846            duration_millis,
2847            RampCurve::Linear,
2848        )])
2849    }
2850
2851    /// Append a stage and return the profile.
2852    pub fn add_stage(mut self, stage: LoadStage) -> Self {
2853        self.stages.push(stage);
2854        self
2855    }
2856}
2857
2858/// A single templated request step in a load scenario. Maps to the `LoadStep`
2859/// schema.
2860#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2861#[serde(rename_all = "camelCase")]
2862pub struct LoadStep {
2863    /// The templated request to fire each iteration.
2864    pub request: HttpRequest,
2865
2866    /// Optional inter-step pause (a [`Delay`]).
2867    #[serde(skip_serializing_if = "Option::is_none")]
2868    pub think_time: Option<Delay>,
2869
2870    /// Optional cross-step capture rules applied to this step's response. Each
2871    /// binds an extracted value to a variable name visible to SUBSEQUENT steps
2872    /// in the same iteration.
2873    #[serde(skip_serializing_if = "Vec::is_empty")]
2874    pub captures: Vec<LoadCapture>,
2875
2876    /// Relative selection weight, used only when the scenario's
2877    /// `stepSelection` is `WEIGHTED`. Must be > 0 when `WEIGHTED`; ignored
2878    /// under the default `SEQUENTIAL` mode.
2879    #[serde(skip_serializing_if = "Option::is_none")]
2880    pub weight: Option<f64>,
2881}
2882
2883impl LoadStep {
2884    /// Create a step from a request matcher/template.
2885    pub fn new(request: HttpRequest) -> Self {
2886        Self {
2887            request,
2888            think_time: None,
2889            captures: Vec::new(),
2890            weight: None,
2891        }
2892    }
2893
2894    /// Set the inter-step pause.
2895    pub fn think_time(mut self, delay: Delay) -> Self {
2896        self.think_time = Some(delay);
2897        self
2898    }
2899
2900    /// Append a cross-step capture rule applied to this step's response.
2901    pub fn capture(mut self, capture: LoadCapture) -> Self {
2902        self.captures.push(capture);
2903        self
2904    }
2905
2906    /// Set the relative selection weight (used only under `WEIGHTED`
2907    /// `stepSelection`).
2908    pub fn weight(mut self, weight: f64) -> Self {
2909        self.weight = Some(weight);
2910        self
2911    }
2912}
2913
2914/// An API-driven load scenario: ordered templated steps driven at a target
2915/// concurrency. Maps to the `LoadScenario` schema (the body of
2916/// `PUT /mockserver/loadScenario`, which registers the scenario in the
2917/// registry without running it). The unique [`name`](LoadScenario::name) is the
2918/// registry key used by `start`/`stop` and the per-scenario `GET`/`DELETE`
2919/// endpoints.
2920#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2921#[serde(rename_all = "camelCase")]
2922pub struct LoadScenario {
2923    /// Human-readable scenario name.
2924    pub name: String,
2925
2926    /// Template engine for per-iteration rendering — `"VELOCITY"` (default) or
2927    /// `"MUSTACHE"`. (JavaScript is rejected for load steps.)
2928    #[serde(skip_serializing_if = "Option::is_none")]
2929    pub template_type: Option<String>,
2930
2931    /// Optional hard cap on the total number of requests dispatched.
2932    #[serde(skip_serializing_if = "Option::is_none")]
2933    pub max_requests: Option<u64>,
2934
2935    /// Optional delay (milliseconds) applied between a `start` request being
2936    /// accepted and the scenario actually beginning to drive load. Honoured by
2937    /// `PUT /mockserver/loadScenario/start`.
2938    #[serde(skip_serializing_if = "Option::is_none")]
2939    pub start_delay_millis: Option<u64>,
2940
2941    /// Optional in-run pass/fail thresholds; the run carries a PASS verdict iff
2942    /// all hold, FAIL otherwise. Empty/omitted means no verdict is computed.
2943    #[serde(skip_serializing_if = "Vec::is_empty")]
2944    pub thresholds: Vec<LoadThreshold>,
2945
2946    /// When true, a FAIL verdict aborts the run early. Default false (omitted).
2947    #[serde(skip_serializing_if = "std::ops::Not::not")]
2948    pub abort_on_fail: bool,
2949
2950    /// Suppress `abort_on_fail` for the first N milliseconds of the run so noisy
2951    /// startup samples cannot trigger a premature abort.
2952    #[serde(skip_serializing_if = "Option::is_none")]
2953    pub abort_grace_millis: Option<u64>,
2954
2955    /// Optional adaptive iteration pacing (closed-model VU loop only).
2956    #[serde(skip_serializing_if = "Option::is_none")]
2957    pub pacing: Option<LoadPacing>,
2958
2959    /// Optional parameterized test data (a data feeder).
2960    #[serde(skip_serializing_if = "Option::is_none")]
2961    pub feeder: Option<LoadFeeder>,
2962
2963    /// How each iteration selects which steps to run — `SEQUENTIAL` (default,
2964    /// omitted) or `WEIGHTED`.
2965    #[serde(skip_serializing_if = "Option::is_none")]
2966    pub step_selection: Option<LoadStepSelection>,
2967
2968    /// The ramp profile.
2969    pub profile: LoadProfile,
2970
2971    /// Ordered list of request steps fired in sequence each iteration (max 50).
2972    pub steps: Vec<LoadStep>,
2973}
2974
2975impl LoadScenario {
2976    /// Create a scenario with the given name, profile and steps.
2977    pub fn new(name: impl Into<String>, profile: LoadProfile, steps: Vec<LoadStep>) -> Self {
2978        Self {
2979            name: name.into(),
2980            template_type: None,
2981            max_requests: None,
2982            start_delay_millis: None,
2983            thresholds: Vec::new(),
2984            abort_on_fail: false,
2985            abort_grace_millis: None,
2986            pacing: None,
2987            feeder: None,
2988            step_selection: None,
2989            profile,
2990            steps,
2991        }
2992    }
2993
2994    /// Add an in-run pass/fail threshold.
2995    pub fn threshold(mut self, threshold: LoadThreshold) -> Self {
2996        self.thresholds.push(threshold);
2997        self
2998    }
2999
3000    /// Set whether a FAIL verdict aborts the run early.
3001    pub fn abort_on_fail(mut self, abort_on_fail: bool) -> Self {
3002        self.abort_on_fail = abort_on_fail;
3003        self
3004    }
3005
3006    /// Set the abort grace window (milliseconds) for `abort_on_fail`.
3007    pub fn abort_grace_millis(mut self, abort_grace_millis: u64) -> Self {
3008        self.abort_grace_millis = Some(abort_grace_millis);
3009        self
3010    }
3011
3012    /// Set the adaptive iteration pacing.
3013    pub fn pacing(mut self, pacing: LoadPacing) -> Self {
3014        self.pacing = Some(pacing);
3015        self
3016    }
3017
3018    /// Set the parameterized test data feeder.
3019    pub fn feeder(mut self, feeder: LoadFeeder) -> Self {
3020        self.feeder = Some(feeder);
3021        self
3022    }
3023
3024    /// Set how each iteration selects which steps to run.
3025    pub fn step_selection(mut self, step_selection: LoadStepSelection) -> Self {
3026        self.step_selection = Some(step_selection);
3027        self
3028    }
3029
3030    /// Set the template engine (`"VELOCITY"` or `"MUSTACHE"`).
3031    pub fn template_type(mut self, template_type: impl Into<String>) -> Self {
3032        self.template_type = Some(template_type.into());
3033        self
3034    }
3035
3036    /// Set the hard cap on total requests dispatched.
3037    pub fn max_requests(mut self, max_requests: u64) -> Self {
3038        self.max_requests = Some(max_requests);
3039        self
3040    }
3041
3042    /// Set the delay (milliseconds) before the scenario begins driving load
3043    /// once started.
3044    pub fn start_delay_millis(mut self, start_delay_millis: u64) -> Self {
3045        self.start_delay_millis = Some(start_delay_millis);
3046        self
3047    }
3048}
3049
3050// ---------------------------------------------------------------------------
3051// SLO verdicts (PUT /mockserver/verifySLO)
3052// ---------------------------------------------------------------------------
3053
3054/// A single service-level objective over the recorded SLI samples. Maps to the
3055/// `SloObjective` schema.
3056#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3057#[serde(rename_all = "camelCase")]
3058pub struct SloObjective {
3059    /// The indicator to evaluate — one of `LATENCY_P50`, `LATENCY_P95`,
3060    /// `LATENCY_P99`, `ERROR_RATE`.
3061    pub sli: String,
3062
3063    /// How the observed value is compared to the threshold — one of
3064    /// `LESS_THAN`, `LESS_THAN_OR_EQUAL`, `GREATER_THAN`,
3065    /// `GREATER_THAN_OR_EQUAL`.
3066    pub comparator: String,
3067
3068    /// The objective threshold (milliseconds for latency SLIs, a 0.0–1.0
3069    /// fraction for `ERROR_RATE`).
3070    pub threshold: f64,
3071
3072    /// Which recorded traffic to evaluate — `"FORWARD"` (default) or
3073    /// `"INBOUND"`.
3074    #[serde(skip_serializing_if = "Option::is_none")]
3075    pub scope: Option<String>,
3076}
3077
3078impl SloObjective {
3079    /// Create an objective.
3080    pub fn new(sli: impl Into<String>, comparator: impl Into<String>, threshold: f64) -> Self {
3081        Self {
3082            sli: sli.into(),
3083            comparator: comparator.into(),
3084            threshold,
3085            scope: None,
3086        }
3087    }
3088
3089    /// Set the evaluation scope (`"FORWARD"` or `"INBOUND"`).
3090    pub fn scope(mut self, scope: impl Into<String>) -> Self {
3091        self.scope = Some(scope.into());
3092        self
3093    }
3094}
3095
3096/// The time window of an SLO evaluation. Maps to the `SloCriteria.window`
3097/// object.
3098#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
3099#[serde(rename_all = "camelCase")]
3100pub struct SloWindow {
3101    /// `"LOOKBACK"` (default) or `"EXPLICIT"`.
3102    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
3103    pub window_type: Option<String>,
3104
3105    /// LOOKBACK: window length ending now.
3106    #[serde(skip_serializing_if = "Option::is_none")]
3107    pub lookback_millis: Option<u64>,
3108
3109    /// EXPLICIT: window start in epoch milliseconds.
3110    #[serde(skip_serializing_if = "Option::is_none")]
3111    pub from_epoch_millis: Option<u64>,
3112
3113    /// EXPLICIT: window end in epoch milliseconds.
3114    #[serde(skip_serializing_if = "Option::is_none")]
3115    pub to_epoch_millis: Option<u64>,
3116}
3117
3118impl SloWindow {
3119    /// A LOOKBACK window of `millis` ending now.
3120    pub fn lookback(millis: u64) -> Self {
3121        Self {
3122            window_type: Some("LOOKBACK".to_string()),
3123            lookback_millis: Some(millis),
3124            ..Default::default()
3125        }
3126    }
3127
3128    /// An EXPLICIT window between two epoch-millisecond bounds.
3129    pub fn explicit(from_epoch_millis: u64, to_epoch_millis: u64) -> Self {
3130        Self {
3131            window_type: Some("EXPLICIT".to_string()),
3132            from_epoch_millis: Some(from_epoch_millis),
3133            to_epoch_millis: Some(to_epoch_millis),
3134            ..Default::default()
3135        }
3136    }
3137}
3138
3139/// A named set of service-level objectives over a time window. Maps to the
3140/// `SloCriteria` schema (the body of `PUT /mockserver/verifySLO`).
3141#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3142#[serde(rename_all = "camelCase")]
3143pub struct SloCriteria {
3144    /// Human-readable criteria name, echoed back in the verdict.
3145    #[serde(skip_serializing_if = "Option::is_none")]
3146    pub name: Option<String>,
3147
3148    /// The time window to evaluate over.
3149    #[serde(skip_serializing_if = "Option::is_none")]
3150    pub window: Option<SloWindow>,
3151
3152    /// Minimum samples required in the window; below this the verdict is
3153    /// INCONCLUSIVE.
3154    #[serde(skip_serializing_if = "Option::is_none")]
3155    pub minimum_sample_count: Option<u64>,
3156
3157    /// Optional list of upstream hosts to restrict the evaluation to.
3158    #[serde(skip_serializing_if = "Option::is_none")]
3159    pub upstream_hosts: Option<Vec<String>>,
3160
3161    /// The objectives (the verdict is the logical AND of all of them).
3162    pub objectives: Vec<SloObjective>,
3163}
3164
3165impl SloCriteria {
3166    /// Create criteria from a set of objectives.
3167    pub fn new(objectives: Vec<SloObjective>) -> Self {
3168        Self {
3169            name: None,
3170            window: None,
3171            minimum_sample_count: None,
3172            upstream_hosts: None,
3173            objectives,
3174        }
3175    }
3176
3177    /// Set the criteria name.
3178    pub fn name(mut self, name: impl Into<String>) -> Self {
3179        self.name = Some(name.into());
3180        self
3181    }
3182
3183    /// Set the evaluation window.
3184    pub fn window(mut self, window: SloWindow) -> Self {
3185        self.window = Some(window);
3186        self
3187    }
3188
3189    /// Set the minimum sample count.
3190    pub fn minimum_sample_count(mut self, count: u64) -> Self {
3191        self.minimum_sample_count = Some(count);
3192        self
3193    }
3194
3195    /// Restrict the evaluation to the given upstream hosts.
3196    pub fn upstream_hosts(mut self, hosts: Vec<String>) -> Self {
3197        self.upstream_hosts = Some(hosts);
3198        self
3199    }
3200}
3201
3202/// The evaluated result of a single objective. Maps to the `SloObjectiveResult`
3203/// schema.
3204#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3205#[serde(rename_all = "camelCase")]
3206pub struct SloObjectiveResult {
3207    #[serde(skip_serializing_if = "Option::is_none")]
3208    pub sli: Option<String>,
3209    #[serde(skip_serializing_if = "Option::is_none")]
3210    pub comparator: Option<String>,
3211    #[serde(skip_serializing_if = "Option::is_none")]
3212    pub threshold: Option<f64>,
3213    #[serde(skip_serializing_if = "Option::is_none")]
3214    pub observed_value: Option<f64>,
3215    /// `PASS`, `FAIL` or `INCONCLUSIVE`.
3216    #[serde(skip_serializing_if = "Option::is_none")]
3217    pub result: Option<String>,
3218    #[serde(skip_serializing_if = "Option::is_none")]
3219    pub detail: Option<String>,
3220}
3221
3222/// The overall verdict of an SLO evaluation. Maps to the `SloVerdict` schema —
3223/// the response of `PUT /mockserver/verifySLO`.
3224#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
3225#[serde(rename_all = "camelCase")]
3226pub struct SloVerdict {
3227    #[serde(skip_serializing_if = "Option::is_none")]
3228    pub name: Option<String>,
3229    /// `PASS`, `FAIL` or `INCONCLUSIVE`.
3230    #[serde(skip_serializing_if = "Option::is_none")]
3231    pub result: Option<String>,
3232    #[serde(skip_serializing_if = "Option::is_none")]
3233    pub window_from_epoch_millis: Option<u64>,
3234    #[serde(skip_serializing_if = "Option::is_none")]
3235    pub window_to_epoch_millis: Option<u64>,
3236    #[serde(skip_serializing_if = "Option::is_none")]
3237    pub sample_count: Option<u64>,
3238    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3239    pub objective_results: Vec<SloObjectiveResult>,
3240}
3241
3242impl SloVerdict {
3243    /// Whether the overall verdict is `PASS`.
3244    pub fn is_pass(&self) -> bool {
3245        self.result.as_deref() == Some("PASS")
3246    }
3247
3248    /// Whether the overall verdict is `FAIL`.
3249    pub fn is_fail(&self) -> bool {
3250        self.result.as_deref() == Some("FAIL")
3251    }
3252
3253    /// Whether the overall verdict is `INCONCLUSIVE`.
3254    pub fn is_inconclusive(&self) -> bool {
3255        self.result.as_deref() == Some("INCONCLUSIVE")
3256    }
3257}
3258
3259// ---------------------------------------------------------------------------
3260// Preemption (PUT/GET/DELETE /mockserver/preemption)
3261// ---------------------------------------------------------------------------
3262
3263/// Preemption simulation parameters (all fields optional). Maps to the
3264/// `PreemptionRequest` schema (the body of `PUT /mockserver/preemption`).
3265#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
3266#[serde(rename_all = "camelCase")]
3267pub struct PreemptionRequest {
3268    /// How draining is signalled — `"reject503"`, `"goaway"` or `"both"`
3269    /// (default).
3270    #[serde(skip_serializing_if = "Option::is_none")]
3271    pub mode: Option<String>,
3272
3273    /// How long in-flight requests are allowed to drain (clamped server-side).
3274    #[serde(skip_serializing_if = "Option::is_none")]
3275    pub drain_millis: Option<u64>,
3276
3277    /// Auto-uncordon after this many milliseconds (dead-man's switch); `0`
3278    /// (default) means no auto-uncordon.
3279    #[serde(skip_serializing_if = "Option::is_none")]
3280    pub ttl_millis: Option<u64>,
3281
3282    /// HTTP/2 GOAWAY `last_stream_id` to advertise; `-1` (default) lets the
3283    /// server choose.
3284    #[serde(skip_serializing_if = "Option::is_none")]
3285    pub last_stream_id: Option<i64>,
3286}
3287
3288impl PreemptionRequest {
3289    /// An empty request (server defaults: mode "both", default drain, no TTL).
3290    pub fn new() -> Self {
3291        Self::default()
3292    }
3293
3294    /// Set the signalling mode (`"reject503"`, `"goaway"` or `"both"`).
3295    pub fn mode(mut self, mode: impl Into<String>) -> Self {
3296        self.mode = Some(mode.into());
3297        self
3298    }
3299
3300    /// Set the drain window in milliseconds.
3301    pub fn drain_millis(mut self, millis: u64) -> Self {
3302        self.drain_millis = Some(millis);
3303        self
3304    }
3305
3306    /// Set the auto-uncordon TTL in milliseconds.
3307    pub fn ttl_millis(mut self, millis: u64) -> Self {
3308        self.ttl_millis = Some(millis);
3309        self
3310    }
3311
3312    /// Set the HTTP/2 GOAWAY `last_stream_id` to advertise.
3313    pub fn last_stream_id(mut self, id: i64) -> Self {
3314        self.last_stream_id = Some(id);
3315        self
3316    }
3317}
3318
3319/// The current cordon/drain status of the server. Maps to the
3320/// `PreemptionStatus` schema — the response of `PUT`/`GET /mockserver/preemption`.
3321#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
3322#[serde(rename_all = "camelCase")]
3323pub struct PreemptionStatus {
3324    /// `"inactive"`, `"draining"` or `"drained"`.
3325    #[serde(skip_serializing_if = "Option::is_none")]
3326    pub state: Option<String>,
3327
3328    /// Number of requests currently in flight.
3329    #[serde(skip_serializing_if = "Option::is_none")]
3330    pub in_flight: Option<u64>,
3331
3332    /// Milliseconds left in the drain window.
3333    #[serde(skip_serializing_if = "Option::is_none")]
3334    pub drain_remaining_millis: Option<u64>,
3335
3336    /// Active signalling mode (omitted when inactive).
3337    #[serde(skip_serializing_if = "Option::is_none")]
3338    pub mode: Option<String>,
3339}
3340
3341// ---------------------------------------------------------------------------
3342// Service chaos (PUT /mockserver/serviceChaos)
3343// ---------------------------------------------------------------------------
3344
3345/// An HTTP chaos / fault-injection profile for a host or expectation. Maps to
3346/// the `HttpChaosProfile` schema. Captures the commonly-used fields; the model
3347/// carries an `extra` map for any additional server-supported keys.
3348#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
3349#[serde(rename_all = "camelCase")]
3350pub struct HttpChaosProfile {
3351    /// HTTP error status code to return instead of the real response.
3352    #[serde(skip_serializing_if = "Option::is_none")]
3353    pub error_status: Option<u16>,
3354
3355    /// Probability (0.0–1.0) that a request triggers the error.
3356    #[serde(skip_serializing_if = "Option::is_none")]
3357    pub error_probability: Option<f64>,
3358
3359    /// Injected latency (a [`Delay`]).
3360    #[serde(skip_serializing_if = "Option::is_none")]
3361    pub latency: Option<Delay>,
3362
3363    /// When true, drops the TCP connection without responding.
3364    #[serde(skip_serializing_if = "Option::is_none")]
3365    pub connection_drop: Option<bool>,
3366
3367    /// Fixed seed for deterministic probabilistic outcomes.
3368    #[serde(skip_serializing_if = "Option::is_none")]
3369    pub seed: Option<i64>,
3370
3371    /// Any additional fields the server supports that are not modelled above.
3372    #[serde(flatten)]
3373    pub extra: HashMap<String, serde_json::Value>,
3374}
3375
3376impl HttpChaosProfile {
3377    /// Create an empty chaos profile.
3378    pub fn new() -> Self {
3379        Self::default()
3380    }
3381
3382    /// Set the error status code returned on fault.
3383    pub fn error_status(mut self, status: u16) -> Self {
3384        self.error_status = Some(status);
3385        self
3386    }
3387
3388    /// Set the probability (0.0–1.0) of triggering the error.
3389    pub fn error_probability(mut self, probability: f64) -> Self {
3390        self.error_probability = Some(probability);
3391        self
3392    }
3393
3394    /// Set the injected latency.
3395    pub fn latency(mut self, latency: Delay) -> Self {
3396        self.latency = Some(latency);
3397        self
3398    }
3399
3400    /// Drop the TCP connection without responding.
3401    pub fn connection_drop(mut self, drop: bool) -> Self {
3402        self.connection_drop = Some(drop);
3403        self
3404    }
3405
3406    /// Set the deterministic seed.
3407    pub fn seed(mut self, seed: i64) -> Self {
3408        self.seed = Some(seed);
3409        self
3410    }
3411}
3412
3413// ---------------------------------------------------------------------------
3414// Chaos experiment (PUT /mockserver/chaosExperiment)
3415// ---------------------------------------------------------------------------
3416
3417/// A single stage of a chaos experiment. Maps to a `ChaosExperiment.stages[]`
3418/// entry.
3419#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
3420#[serde(rename_all = "camelCase")]
3421pub struct ChaosStage {
3422    /// How long this stage runs before advancing (max 86_400_000 = 24h).
3423    pub duration_millis: u64,
3424
3425    /// Map of host -> chaos profile to apply during this stage.
3426    pub profiles: HashMap<String, HttpChaosProfile>,
3427}
3428
3429impl ChaosStage {
3430    /// Create a stage running for `duration_millis`.
3431    pub fn new(duration_millis: u64) -> Self {
3432        Self {
3433            duration_millis,
3434            profiles: HashMap::new(),
3435        }
3436    }
3437
3438    /// Add a host -> chaos profile to apply during the stage.
3439    pub fn profile(mut self, host: impl Into<String>, profile: HttpChaosProfile) -> Self {
3440        self.profiles.insert(host.into(), profile);
3441        self
3442    }
3443}
3444
3445/// A scheduled multi-stage chaos experiment definition. Maps to the
3446/// `ChaosExperiment` schema (the body of `PUT /mockserver/chaosExperiment`).
3447#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3448#[serde(rename_all = "camelCase")]
3449pub struct ChaosExperiment {
3450    /// Human-readable experiment name.
3451    #[serde(skip_serializing_if = "Option::is_none")]
3452    pub name: Option<String>,
3453
3454    /// Whether to loop back to stage 0 after the last stage completes (default
3455    /// false). Serialized as `loop` on the wire.
3456    #[serde(rename = "loop", skip_serializing_if = "Option::is_none")]
3457    pub loop_back: Option<bool>,
3458
3459    /// The ordered sequence of stages.
3460    pub stages: Vec<ChaosStage>,
3461}
3462
3463impl ChaosExperiment {
3464    /// Create an experiment from an ordered list of stages.
3465    pub fn new(stages: Vec<ChaosStage>) -> Self {
3466        Self {
3467            name: None,
3468            loop_back: None,
3469            stages,
3470        }
3471    }
3472
3473    /// Set the experiment name.
3474    pub fn name(mut self, name: impl Into<String>) -> Self {
3475        self.name = Some(name.into());
3476        self
3477    }
3478
3479    /// Set whether the experiment loops back to the first stage.
3480    pub fn loop_back(mut self, loop_back: bool) -> Self {
3481        self.loop_back = Some(loop_back);
3482        self
3483    }
3484}
3485
3486// ---------------------------------------------------------------------------
3487// Tests
3488// ---------------------------------------------------------------------------
3489
3490#[cfg(test)]
3491mod tests {
3492    use super::*;
3493
3494    #[test]
3495    fn test_grpc_services_deserialize_from_server_wire_shape() {
3496        // Mirrors the JSON array produced by `PUT /mockserver/grpc/services`
3497        // in mockserver-core HttpState.java (camelCase keys, full type names).
3498        let wire = r#"[
3499            {
3500                "name": "helloworld.Greeter",
3501                "methods": [
3502                    {
3503                        "name": "SayHello",
3504                        "inputType": "helloworld.HelloRequest",
3505                        "outputType": "helloworld.HelloReply",
3506                        "clientStreaming": false,
3507                        "serverStreaming": false
3508                    },
3509                    {
3510                        "name": "LotsOfReplies",
3511                        "inputType": "helloworld.HelloRequest",
3512                        "outputType": "helloworld.HelloReply",
3513                        "clientStreaming": false,
3514                        "serverStreaming": true
3515                    }
3516                ]
3517            }
3518        ]"#;
3519
3520        let services: Vec<GrpcService> = serde_json::from_str(wire).unwrap();
3521        assert_eq!(services.len(), 1);
3522        let svc = &services[0];
3523        assert_eq!(svc.name, "helloworld.Greeter");
3524        assert_eq!(svc.methods.len(), 2);
3525
3526        let unary = &svc.methods[0];
3527        assert_eq!(unary.name, "SayHello");
3528        assert_eq!(unary.input_type, "helloworld.HelloRequest");
3529        assert_eq!(unary.output_type, "helloworld.HelloReply");
3530        assert!(!unary.client_streaming);
3531        assert!(!unary.server_streaming);
3532
3533        let server_stream = &svc.methods[1];
3534        assert_eq!(server_stream.name, "LotsOfReplies");
3535        assert!(!server_stream.client_streaming);
3536        assert!(server_stream.server_streaming);
3537    }
3538
3539    #[test]
3540    fn test_grpc_method_serializes_with_camel_case_keys() {
3541        let method = GrpcMethod {
3542            name: "BidiChat".into(),
3543            input_type: "chat.Message".into(),
3544            output_type: "chat.Message".into(),
3545            client_streaming: true,
3546            server_streaming: true,
3547        };
3548        let value = serde_json::to_value(&method).unwrap();
3549        assert_eq!(value["name"], "BidiChat");
3550        assert_eq!(value["inputType"], "chat.Message");
3551        assert_eq!(value["outputType"], "chat.Message");
3552        assert_eq!(value["clientStreaming"], true);
3553        assert_eq!(value["serverStreaming"], true);
3554    }
3555
3556    #[test]
3557    fn test_grpc_services_empty_array() {
3558        let services: Vec<GrpcService> = serde_json::from_str("[]").unwrap();
3559        assert!(services.is_empty());
3560    }
3561
3562    #[test]
3563    fn test_grpc_service_round_trips() {
3564        let original = GrpcService {
3565            name: "helloworld.Greeter".into(),
3566            methods: vec![GrpcMethod {
3567                name: "SayHello".into(),
3568                input_type: "helloworld.HelloRequest".into(),
3569                output_type: "helloworld.HelloReply".into(),
3570                client_streaming: false,
3571                server_streaming: false,
3572            }],
3573        };
3574        let json = serde_json::to_string(&original).unwrap();
3575        let parsed: GrpcService = serde_json::from_str(&json).unwrap();
3576        assert_eq!(original, parsed);
3577    }
3578}