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(¶ms)?;
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}