Skip to main content

xdiff_live/config/
mod.rs

1//! Configuration management for XDiff-NG tools.
2//!
3//! This module provides configuration structures and utilities for loading
4//! and validating configuration files for both `xdiff` and `xreq` tools.
5//! It handles YAML configuration parsing, HTTP request building, and response processing.
6
7mod xdiff;
8mod xreq;
9
10use anyhow::{Ok, Result};
11use async_trait::async_trait;
12use reqwest::{
13    header::{self, HeaderMap, HeaderName, HeaderValue},
14    Client, Method, Response,
15};
16use serde::{de::DeserializeOwned, Deserialize, Serialize};
17use serde_json::json;
18use std::fmt::Write;
19use std::str::FromStr;
20use tokio::fs;
21use url::Url;
22
23pub use crate::{ExtraArgs, ResponseProfile};
24pub use xdiff::*;
25pub use xreq::*;
26
27/// Checks if a value is equal to its default.
28///
29/// This utility function is used in serde serialization to determine
30/// whether to skip serializing fields that have default values.
31///
32/// # Arguments
33///
34/// * `v` - The value to check against its default
35///
36/// # Returns
37///
38/// `true` if the value equals its default, `false` otherwise.
39pub fn is_default<T: Default + PartialEq>(v: &T) -> bool {
40    v == &T::default()
41}
42
43/// Trait for loading configuration from YAML files.
44///
45/// This trait provides functionality to load and parse configuration
46/// from YAML files or strings, with built-in validation.
47///
48/// # Type Requirements
49///
50/// Types implementing this trait must also implement:
51/// - `ValidateConfig` for validation logic
52/// - `DeserializeOwned` for YAML deserialization
53#[async_trait]
54pub trait LoadConfig
55where
56    Self: ValidateConfig + DeserializeOwned,
57{
58    /// Loads configuration from a YAML file.
59    ///
60    /// This method reads the specified file and parses it as YAML,
61    /// then validates the resulting configuration.
62    ///
63    /// # Arguments
64    ///
65    /// * `path` - Path to the YAML configuration file
66    ///
67    /// # Returns
68    ///
69    /// A `Result` containing the parsed and validated configuration,
70    /// or an error if loading or validation fails.
71    ///
72    /// # Examples
73    ///
74    /// ```no_run
75    /// use xdiff_live::config::{DiffConfig, LoadConfig};
76    ///
77    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
78    /// let config = DiffConfig::load_yaml("config.yml").await?;
79    /// # Ok(())
80    /// # }
81    /// ```
82    async fn load_yaml(path: &str) -> Result<Self> {
83        let content = fs::read_to_string(path).await?;
84        Self::from_yaml(&content)
85    }
86
87    /// Loads configuration from a YAML string.
88    ///
89    /// This method parses the provided YAML string and validates
90    /// the resulting configuration.
91    ///
92    /// # Arguments
93    ///
94    /// * `content` - YAML content as a string
95    ///
96    /// # Returns
97    ///
98    /// A `Result` containing the parsed and validated configuration,
99    /// or an error if parsing or validation fails.
100    ///
101    /// # Examples
102    ///
103    /// ```
104    /// use xdiff_live::config::{LoadConfig, DiffConfig};
105    ///
106    /// let yaml = r#"
107    /// profile1:
108    ///   req1:
109    ///     url: https://example.com
110    ///   req2:
111    ///     url: https://example.org
112    /// "#;
113    ///
114    /// let config = DiffConfig::from_yaml(yaml).unwrap();
115    /// ```
116    fn from_yaml(content: &str) -> Result<Self> {
117        let config: Self = serde_yaml::from_str(content)?;
118        config.validate()?;
119        Ok(config)
120    }
121}
122
123/// Trait for validating configuration structures.
124///
125/// This trait ensures that loaded configurations are valid and
126/// contain all required fields with appropriate values.
127pub trait ValidateConfig {
128    /// Validates the configuration.
129    ///
130    /// # Returns
131    ///
132    /// `Ok(())` if the configuration is valid, or an error describing
133    /// what validation failed.
134    fn validate(&self) -> Result<()>;
135}
136
137/// Configuration for a single HTTP request.
138///
139/// This structure defines all the parameters needed to make an HTTP request,
140/// including method, URL, headers, query parameters, and body content.
141/// It serves as the base configuration for both individual requests and
142/// request comparisons.
143///
144/// # Examples
145///
146/// ```
147/// use xdiff_live::config::RequestProfile;
148/// use reqwest::Method;
149/// use url::Url;
150///
151/// let profile = RequestProfile {
152///     method: Method::GET,
153///     url: Url::parse("https://api.example.com/users").unwrap(),
154///     params: None,
155///     headers: Default::default(),
156///     body: None,
157/// };
158/// ```
159#[derive(Debug, Deserialize, Serialize, Clone)]
160pub struct RequestProfile {
161    /// HTTP method (GET, POST, PUT, DELETE, etc.)
162    #[serde(with = "http_serde::method", default)]
163    pub method: Method,
164    /// Target URL for the request
165    pub url: Url,
166    /// Query parameters as JSON value
167    // skip_serializing_if
168    // 调用函数来确定是否跳过序列化该字段。
169    // 给定的函数必须可调用为 fn(&T) -> bool,尽管它可能是T上的通用函数。
170    // 例如,skip_serializing_if = "Option::is_none"将跳过为None的选项。
171    #[serde(skip_serializing_if = "empty_json_value", default)]
172    // #[serde(default)]: If the value is not present when deserializing, use the Default::default().
173    pub params: Option<serde_json::Value>,
174    /// HTTP headers for the request
175    #[serde(
176        skip_serializing_if = "HeaderMap::is_empty",
177        with = "http_serde::header_map",
178        default
179    )]
180    pub headers: HeaderMap,
181    /// Request body content as JSON value
182    #[serde(skip_serializing_if = "Option::is_none", default)]
183    pub body: Option<serde_json::Value>,
184}
185
186/// Checks if a JSON value is empty or null.
187///
188/// This function is used by serde to determine whether to skip
189/// serializing optional JSON values that are empty.
190///
191/// # Arguments
192///
193/// * `v` - Optional JSON value to check
194///
195/// # Returns
196///
197/// `true` if the value is None, null, or an empty object; `false` otherwise.
198fn empty_json_value(v: &Option<serde_json::Value>) -> bool {
199    v.as_ref().map_or(true, |v| {
200        v.is_null() || (v.is_object() && v.as_object().unwrap().is_empty())
201    })
202}
203
204/// Extended response wrapper with additional processing capabilities.
205///
206/// This structure wraps a `reqwest::Response` and provides additional
207/// methods for extracting and formatting response data according to
208/// filtering rules defined in response profiles.
209#[derive(Debug)]
210pub struct ResponseExt(Response);
211
212impl ResponseExt {
213    /// Extracts the inner Response object.
214    ///
215    /// # Returns
216    ///
217    /// The wrapped `reqwest::Response` object.
218    pub fn into_inner(self) -> Response {
219        self.0
220    }
221
222    /// Extracts formatted text from the response according to profile rules.
223    ///
224    /// This method processes the HTTP response and formats it as text,
225    /// applying any skip rules defined in the response profile for headers
226    /// and body content.
227    ///
228    /// # Arguments
229    ///
230    /// * `profile` - Response profile containing skip rules for headers and body
231    ///
232    /// # Returns
233    ///
234    /// A `Result<String>` containing the formatted response text, including
235    /// status line, filtered headers, and filtered body content.
236    ///
237    /// # Examples
238    ///
239    /// ```no_run
240    /// use xdiff_live::config::{ResponseExt, ResponseProfile};
241    ///
242    /// # async fn example(response_ext: ResponseExt) -> Result<(), Box<dyn std::error::Error>> {
243    /// let profile = ResponseProfile {
244    ///     skip_headers: vec!["date".to_string(), "server".to_string()],
245    ///     skip_body: vec!["timestamp".to_string()],
246    /// };
247    ///
248    /// let formatted_text = response_ext.get_text(&profile).await?;
249    /// println!("{}", formatted_text);
250    /// # Ok(())
251    /// # }
252    /// ```
253    pub async fn get_text(self, profile: &ResponseProfile) -> Result<String> {
254        let res = self.0;
255        let mut output = get_status_text(&res)?;
256
257        write!(
258            &mut output,
259            "{}",
260            get_headers_text(&res, &profile.skip_headers)?
261        )?;
262
263        // let mut output = get_headers_text(&res, &profile.skip_headers)?;
264        // let content_type = get_content_type(res.headers());
265        // let text = res.text().await?;
266
267        // match content_type.as_deref() {
268        //     Some("application/json") => {
269        //         let text = filter_json(&text, &profile.skip_body)?;
270        //         output.push_str(&text);
271        //     }
272        //     _ => {
273        //         output.push_str(&text);
274        //     }
275        // }
276
277        writeln!(
278            &mut output,
279            "{}",
280            get_body_text(res, &profile.skip_body).await?
281        )?;
282
283        Ok(output)
284    }
285
286    /// Extracts all header keys from the response.
287    ///
288    /// This method returns a list of all header names present in the response,
289    /// which can be useful for debugging or dynamic header processing.
290    ///
291    /// # Returns
292    ///
293    /// A vector of header names as strings.
294    ///
295    /// # Examples
296    ///
297    /// ```no_run
298    /// use xdiff_live::config::ResponseExt;
299    ///
300    /// # fn example(response_ext: &ResponseExt) {
301    /// let header_keys = response_ext.get_header_keys();
302    /// println!("Response headers: {:?}", header_keys);
303    /// # }
304    /// ```
305    pub fn get_header_keys(&self) -> Vec<String> {
306        let res = &self.0;
307        let headers = res.headers();
308        headers
309            .iter()
310            .map(|(k, _)| k.as_str().to_string())
311            .collect()
312    }
313}
314
315/// Extracts and formats the body text from an HTTP response.
316///
317/// This function processes the response body according to its content type
318/// and applies filtering rules for JSON content. For JSON responses, it
319/// filters out specified fields; for other content types, it returns the
320/// raw text.
321///
322/// # Arguments
323///
324/// * `res` - The HTTP response to process
325/// * `skip_body` - A slice of field names to skip when filtering JSON content
326///
327/// # Returns
328///
329/// A `Result<String>` containing the processed body text, or an error if
330/// processing fails.
331///
332/// # Examples
333///
334/// ```no_run
335/// use xdiff_live::config::get_body_text;
336///
337/// # async fn example(response: reqwest::Response) -> Result<(), Box<dyn std::error::Error>> {
338/// let skip_fields = vec!["timestamp".to_string(), "request_id".to_string()];
339/// let body_text = get_body_text(response, &skip_fields).await?;
340/// println!("{}", body_text);
341/// # Ok(())
342/// # }
343/// ```
344pub async fn get_body_text(res: Response, skip_body: &[String]) -> Result<String> {
345    let content_type = get_content_type(res.headers());
346    let text = res.text().await?;
347
348    // match content_type.as_deref() {
349    //     Some("application/json") => {
350    //         let text = filter_json(&text, &profile.skip_body)?;
351    //         writeln!(&mut output, "{}", text)?;
352    //     }
353    //     _ => {
354    //         writeln!(&mut output, "{}", text)?;
355    //     }
356    // }
357
358    match content_type.as_deref() {
359        Some("application/json") => filter_json(&text, skip_body),
360        _ => Ok(text),
361    }
362}
363
364/// Formats the HTTP status line from a response.
365///
366/// This function extracts the HTTP version and status code from a response
367/// and formats them into a human-readable status line.
368///
369/// # Arguments
370///
371/// * `res` - The HTTP response to extract status from
372///
373/// # Returns
374///
375/// A `Result<String>` containing the formatted status line (e.g., "HTTP/1.1 200 OK\n").
376///
377/// # Examples
378///
379/// ```no_run
380/// use xdiff_live::config::get_status_text;
381///
382/// # fn example(response: &reqwest::Response) -> Result<(), Box<dyn std::error::Error>> {
383/// let status_line = get_status_text(response)?;
384/// println!("{}", status_line);
385/// # Ok(())
386/// # }
387/// ```
388pub fn get_status_text(res: &Response) -> Result<String> {
389    Ok(format!("{:?} {}\n", res.version(), res.status()))
390}
391
392/// Formats HTTP headers from a response, excluding specified headers.
393///
394/// This function extracts headers from an HTTP response and formats them
395/// as text, optionally skipping headers specified in the skip list.
396///
397/// # Arguments
398///
399/// * `res` - The HTTP response to extract headers from
400/// * `skip_headers` - A slice of header names to exclude from the output
401///
402/// # Returns
403///
404/// A `Result<String>` containing the formatted headers text, or an error if
405/// formatting fails.
406///
407/// # Examples
408///
409/// ```no_run
410/// use xdiff_live::config::get_headers_text;
411///
412/// # fn example(response: &reqwest::Response) -> Result<(), Box<dyn std::error::Error>> {
413/// let skip_list = vec!["date".to_string(), "server".to_string()];
414/// let headers_text = get_headers_text(response, &skip_list)?;
415/// println!("{}", headers_text);
416/// # Ok(())
417/// # }
418/// ```
419pub fn get_headers_text(res: &Response, skip_headers: &[String]) -> Result<String> {
420    let mut output = String::new();
421    // write!(output, "{:?} {}\r", self.0.version(), self.0.status())?;
422    // output.push_str(&format!("{:?} {}\n", res.version(), res.status()));
423
424    let headers = res.headers();
425    for (k, v) in headers.iter() {
426        if !skip_headers.contains(&k.to_string()) {
427            // if !profile.skip_headers.iter().any(|x| x == k.as_str( ) ) {
428            output.push_str(&format!("{}: {:?}\n", k, v));
429            // write!(&mut output, "{}: {:?}\n", k, v)?;
430        }
431    }
432
433    Ok(output)
434}
435
436fn filter_json(text: &str, skip: &[String]) -> Result<String> {
437    let mut json: serde_json::Value = serde_json::from_str(text)?;
438
439    // match json {
440    //     serde_json::Value::Object(ref mut obj) => {
441    //         for key in skip {
442    //             obj.remove(key);
443    //         }
444    //     }
445    //     _ =>
446    //         // for now we just ignore non_object values, we don't how to filter them
447    //         //  In future, we might support array of primitives
448    //         {}
449    // }
450
451    // for now we just ignore non_object values, we don't how to filter them
452    // In future, we might support array of objects
453    if let serde_json::Value::Object(ref mut obj) = json {
454        for key in skip {
455            obj.remove(key);
456        }
457    }
458
459    Ok(serde_json::to_string_pretty(&json)?)
460}
461
462impl RequestProfile {
463    pub fn new(
464        method: Method,
465        url: Url,
466        params: Option<serde_json::Value>,
467        headers: HeaderMap,
468        body: Option<serde_json::Value>,
469    ) -> Self {
470        Self {
471            method,
472            url,
473            params,
474            headers,
475            body,
476        }
477    }
478
479    pub async fn send(&self, args: &ExtraArgs) -> Result<ResponseExt> {
480        let (headers, query, body) = self.generate(args)?;
481        let client = Client::new();
482        let req = client
483            .request(self.method.clone(), self.url.clone())
484            .query(&query)
485            .headers(headers)
486            .body(body)
487            .build()?;
488
489        let res = client.execute(req).await?;
490
491        Ok(ResponseExt(res))
492    }
493
494    pub fn get_url(&self, args: &ExtraArgs) -> Result<String> {
495        let (_, params, _) = self.generate(args)?;
496        let mut url = self.url.clone();
497        if !params.as_object().unwrap().is_empty() {
498            let query = serde_qs::to_string(&params)?;
499            url.set_query(Some(&query));
500        }
501        // url.set_query(None);
502        // let mut query = serde_qs::to_string(&query)?;
503        // if !query.is_empty() {
504        //     // url.set_query(Some(&query));
505        //     write!(url, "?{}", &query)?;
506        // }
507        Ok(url.to_string())
508    }
509
510    fn generate(&self, args: &ExtraArgs) -> Result<(HeaderMap, serde_json::Value, String)> {
511        let mut headers = self.headers.clone();
512        let mut query = self.params.clone().unwrap_or_else(|| json!({}));
513        let mut body = self.body.clone().unwrap_or_else(|| json!({}));
514
515        for (k, v) in &args.headers {
516            // println!("测试:{}{}", k, v);
517            headers.insert(HeaderName::from_str(k)?, HeaderValue::from_str(v)?);
518        }
519
520        if !headers.contains_key(header::CONTENT_TYPE) {
521            // println!("测试:{}___{:?}", header::CONTENT_TYPE, HeaderValue::from_static("application/json"));
522            headers.insert(
523                header::CONTENT_TYPE,
524                // 用于指示资源的媒体类型。
525                // 在响应中,Content-Type 标头告诉客户端返回内容的实际内容类型。
526                // 在某些情况下,浏览器会进行 MIME 嗅探,但不一定会遵循此标头的值;
527                // 为了防止这种行为,可以将标头 X-Content-Type-Options 设置为 nosniff。
528                // 在请求(例如 POST 或 PUT)中,客户端告诉服务器实际发送的数据类型。
529                HeaderValue::from_static("application/json"),
530            );
531            // "Content-Type" 是 HTTP 请求头部中的一个字段,它用于指定请求或响应中携带的实体数据的媒体类型(即数据的类型和格式)
532        }
533
534        for (k, v) in &args.query {
535            query[k] = v.parse()?;
536            // parse() -> Result<T, <T as FromStr>::Err>
537            // 将此字符串切片解析为另一种类型。
538            // 由于解析非常通用,因此可能会导致类型推断出现问题。
539            // 因此,解析是您会看到被亲切地称为“turbofish”的语法的少数情况之一:::<>。
540            // 这有助于推理算法具体了解您要解析的类型。
541        }
542
543        for (k, v) in &args.body {
544            body[k] = v.parse()?;
545        }
546
547        // println!("测试:{:?}", headers);
548
549        let content_type = get_content_type(&headers);
550
551        // println!("测试:{:?}", content_type);
552        
553        match content_type.as_deref() {
554            // as_deref()是一个Rust标准库中的方法,它用于将Option<&T>转换为Option<&U>,其中T和U是具体的类型。
555            Some("application/json") => {
556                let body = serde_json::to_string(&body)?;
557                Ok((headers, query, body))
558            }
559            Some("application/x-www-form-urlencoded" | "multipart/form-data") => {
560                let body = serde_urlencoded::to_string(&body)?;
561                Ok((headers, query, body))
562            }
563            _ => Err(anyhow::anyhow!("unsupported content-type")),
564        }
565    }
566}
567
568impl ValidateConfig for RequestProfile {
569    fn validate(&self) -> Result<()> {
570        if let Some(params) = self.params.as_ref() {
571            if !params.is_object() {
572                return Err(anyhow::anyhow!(
573                    "Params must be an object but got\n{}",
574                    serde_yaml::to_string(params)?
575                ));
576            }
577        }
578        if let Some(body) = self.body.as_ref() {
579            if !body.is_object() {
580                return Err(anyhow::anyhow!(
581                    "Body must be an object but got\n{}",
582                    serde_yaml::to_string(body)?
583                ));
584            }
585        }
586        Ok(())
587    }
588}
589
590fn get_content_type(headers: &HeaderMap) -> Option<String> {
591    headers
592        .get(header::CONTENT_TYPE)
593        // .map(|v| v.to_str().unwrap().split(';').next())
594        // .flatten()
595        // .map(|v| v.to_string())
596        .and_then(|v| v.to_str().unwrap().split(";").next().map(|v| v.to_string()))
597}
598
599impl FromStr for RequestProfile {
600    type Err = anyhow::Error;
601
602    fn from_str(s: &str) -> Result<Self> {
603        let mut url = Url::parse(s)?;
604        let qs = url.query_pairs();
605        let mut params = json!({});
606        for (k, v) in qs {
607            params[&*k] = v.parse()?;
608        }
609
610        url.set_query(None);
611
612        Ok(RequestProfile::new(
613            Method::GET,
614            url,
615            Some(params),
616            HeaderMap::new(),
617            None,
618        ))
619    }
620}
621
622#[cfg(test)]
623mod tests {
624    use super::*;
625    use mockito::{mock, Mock};
626    use reqwest::StatusCode;
627
628    #[tokio::test]
629    async fn request_profile_send_should_work() {
630        let _m = mock_for_url("/todo?a=1&b=2", json!({"id": 1, "title": "todo"}));
631        let res = get_response("/todo?a=1&b=2", &Default::default())
632            .await
633            .into_inner();
634        assert_eq!(res.status(), StatusCode::OK);
635    }
636
637    #[tokio::test]
638    async fn request_profile_send_with_extra_args_should_work() {
639        let _m = mock_for_url("/todo?a=1&b=3", json!({"id": 1, "title": "todo"}));
640
641        let args = ExtraArgs::new_with_query(vec![("b".into(), "3".into())]);
642
643        let res = get_response("/todo?a=1&b=2", &args).await.into_inner();
644        assert_eq!(res.status(), StatusCode::OK);
645    }
646
647    #[test]
648    fn request_profile_get_url_should_work() {
649        let profile = get_profile("/todo?a=1&b=2");
650        assert_eq!(
651            profile.get_url(&Default::default()).unwrap(),
652            get_url("/todo?a=1&b=2") // format!("{}/todo?a=1&b=2", mockito::server_url())
653        );
654    }
655
656    #[test]
657    fn request_profile_get_url_with_args_should_work() {
658        let profile = get_profile("/todo?a=1&b=2");
659
660        let args = ExtraArgs::new_with_query(vec![("c".into(), "3".into())]);
661
662        assert_eq!(
663            profile.get_url(&args).unwrap(),
664            get_url("/todo?a=1&b=2&c=3") // format!("{}/todo?a=1&b=2&c=3", mockito::server_url())
665        );
666    }
667
668    #[test]
669    fn request_profile_validate_should_work() {
670        let profile = get_profile("/todo?a=1&b=2");
671        assert!(profile.validate().is_ok());
672    }
673
674    #[test]
675    fn request_profile_with_bad_params_validate_should_fail() {
676        let profile = RequestProfile::new(
677            Method::GET,
678            Url::parse("http://localhost:1234/todo").unwrap(),
679            Some(json!([1, 2, 3])),
680            HeaderMap::new(),
681            None,
682        );
683        let result = profile.validate();
684        assert!(profile.validate().is_err());
685        assert_eq!(
686            result.unwrap_err().to_string(),
687            "Params must be an object but got\n- 1\n- 2\n- 3\n"
688        );
689    }
690
691    #[tokio::test]
692    async fn response_ext_get_text_should_work() {
693        let _m = mock_for_url("/todo", json!({"id": 1, "title": "todo"}));
694        let res = get_response("/todo", &Default::default()).await;
695
696        let response_profile = ResponseProfile::new(
697            vec!["connection".into(), "content-length".into()],
698            vec!["title".into()],
699        );
700        assert_eq!(
701            res.get_text(&response_profile).await.unwrap(),
702            "HTTP/1.1 200 OK\ncontent-type: \"application/json\"\n{\n  \"id\": 1\n}\n"
703        );
704    }
705
706    #[tokio::test]
707    async fn response_ext_get_header_should_work() {
708        let _m = mock_for_url("/todo", json!({"id": 1, "title": "todo"}));
709        let res = get_response("/todo", &Default::default()).await;
710        let mut sorted_header_keys = res.get_header_keys();
711        sorted_header_keys.sort();
712        let expected_header_keys = vec!["connection", "content-length", "content-type"];
713        // assert_eq!(
714        //     res.get_header_keys(),
715        //     &["connection", "content-type", "content-length"]
716        // );
717        assert_eq!(sorted_header_keys, expected_header_keys);
718    }
719
720    #[test]
721    fn test_get_content_type() {
722        let mut headers = HeaderMap::new();
723        headers.insert(
724            header::CONTENT_TYPE,
725            HeaderValue::from_static("application/json; charset=utf-8"),
726        );
727        assert_eq!(
728            get_content_type(&headers),
729            Some("application/json".to_string())
730        );
731    }
732
733    #[tokio::test]
734    async fn get_status_text_should_work() {
735        let _m = mock_for_url("/todo", json!({"id": 1, "title": "todo"}));
736        let res = get_response("/todo", &Default::default())
737            .await
738            .into_inner();
739        assert_eq!(get_status_text(&res).unwrap(), "HTTP/1.1 200 OK\n");
740    }
741
742    #[tokio::test]
743    async fn get_headers_text_should_work() {
744        let _m = mock_for_url("/todo", json!({"id": 1, "title": "todo"}));
745        let res = get_response("/todo", &Default::default())
746            .await
747            .into_inner();
748        assert_eq!(
749            get_headers_text(&res, &["connection".into(), "content-length".into()]).unwrap(),
750            "content-type: \"application/json\"\n"
751        );
752    }
753
754    #[tokio::test]
755    async fn get_body_text_should_work() {
756        let _m = mock_for_url("/todo", json!({"id": 1, "title": "todo"}));
757        let res = get_response("/todo", &Default::default())
758            .await
759            .into_inner();
760        assert_eq!(
761            get_body_text(res, &["id".into()]).await.unwrap(),
762            "{\n  \"title\": \"todo\"\n}"
763        );
764    }
765
766    fn mock_for_url(path_and_query: &str, resp_body: serde_json::Value) -> Mock {
767        mock("GET", path_and_query)
768            .with_status(200)
769            .with_header("content-type", "application/json")
770            .with_body(serde_json::to_string(&resp_body).unwrap())
771            .create()
772    }
773
774    fn get_url(path: &str) -> String {
775        format!("{}{}", mockito::server_url(), path)
776    }
777
778    fn get_profile(path_and_query: &str) -> RequestProfile {
779        let url = get_url(path_and_query);
780        RequestProfile::from_str(&url).unwrap()
781    }
782
783    async fn get_response(path_and_query: &str, args: &ExtraArgs) -> ResponseExt {
784        let profile = get_profile(path_and_query);
785        profile.send(args).await.unwrap()
786    }
787}