1use crate::req::RequestContext;
2use anyhow::Result;
3use console::{style, Style};
4use reqwest::Response;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use similar::{ChangeTag, TextDiff};
8use std::{collections::HashMap, fmt, io::Write, path::Path};
9use tokio::fs;
10
11#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
12pub struct DiffConfig {
13 #[serde(flatten)]
14 ctxs: HashMap<String, DiffContext>,
15}
16
17#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
18pub struct DiffContext {
19 pub request1: RequestContext,
20 pub request2: RequestContext,
21 #[serde(skip_serializing_if = "is_default_response", default)]
22 pub response: ResponseContext,
23}
24
25fn is_default_response(r: &ResponseContext) -> bool {
26 r == &ResponseContext::default()
27}
28
29#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)]
30pub struct ResponseContext {
31 #[serde(skip_serializing_if = "Vec::is_empty")]
32 pub skip_headers: Vec<String>,
33}
34
35#[derive(Debug, PartialEq, Eq)]
36pub enum DiffResult {
37 Equal,
38 Diff(String),
39}
40
41impl ResponseContext {
42 pub fn new(skip_headers: Vec<String>) -> Self {
43 Self { skip_headers }
44 }
45}
46
47struct Line(Option<usize>);
48
49impl fmt::Display for Line {
50 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
51 match self.0 {
52 None => write!(f, " "),
53 Some(idx) => write!(f, "{:<4}", idx + 1),
54 }
55 }
56}
57
58impl DiffConfig {
59 pub fn new_with_profile(
60 profile: String,
61 req1: RequestContext,
62 req2: RequestContext,
63 res: ResponseContext,
64 ) -> Self {
65 let ctx = DiffContext::new(req1, req2, res);
66 let mut ctxs = HashMap::new();
67 ctxs.insert(profile, ctx);
68 Self { ctxs }
69 }
70
71 pub async fn try_load(path: impl AsRef<Path>) -> Result<DiffConfig> {
72 let file = fs::read_to_string(path).await?;
73 let config: DiffConfig = serde_yaml::from_str(&file)?;
74 for (profile, ctx) in config.ctxs.iter() {
75 if !ctx.request1.params.is_object() || !ctx.request2.params.is_object() {
76 return Err(anyhow::anyhow!(
77 "params in request1 or request2 must be an object in profile: {}",
78 profile
79 ));
80 }
81 }
82 Ok(config)
83 }
84
85 pub fn get(&self, profile: &str) -> Result<&DiffContext> {
86 self.ctxs.get(profile).ok_or_else(|| {
87 anyhow::anyhow!(
88 "profile {} not found. Available profiles: {:?}.",
89 profile,
90 self.ctxs.keys()
91 )
92 })
93 }
94
95 pub async fn diff(&self, profile: &str) -> Result<DiffResult> {
96 let ctx = self.get(profile)?;
97
98 ctx.diff().await
99 }
100}
101
102impl DiffContext {
103 pub fn new(req1: RequestContext, req2: RequestContext, resp: ResponseContext) -> Self {
104 Self {
105 request1: req1,
106 request2: req2,
107 response: resp,
108 }
109 }
110
111 pub async fn diff(&self) -> Result<DiffResult> {
112 let res1 = self.request1.send().await?;
113 let res2 = self.request2.send().await?;
114
115 self.diff_response(res1, res2).await
116 }
117
118 async fn diff_response(&self, res1: Response, res2: Response) -> Result<DiffResult> {
119 let url1 = res1.url().to_string();
120 let url2 = res2.url().to_string();
121
122 let text1 = self.request_to_string(res1).await?;
123 let text2 = self.request_to_string(res2).await?;
124
125 if text1 != text2 {
126 let headers = format!("--- a/{}\n+++ b/{}\n", url1, url2);
127 return Ok(DiffResult::Diff(build_diff(headers, text1, text2)?));
128 }
129
130 Ok(DiffResult::Equal)
131 }
132
133 async fn request_to_string(&self, res: Response) -> Result<String> {
134 let mut buf = Vec::new();
135
136 writeln!(&mut buf, "{}", res.status()).unwrap();
137 res.headers().iter().for_each(|(k, v)| {
138 if self.response.skip_headers.iter().any(|v| v == k.as_str()) {
139 return;
140 }
141 writeln!(&mut buf, "{}: {:?}", k, v).unwrap();
142 });
143 writeln!(&mut buf).unwrap();
144
145 let mut body = res.text().await?;
146
147 if let Ok(json) = serde_json::from_str::<Value>(&body) {
148 body = serde_json::to_string_pretty(&json)?;
149 }
150
151 writeln!(&mut buf, "{}", body).unwrap();
152
153 Ok(String::from_utf8(buf)?)
154 }
155}
156
157fn build_diff(headers: String, old: String, new: String) -> Result<String> {
158 let diff = TextDiff::from_lines(&old, &new);
159 let mut buf = Vec::with_capacity(4096);
160 writeln!(&mut buf, "{}", headers).unwrap();
161 for (idx, group) in diff.grouped_ops(3).iter().enumerate() {
162 if idx > 0 {
163 writeln!(&mut buf, "{:-^1$}", "-", 80)?;
164 }
165 for op in group {
166 for change in diff.iter_inline_changes(op) {
167 let (sign, s) = match change.tag() {
168 ChangeTag::Delete => ("-", Style::new().red()),
169 ChangeTag::Insert => ("+", Style::new().green()),
170 ChangeTag::Equal => (" ", Style::new().dim()),
171 };
172 write!(
173 &mut buf,
174 "{}{} |{}",
175 style(Line(change.old_index())).dim(),
176 style(Line(change.new_index())).dim(),
177 s.apply_to(sign).bold(),
178 )?;
179 for (emphasized, value) in change.iter_strings_lossy() {
180 if emphasized {
181 write!(&mut buf, "{}", s.apply_to(value).underlined().on_black())?;
182 } else {
183 write!(&mut buf, "{}", s.apply_to(value))?;
184 }
185 }
186 if change.missing_newline() {
187 writeln!(&mut buf)?;
188 }
189 }
190 }
191 }
192 Ok(String::from_utf8(buf)?)
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[tokio::test]
200 async fn diff_request_should_work() {
201 let config = DiffConfig::try_load("fixtures/diff.yml").await.unwrap();
202 let result = config.diff("rust").await.unwrap();
203 assert_eq!(result, DiffResult::Equal);
204 }
205}