Skip to main content

mockserver_client/
client.rs

1//! The MockServer client and its builder.
2
3use reqwest::blocking::Client;
4use serde_json::Value;
5
6use crate::breakpoint::{
7    BreakpointMatcherList, BreakpointMatcherRegistration, BreakpointMatcherResponse,
8    BreakpointRequestHandler, BreakpointResponseHandler, BreakpointStreamFrameHandler,
9    BreakpointWebSocketClient,
10};
11use crate::error::{Error, Result};
12use crate::model::*;
13
14// ---------------------------------------------------------------------------
15// ClientBuilder
16// ---------------------------------------------------------------------------
17
18/// Builder for constructing a [`MockServerClient`].
19///
20/// # Example
21/// ```no_run
22/// use mockserver_client::ClientBuilder;
23///
24/// let client = ClientBuilder::new("localhost", 1080)
25///     .context_path("/api")
26///     .secure(true)
27///     .build()
28///     .unwrap();
29/// ```
30pub struct ClientBuilder {
31    host: String,
32    port: u16,
33    context_path: String,
34    secure: bool,
35    tls_verify: bool,
36}
37
38impl ClientBuilder {
39    /// Create a new builder targeting the given host and port.
40    pub fn new(host: impl Into<String>, port: u16) -> Self {
41        Self {
42            host: host.into(),
43            port,
44            context_path: String::new(),
45            secure: false,
46            tls_verify: true,
47        }
48    }
49
50    /// Set a context path prefix (e.g., "/mockserver" if deployed behind a reverse proxy).
51    pub fn context_path(mut self, path: impl Into<String>) -> Self {
52        self.context_path = path.into();
53        self
54    }
55
56    /// Use HTTPS instead of HTTP.
57    pub fn secure(mut self, secure: bool) -> Self {
58        self.secure = secure;
59        self
60    }
61
62    /// Whether to verify TLS certificates (default: true).
63    pub fn tls_verify(mut self, verify: bool) -> Self {
64        self.tls_verify = verify;
65        self
66    }
67
68    /// Build the client.
69    pub fn build(self) -> Result<MockServerClient> {
70        let scheme = if self.secure { "https" } else { "http" };
71        let ctx = if self.context_path.is_empty() {
72            String::new()
73        } else if self.context_path.starts_with('/') {
74            self.context_path
75        } else {
76            format!("/{}", self.context_path)
77        };
78        let base_url = format!("{scheme}://{}:{}{ctx}", self.host, self.port);
79
80        let http_client = Client::builder()
81            .danger_accept_invalid_certs(!self.tls_verify)
82            .build()?;
83
84        Ok(MockServerClient {
85            base_url,
86            http: http_client,
87            breakpoint_ws: std::sync::Mutex::new(None),
88        })
89    }
90}
91
92// ---------------------------------------------------------------------------
93// MockServerClient
94// ---------------------------------------------------------------------------
95
96/// A blocking client for the MockServer control-plane REST API.
97///
98/// Created via [`ClientBuilder`]. All methods are synchronous.
99pub struct MockServerClient {
100    pub(crate) base_url: String,
101    http: Client,
102    breakpoint_ws: std::sync::Mutex<Option<BreakpointWebSocketClient>>,
103}
104
105impl MockServerClient {
106    // ------------------------------------------------------------------
107    // Expectation creation
108    // ------------------------------------------------------------------
109
110    /// Create one or more expectations on the server.
111    ///
112    /// Returns the created expectations as echoed by the server.
113    pub fn upsert(&self, expectations: &[Expectation]) -> Result<Vec<Expectation>> {
114        let body = serde_json::to_value(expectations)?;
115        let resp = self
116            .http
117            .put(self.url("/mockserver/expectation"))
118            .json(&body)
119            .send()?;
120
121        let status = resp.status().as_u16();
122        match status {
123            200 | 201 => {
124                let text = resp.text()?;
125                if text.is_empty() {
126                    Ok(expectations.to_vec())
127                } else {
128                    Ok(serde_json::from_str(&text)?)
129                }
130            }
131            400 => Err(Error::InvalidRequest(resp.text()?)),
132            _ => Err(Error::UnexpectedStatus {
133                status,
134                body: resp.text().unwrap_or_default(),
135            }),
136        }
137    }
138
139    // ------------------------------------------------------------------
140    // Fluent API entry point
141    // ------------------------------------------------------------------
142
143    /// Begin building an expectation with the fluent `when(...).respond(...)` API.
144    ///
145    /// # Example
146    /// ```no_run
147    /// use mockserver_client::{ClientBuilder, HttpRequest, HttpResponse};
148    ///
149    /// let client = ClientBuilder::new("localhost", 1080).build().unwrap();
150    /// client.when(HttpRequest::new().method("GET").path("/foo"))
151    ///     .respond(HttpResponse::new().status_code(200).body("bar"))
152    ///     .unwrap();
153    /// ```
154    pub fn when(&self, request: HttpRequest) -> ForwardChainExpectation<'_> {
155        ForwardChainExpectation {
156            client: self,
157            request,
158            times: None,
159            time_to_live: None,
160            priority: None,
161            id: None,
162        }
163    }
164
165    // ------------------------------------------------------------------
166    // Verify
167    // ------------------------------------------------------------------
168
169    /// Verify that a request was received the specified number of times.
170    ///
171    /// Returns `Ok(())` if verification passes, or
172    /// `Err(Error::VerificationFailure)` with the server's failure message.
173    pub fn verify(&self, request: HttpRequest, times: VerificationTimes) -> Result<()> {
174        let verification = Verification {
175            http_request: Some(request),
176            http_response: None,
177            times: Some(times),
178            maximum_number_of_request_to_return_in_verification_failure: None,
179        };
180        self.do_verify(&verification)
181    }
182
183    /// Verify that a request/response pair was received the specified number of times.
184    ///
185    /// Both the request matcher and the response matcher must match for a
186    /// recorded exchange to count. The response matcher uses the same
187    /// [`HttpResponse`] type as expectations — the server matches against the
188    /// recorded response's status code, headers, and body.
189    pub fn verify_request_and_response(
190        &self,
191        request: HttpRequest,
192        response: HttpResponse,
193        times: VerificationTimes,
194    ) -> Result<()> {
195        let verification = Verification {
196            http_request: Some(request),
197            http_response: Some(response),
198            times: Some(times),
199            maximum_number_of_request_to_return_in_verification_failure: None,
200        };
201        self.do_verify(&verification)
202    }
203
204    /// Verify that a response (regardless of request) was returned the
205    /// specified number of times.
206    ///
207    /// The `httpRequest` field is omitted from the JSON so the server matches
208    /// any request.
209    pub fn verify_response(
210        &self,
211        response: HttpResponse,
212        times: VerificationTimes,
213    ) -> Result<()> {
214        let verification = Verification {
215            http_request: None,
216            http_response: Some(response),
217            times: Some(times),
218            maximum_number_of_request_to_return_in_verification_failure: None,
219        };
220        self.do_verify(&verification)
221    }
222
223    /// Send a fully constructed [`Verification`] to the server.
224    ///
225    /// This is the most flexible form — callers can set every field,
226    /// including `maximum_number_of_request_to_return_in_verification_failure`.
227    pub fn verify_raw(&self, verification: &Verification) -> Result<()> {
228        self.do_verify(verification)
229    }
230
231    /// Verify that requests were received in the given order.
232    pub fn verify_sequence(&self, requests: Vec<HttpRequest>) -> Result<()> {
233        let verification = VerificationSequence {
234            http_requests: Some(requests),
235            http_responses: None,
236        };
237        self.do_verify_sequence(&verification)
238    }
239
240    /// Verify that request/response pairs were received in the given order.
241    ///
242    /// `responses` is index-aligned with `requests` — each entry constrains
243    /// the response that must have been returned for the corresponding request.
244    pub fn verify_sequence_with_responses(
245        &self,
246        requests: Vec<HttpRequest>,
247        responses: Vec<HttpResponse>,
248    ) -> Result<()> {
249        let verification = VerificationSequence {
250            http_requests: Some(requests),
251            http_responses: Some(responses),
252        };
253        self.do_verify_sequence(&verification)
254    }
255
256    /// Send a fully constructed [`VerificationSequence`] to the server.
257    pub fn verify_sequence_raw(&self, verification: &VerificationSequence) -> Result<()> {
258        self.do_verify_sequence(verification)
259    }
260
261    // ------------------------------------------------------------------
262    // Clear / Reset
263    // ------------------------------------------------------------------
264
265    /// Clear expectations and/or logs matching the given request.
266    ///
267    /// If `request` is `None`, clears everything of the specified type.
268    pub fn clear(
269        &self,
270        request: Option<&HttpRequest>,
271        clear_type: Option<ClearType>,
272    ) -> Result<()> {
273        let mut url = self.url("/mockserver/clear");
274        if let Some(ct) = clear_type {
275            url = format!("{url}?type={}", ct.as_str());
276        }
277
278        let mut builder = self.http.put(&url);
279        builder = builder.header("Content-Type", "application/json");
280        if let Some(req) = request {
281            builder = builder.json(req);
282        } else {
283            builder = builder.body("");
284        }
285
286        let resp = builder.send()?;
287        let status = resp.status().as_u16();
288        match status {
289            200 => Ok(()),
290            400 => Err(Error::InvalidRequest(resp.text()?)),
291            _ => Err(Error::UnexpectedStatus {
292                status,
293                body: resp.text().unwrap_or_default(),
294            }),
295        }
296    }
297
298    /// Clear expectations by expectation ID.
299    pub fn clear_by_id(
300        &self,
301        expectation_id: impl Into<String>,
302        clear_type: Option<ClearType>,
303    ) -> Result<()> {
304        let mut url = self.url("/mockserver/clear");
305        if let Some(ct) = clear_type {
306            url = format!("{url}?type={}", ct.as_str());
307        }
308
309        let body = serde_json::json!({ "id": expectation_id.into() });
310        let resp = self.http.put(&url).json(&body).send()?;
311
312        let status = resp.status().as_u16();
313        match status {
314            200 => Ok(()),
315            400 => Err(Error::InvalidRequest(resp.text()?)),
316            _ => Err(Error::UnexpectedStatus {
317                status,
318                body: resp.text().unwrap_or_default(),
319            }),
320        }
321    }
322
323    /// Reset all expectations and recorded requests.
324    pub fn reset(&self) -> Result<()> {
325        let resp = self
326            .http
327            .put(self.url("/mockserver/reset"))
328            .header("Content-Type", "application/json")
329            .body("")
330            .send()?;
331
332        let status = resp.status().as_u16();
333        match status {
334            200 => Ok(()),
335            _ => Err(Error::UnexpectedStatus {
336                status,
337                body: resp.text().unwrap_or_default(),
338            }),
339        }
340    }
341
342    // ------------------------------------------------------------------
343    // Retrieve
344    // ------------------------------------------------------------------
345
346    /// Retrieve recorded requests matching the optional filter.
347    pub fn retrieve_recorded_requests(
348        &self,
349        request: Option<&HttpRequest>,
350    ) -> Result<Vec<HttpRequest>> {
351        let text =
352            self.do_retrieve(request, RetrieveType::Requests, RetrieveFormat::Json)?;
353        if text.is_empty() {
354            return Ok(vec![]);
355        }
356        Ok(serde_json::from_str(&text)?)
357    }
358
359    /// Retrieve active expectations matching the optional filter.
360    pub fn retrieve_active_expectations(
361        &self,
362        request: Option<&HttpRequest>,
363    ) -> Result<Vec<Expectation>> {
364        let text = self.do_retrieve(
365            request,
366            RetrieveType::ActiveExpectations,
367            RetrieveFormat::Json,
368        )?;
369        if text.is_empty() {
370            return Ok(vec![]);
371        }
372        Ok(serde_json::from_str(&text)?)
373    }
374
375    /// Retrieve recorded expectations matching the optional filter.
376    pub fn retrieve_recorded_expectations(
377        &self,
378        request: Option<&HttpRequest>,
379    ) -> Result<Vec<Expectation>> {
380        let text = self.do_retrieve(
381            request,
382            RetrieveType::RecordedExpectations,
383            RetrieveFormat::Json,
384        )?;
385        if text.is_empty() {
386            return Ok(vec![]);
387        }
388        Ok(serde_json::from_str(&text)?)
389    }
390
391    /// Retrieve log messages matching the optional filter.
392    pub fn retrieve_log_messages(
393        &self,
394        request: Option<&HttpRequest>,
395    ) -> Result<Vec<String>> {
396        let text =
397            self.do_retrieve(request, RetrieveType::Logs, RetrieveFormat::LogEntries)?;
398        if text.is_empty() {
399            return Ok(vec![]);
400        }
401        // Log messages may be returned as a JSON array of strings or as a
402        // separator-delimited block. Try JSON first.
403        if let Ok(arr) = serde_json::from_str::<Vec<String>>(&text) {
404            return Ok(arr);
405        }
406        // Fall back to splitting on the separator used by MockServer.
407        Ok(text
408            .split("------------------------------------\n")
409            .map(|s| s.to_string())
410            .filter(|s| !s.is_empty())
411            .collect())
412    }
413
414    /// Retrieve recorded request/response pairs.
415    pub fn retrieve_request_responses(
416        &self,
417        request: Option<&HttpRequest>,
418    ) -> Result<Vec<Value>> {
419        let text = self.do_retrieve(
420            request,
421            RetrieveType::RequestResponses,
422            RetrieveFormat::Json,
423        )?;
424        if text.is_empty() {
425            return Ok(vec![]);
426        }
427        Ok(serde_json::from_str(&text)?)
428    }
429
430    // ------------------------------------------------------------------
431    // Status / Bind
432    // ------------------------------------------------------------------
433
434    /// Query the server's listening ports.
435    pub fn status(&self) -> Result<Ports> {
436        let resp = self
437            .http
438            .put(self.url("/mockserver/status"))
439            .header("Content-Type", "application/json")
440            .body("")
441            .send()?;
442
443        let status = resp.status().as_u16();
444        match status {
445            200 => {
446                let text = resp.text()?;
447                Ok(serde_json::from_str(&text)?)
448            }
449            _ => Err(Error::UnexpectedStatus {
450                status,
451                body: resp.text().unwrap_or_default(),
452            }),
453        }
454    }
455
456    /// Bind additional listening ports.
457    pub fn bind(&self, ports: &[u16]) -> Result<Ports> {
458        let body = Ports {
459            ports: ports.to_vec(),
460        };
461        let resp = self
462            .http
463            .put(self.url("/mockserver/bind"))
464            .json(&body)
465            .send()?;
466
467        let status = resp.status().as_u16();
468        match status {
469            200 => {
470                let text = resp.text()?;
471                Ok(serde_json::from_str(&text)?)
472            }
473            400 => Err(Error::InvalidRequest(resp.text()?)),
474            406 => Err(Error::VerificationFailure(resp.text()?)),
475            _ => Err(Error::UnexpectedStatus {
476                status,
477                body: resp.text().unwrap_or_default(),
478            }),
479        }
480    }
481
482    /// Check if the MockServer has started (polls with retries).
483    pub fn has_started(&self, attempts: u32, timeout_ms: u64) -> bool {
484        for i in 0..attempts {
485            match self.status() {
486                Ok(_) => return true,
487                Err(_) => {
488                    if i < attempts - 1 {
489                        std::thread::sleep(std::time::Duration::from_millis(timeout_ms));
490                    }
491                }
492            }
493        }
494        false
495    }
496
497    // ------------------------------------------------------------------
498    // Breakpoints
499    // ------------------------------------------------------------------
500
501    /// Ensure the breakpoint WebSocket client is connected and return the clientId.
502    /// If the existing connection's read loop has exited, it is replaced transparently.
503    fn ensure_breakpoint_ws(&self) -> Result<String> {
504        let mut guard = self.breakpoint_ws.lock().unwrap();
505        let needs_connect = match guard.as_ref() {
506            None => true,
507            Some(ws) => ws.is_dead(),
508        };
509        if needs_connect {
510            // Close the old dead connection if present
511            if let Some(old) = guard.take() {
512                old.close();
513            }
514            let ws = BreakpointWebSocketClient::connect(&self.base_url)?;
515            *guard = Some(ws);
516        }
517        Ok(guard.as_ref().unwrap().client_id.clone())
518    }
519
520    /// Register a breakpoint matcher with the given phases and handlers.
521    /// Returns the server-assigned breakpoint id.
522    pub fn add_breakpoint(
523        &self,
524        matcher: HttpRequest,
525        phases: &[&str],
526        request_handler: Option<BreakpointRequestHandler>,
527        response_handler: Option<BreakpointResponseHandler>,
528        stream_frame_handler: Option<BreakpointStreamFrameHandler>,
529    ) -> Result<String> {
530        if phases.is_empty() {
531            return Err(Error::InvalidRequest(
532                "At least one phase is required".into(),
533            ));
534        }
535
536        let client_id = self.ensure_breakpoint_ws()?;
537
538        let reg = BreakpointMatcherRegistration {
539            http_request: matcher,
540            phases: phases.iter().map(|s| s.to_string()).collect(),
541            client_id: Some(client_id),
542        };
543
544        let resp = self
545            .http
546            .put(self.url("/mockserver/breakpoint/matcher"))
547            .json(&reg)
548            .send()?;
549
550        let status = resp.status().as_u16();
551        let text = resp.text()?;
552        if status >= 400 {
553            return Err(Error::UnexpectedStatus {
554                status,
555                body: text,
556            });
557        }
558
559        let result: BreakpointMatcherResponse = serde_json::from_str(&text)?;
560        let id = result.id.clone();
561
562        // Register handlers
563        let guard = self.breakpoint_ws.lock().unwrap();
564        if let Some(ws) = guard.as_ref() {
565            if let Some(h) = request_handler {
566                ws.set_request_handler(&id, h);
567            }
568            if let Some(h) = response_handler {
569                ws.set_response_handler(&id, h);
570            }
571            if let Some(h) = stream_frame_handler {
572                ws.set_stream_frame_handler(&id, h);
573            }
574        }
575
576        Ok(id)
577    }
578
579    /// Convenience: register a REQUEST-only breakpoint.
580    pub fn add_request_breakpoint(
581        &self,
582        matcher: HttpRequest,
583        handler: BreakpointRequestHandler,
584    ) -> Result<String> {
585        self.add_breakpoint(
586            matcher,
587            &[crate::breakpoint::phase::REQUEST],
588            Some(handler),
589            None,
590            None,
591        )
592    }
593
594    /// Convenience: register a REQUEST + RESPONSE breakpoint.
595    pub fn add_request_response_breakpoint(
596        &self,
597        matcher: HttpRequest,
598        request_handler: BreakpointRequestHandler,
599        response_handler: BreakpointResponseHandler,
600    ) -> Result<String> {
601        self.add_breakpoint(
602            matcher,
603            &[
604                crate::breakpoint::phase::REQUEST,
605                crate::breakpoint::phase::RESPONSE,
606            ],
607            Some(request_handler),
608            Some(response_handler),
609            None,
610        )
611    }
612
613    /// Convenience: register a streaming-phase breakpoint.
614    pub fn add_stream_breakpoint(
615        &self,
616        matcher: HttpRequest,
617        phases: &[&str],
618        handler: BreakpointStreamFrameHandler,
619    ) -> Result<String> {
620        self.add_breakpoint(matcher, phases, None, None, Some(handler))
621    }
622
623    /// List all registered breakpoint matchers.
624    pub fn list_breakpoint_matchers(&self) -> Result<BreakpointMatcherList> {
625        let resp = self
626            .http
627            .get(self.url("/mockserver/breakpoint/matchers"))
628            .send()?;
629
630        let status = resp.status().as_u16();
631        let text = resp.text()?;
632        if status >= 400 {
633            return Err(Error::UnexpectedStatus {
634                status,
635                body: text,
636            });
637        }
638
639        Ok(serde_json::from_str(&text)?)
640    }
641
642    /// Remove a breakpoint matcher by id.
643    pub fn remove_breakpoint_matcher(&self, id: impl Into<String>) -> Result<()> {
644        let id = id.into();
645        let body = serde_json::json!({ "id": &id });
646        let resp = self
647            .http
648            .put(self.url("/mockserver/breakpoint/matcher/remove"))
649            .json(&body)
650            .send()?;
651
652        let status = resp.status().as_u16();
653        match status {
654            200 => {
655                let guard = self.breakpoint_ws.lock().unwrap();
656                if let Some(ws) = guard.as_ref() {
657                    ws.remove_handlers(&id);
658                }
659                Ok(())
660            }
661            404 => Err(Error::InvalidRequest(format!(
662                "Breakpoint matcher not found: {id}"
663            ))),
664            _ => Err(Error::UnexpectedStatus {
665                status,
666                body: resp.text().unwrap_or_default(),
667            }),
668        }
669    }
670
671    /// Remove all registered breakpoint matchers.
672    pub fn clear_breakpoint_matchers(&self) -> Result<()> {
673        let resp = self
674            .http
675            .put(self.url("/mockserver/breakpoint/matcher/clear"))
676            .header("Content-Type", "application/json")
677            .body("")
678            .send()?;
679
680        let status = resp.status().as_u16();
681        if status >= 400 {
682            return Err(Error::UnexpectedStatus {
683                status,
684                body: resp.text().unwrap_or_default(),
685            });
686        }
687
688        let guard = self.breakpoint_ws.lock().unwrap();
689        if let Some(ws) = guard.as_ref() {
690            ws.clear_handlers();
691        }
692        Ok(())
693    }
694
695    /// Close the breakpoint callback WebSocket connection.
696    pub fn close_breakpoint_websocket(&self) {
697        let mut guard = self.breakpoint_ws.lock().unwrap();
698        if let Some(ws) = guard.take() {
699            ws.close();
700        }
701    }
702
703    // ------------------------------------------------------------------
704    // Internal helpers
705    // ------------------------------------------------------------------
706
707    fn do_verify(&self, verification: &Verification) -> Result<()> {
708        let resp = self
709            .http
710            .put(self.url("/mockserver/verify"))
711            .json(verification)
712            .send()?;
713
714        let status = resp.status().as_u16();
715        match status {
716            200 | 202 => Ok(()),
717            406 => Err(Error::VerificationFailure(resp.text()?)),
718            400 => Err(Error::InvalidRequest(resp.text()?)),
719            _ => Err(Error::UnexpectedStatus {
720                status,
721                body: resp.text().unwrap_or_default(),
722            }),
723        }
724    }
725
726    fn do_verify_sequence(&self, verification: &VerificationSequence) -> Result<()> {
727        let resp = self
728            .http
729            .put(self.url("/mockserver/verifySequence"))
730            .json(verification)
731            .send()?;
732
733        let status = resp.status().as_u16();
734        match status {
735            200 | 202 => Ok(()),
736            406 => Err(Error::VerificationFailure(resp.text()?)),
737            400 => Err(Error::InvalidRequest(resp.text()?)),
738            _ => Err(Error::UnexpectedStatus {
739                status,
740                body: resp.text().unwrap_or_default(),
741            }),
742        }
743    }
744
745    fn url(&self, path: &str) -> String {
746        format!("{}{path}", self.base_url)
747    }
748
749    fn do_retrieve(
750        &self,
751        request: Option<&HttpRequest>,
752        retrieve_type: RetrieveType,
753        format: RetrieveFormat,
754    ) -> Result<String> {
755        let url = format!(
756            "{}?type={}&format={}",
757            self.url("/mockserver/retrieve"),
758            retrieve_type.as_str(),
759            format.as_str(),
760        );
761
762        let mut builder = self.http.put(&url);
763        builder = builder.header("Content-Type", "application/json");
764        if let Some(req) = request {
765            builder = builder.json(req);
766        } else {
767            builder = builder.body("");
768        }
769
770        let resp = builder.send()?;
771        let status = resp.status().as_u16();
772        match status {
773            200 => Ok(resp.text()?),
774            400 => Err(Error::InvalidRequest(resp.text()?)),
775            _ => Err(Error::UnexpectedStatus {
776                status,
777                body: resp.text().unwrap_or_default(),
778            }),
779        }
780    }
781}
782
783// ---------------------------------------------------------------------------
784// ForwardChainExpectation (fluent builder)
785// ---------------------------------------------------------------------------
786
787/// Fluent builder for creating an expectation via `client.when(...).respond(...)`.
788pub struct ForwardChainExpectation<'a> {
789    client: &'a MockServerClient,
790    request: HttpRequest,
791    times: Option<Times>,
792    time_to_live: Option<TimeToLive>,
793    priority: Option<i32>,
794    id: Option<String>,
795}
796
797impl<'a> ForwardChainExpectation<'a> {
798    /// Set how many times this expectation should match.
799    pub fn times(mut self, times: Times) -> Self {
800        self.times = Some(times);
801        self
802    }
803
804    /// Set the time-to-live for this expectation.
805    pub fn time_to_live(mut self, ttl: TimeToLive) -> Self {
806        self.time_to_live = Some(ttl);
807        self
808    }
809
810    /// Set the priority for this expectation.
811    pub fn priority(mut self, priority: i32) -> Self {
812        self.priority = Some(priority);
813        self
814    }
815
816    /// Set the expectation ID (for upsert semantics).
817    pub fn with_id(mut self, id: impl Into<String>) -> Self {
818        self.id = Some(id.into());
819        self
820    }
821
822    /// Complete the expectation with a response action.
823    pub fn respond(self, response: HttpResponse) -> Result<Vec<Expectation>> {
824        let (client, expectation) = self.into_parts();
825        let expectation = expectation.respond(response);
826        client.upsert(&[expectation])
827    }
828
829    /// Complete the expectation with a forward action.
830    pub fn forward(self, forward: HttpForward) -> Result<Vec<Expectation>> {
831        let (client, expectation) = self.into_parts();
832        let expectation = expectation.forward(forward);
833        client.upsert(&[expectation])
834    }
835
836    /// Complete the expectation with an error action.
837    pub fn error(self, error: HttpError) -> Result<Vec<Expectation>> {
838        let (client, expectation) = self.into_parts();
839        let expectation = expectation.error(error);
840        client.upsert(&[expectation])
841    }
842
843    fn into_parts(self) -> (&'a MockServerClient, Expectation) {
844        let ForwardChainExpectation {
845            client,
846            request,
847            times,
848            time_to_live,
849            priority,
850            id,
851        } = self;
852        let mut exp = Expectation::new(request);
853        exp.times = times;
854        exp.time_to_live = time_to_live;
855        exp.priority = priority;
856        exp.id = id;
857        (client, exp)
858    }
859}