doc_assert/
lib.rs

1// Copyright 2024 The DocAssert Authors
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
15#![allow(clippy::while_let_on_iterator)]
16
17use crate::{
18    domain::{Request, Response},
19    json_diff::path::{Key, Path},
20};
21use serde_json::Value;
22use std::collections::HashMap;
23use std::fmt::Display;
24use std::vec;
25
26mod domain;
27mod executor;
28mod json_diff;
29mod parser;
30
31/// Builder for the assertions.
32///
33/// The builder is used to configure the assertions.
34///
35/// # Examples
36///
37/// ```
38/// # #![allow(unused_mut)]
39/// use doc_assert::DocAssert;
40/// use doc_assert::Variables;
41///
42/// async fn test() {
43///     // Create Variables for values that will be shared between requests and responses
44///     let mut variables = Variables::new();
45///     variables.insert_string("token".to_string(), "abcd".to_string());
46///     // Create a DocAssert builder with the base URL and the path to the documentation file
47///     let mut doc_assert = DocAssert::new()
48///         .with_url("http://localhost:8080")
49///         .with_doc_path("path/to/README.md");
50///     // Execute the assertions
51///     let report = doc_assert.assert().await;
52/// }
53/// ```
54pub struct DocAssert<'a> {
55    url: Option<&'a str>,
56    doc_paths: Vec<&'a str>,
57    pub(crate) variables: Variables,
58}
59
60impl<'a> DocAssert<'a> {
61    /// Constructs a new, empty `DocAssert` builder.
62    ///
63    /// The builder is used to configure the assertions.
64    ///
65    /// # Examples
66    ///
67    /// ```
68    /// # #![allow(unused_mut)]
69    /// use doc_assert::DocAssert;
70    /// let mut doc_assert = DocAssert::new();
71    /// ```
72    pub fn new() -> Self {
73        Self {
74            url: None,
75            doc_paths: vec![],
76            variables: Variables::new(),
77        }
78    }
79
80    /// Sets the base URL to test against.
81    ///
82    /// The URL will be used to make the requests.
83    ///
84    /// # Examples
85    ///
86    /// ```
87    /// # #![allow(unused_mut)]
88    /// use doc_assert::DocAssert;
89    /// let mut doc_assert = DocAssert::new().with_url("http://localhost:8080");
90    /// ```
91    pub fn with_url(mut self, url: &'a str) -> Self {
92        self.url = Some(url);
93        self
94    }
95
96    /// Sets the path to the documentation file.
97    ///
98    /// The path will be used to parse the documentation.
99    ///
100    /// # Examples
101    ///
102    /// ```
103    /// # #![allow(unused_mut)]
104    /// use doc_assert::DocAssert;
105    /// let mut doc_assert = DocAssert::new().with_doc_path("path/to/README.md");
106    /// ```
107    pub fn with_doc_path(mut self, doc_path: &'a str) -> Self {
108        self.doc_paths.push(doc_path);
109        self
110    }
111
112    /// Sets the variables to be used in the assertions.
113    ///
114    /// The variables will be used to replace the placeholders in the documentation.
115    ///
116    /// # Examples
117    ///
118    /// ```
119    /// # #![allow(unused_mut)]
120    /// use doc_assert::DocAssert;
121    /// use doc_assert::Variables;
122    ///
123    /// let mut variables = Variables::new();
124    /// variables.insert_string("token".to_string(), "abcd".to_string());
125    /// let mut doc_assert = DocAssert::new().with_variables(variables);
126    /// ```
127    pub fn with_variables(mut self, variables: Variables) -> Self {
128        self.variables = variables;
129        self
130    }
131
132    /// Execute the assertions
133    ///
134    /// The assertions will be executed and a report will be returned
135    ///
136    /// # Examples
137    ///
138    /// ```
139    /// # #![allow(unused_mut)]
140    /// use doc_assert::DocAssert;
141    /// async fn test() {
142    ///     let mut doc_assert = DocAssert::new()
143    ///         .with_url("http://localhost:8080")
144    ///         .with_doc_path("path/to/README.md");
145    ///     match doc_assert.assert().await {
146    ///         Ok(report) => {
147    ///             // handle success
148    ///         }
149    ///         Err(err) => {
150    ///             // handle error
151    ///         }
152    ///     };
153    /// }
154    /// ```
155    pub async fn assert(mut self) -> Result<Report, AssertionError> {
156        let url = self.url.take().expect("URL is required");
157        let mut total_count = 0;
158        let mut failed_count = 0;
159        let mut summary = String::new();
160        let mut failures = String::new();
161
162        for doc_path in self.doc_paths {
163            let test_cases = parser::parse(doc_path.to_string())
164                .map_err(|e| AssertionError::ParsingError(e.clone()))?;
165            for tc in test_cases {
166                total_count += 1;
167                let id = format!(
168                    "{} {} ({}:{})",
169                    tc.request.http_method, tc.request.uri, doc_path, tc.request.line_number
170                );
171                match executor::execute(url, tc, &mut self.variables).await {
172                    Ok(_) => summary.push_str(format!("{} ✅\n", id).as_str()),
173                    Err(err) => {
174                        summary.push_str(format!("{} ❌\n", id).as_str());
175                        failures.push_str(format!("-------------\n{}: {}\n", id, err).as_str());
176                        failed_count += 1;
177                    }
178                }
179            }
180        }
181
182        if failed_count == 0 {
183            Ok(Report {
184                total_count,
185                failed_count,
186                summary,
187                failures: None,
188            })
189        } else {
190            Err(AssertionError::TestSuiteError(Report {
191                total_count,
192                failed_count,
193                summary,
194                failures: Some(failures),
195            }))
196        }
197    }
198}
199
200impl<'a> Default for DocAssert<'a> {
201    fn default() -> Self {
202        Self::new()
203    }
204}
205
206/// Report of the assertions
207///
208/// The report contains the total number of tests, the number of failed tests,
209/// a summary of passed and failed tests, and detailed information about
210/// the failed assertions.
211///
212/// # Examples
213///
214/// ```
215/// # #![allow(unused_mut)]
216/// use doc_assert::DocAssert;
217/// use doc_assert::Variables;
218///
219/// async fn test() {
220///     let mut doc_assert = DocAssert::new()
221///         .with_url("http://localhost:8080")
222///         .with_doc_path("path/to/README.md");
223///     match doc_assert.assert().await {
224///         Ok(report) => {
225///             println!("{}", report);
226///         }
227///         Err(err) => {
228///             // handle error
229///         }
230///     };
231/// }
232pub struct Report {
233    /// Total number of tests
234    total_count: usize,
235    /// Number of failed tests
236    failed_count: usize,
237    /// Summary of passed and failed tests
238    summary: String,
239    /// Detailed information about the failed assertions
240    failures: Option<String>,
241}
242
243impl Display for Report {
244    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245        match &self.failures {
246            Some(failures) => write!(
247                f,
248                "{} tests\n{}\nfailures:\n{}\ntest result: FAILED. {} passed; {} failed",
249                self.total_count,
250                self.summary,
251                failures,
252                self.total_count - self.failed_count,
253                self.failed_count
254            ),
255            None => write!(
256                f,
257                "{} tests\n{}\ntest result: PASSED. {} passed; 0 failed",
258                self.total_count, self.summary, self.total_count
259            ),
260        }
261    }
262}
263
264/// Error type for DocAssert run
265pub enum AssertionError {
266    /// Error parsing the documentation file
267    ParsingError(String),
268    /// Error executing tests
269    TestSuiteError(Report),
270}
271
272/// Variables to be used in the request and response bodies.
273///
274/// The variables are used to replace placeholders in the request
275/// and response bodies in case some values need to be shared between requests and responses.
276///
277/// # Examples
278///
279/// Variables can be passed one by one with specified type:
280///
281/// ```
282/// # use doc_assert::Variables;
283/// # use serde_json::Value;
284/// let mut variables = Variables::new();
285/// variables.insert_string("name".to_string(), "John".to_string());
286/// variables.insert_int("age".to_string(), 30);
287/// ```
288///
289/// A `Value` can be passed directly:
290///
291/// ```
292/// # use doc_assert::Variables;
293/// # use serde_json::Value;
294/// let mut variables = Variables::new();
295/// variables.insert_value("name".to_string(), Value::String("John".to_string()));
296/// variables.insert_value("age".to_string(), Value::Number(serde_json::Number::from(30)));
297/// ```
298///
299/// Alternatively, they can be passed as a JSON object:
300///
301/// ```
302/// # use doc_assert::Variables;
303/// # use serde_json::Value;
304/// let json = r#"{"name": "John", "age": 30}"#;
305/// let variables = Variables::from_json(&serde_json::from_str(json).unwrap()).unwrap();
306/// ```
307///
308#[derive(Debug, Clone, Default)]
309pub struct Variables {
310    map: HashMap<String, Value>,
311}
312
313impl Variables {
314    /// Constructs a new `Variables`.
315    ///
316    /// # Examples
317    ///
318    /// ```
319    /// # use doc_assert::Variables;
320    /// let variables = Variables::new();
321    /// ```
322    pub fn new() -> Self {
323        Self {
324            map: HashMap::new(),
325        }
326    }
327
328    /// Constructs a new `Variables` from a JSON object.
329    ///
330    /// # Examples
331    ///
332    /// ```
333    /// # use doc_assert::Variables;
334    /// # use serde_json::Value;
335    /// let json = r#"{"name": "John", "age": 30}"#;
336    /// let variables = Variables::from_json(&serde_json::from_str(json).unwrap()).unwrap();
337    /// ```
338    pub fn from_json(json: &Value) -> Result<Self, String> {
339        let mut map = HashMap::new();
340
341        if let Value::Object(obj) = json {
342            for (key, value) in obj {
343                map.insert(key.clone(), value.clone());
344            }
345        } else {
346            return Err("variables must be an object".to_string());
347        }
348
349        Ok(Self { map })
350    }
351
352    /// Inserts a `Value` into the `Variables`.
353    ///
354    /// This can be useful when more complex types are needed.
355    /// Since `Variables` is a wrapper around `HashMap` if you insert duplicate
356    /// keys the value will be overwritten.
357    ///
358    /// # Examples
359    ///
360    /// ```
361    /// # use doc_assert::Variables;
362    /// # use serde_json::Value;
363    /// let mut variables = Variables::new();
364    /// variables.insert_value("name".to_string(), Value::String("John".to_string()));
365    /// ```
366    pub fn insert_value(&mut self, name: String, value: Value) {
367        self.map.insert(name, value);
368    }
369
370    /// Inserts a `String` into the `Variables`.
371    ///
372    /// # Examples
373    ///
374    /// ```
375    /// # use doc_assert::Variables;
376    /// let mut variables = Variables::new();
377    /// variables.insert_string("name".to_string(), "John".to_string());
378    /// ```
379    pub fn insert_string(&mut self, name: String, value: String) {
380        self.map.insert(name, Value::String(value));
381    }
382
383    /// Inserts an `i64` into the `Variables`.
384    ///
385    /// # Examples
386    ///
387    /// ```
388    /// # use doc_assert::Variables;
389    /// let mut variables = Variables::new();
390    /// variables.insert_int("age".to_string(), 30);
391    /// ```
392    pub fn insert_int(&mut self, name: String, value: i64) {
393        self.map
394            .insert(name, Value::Number(serde_json::Number::from(value)));
395    }
396
397    /// Inserts an `f64` into the `Variables`.
398    ///
399    /// # Examples
400    ///
401    /// ```
402    /// # use doc_assert::Variables;
403    /// let mut variables = Variables::new();
404    /// variables.insert_float("age".to_string(), 30.0);
405    /// ```
406    pub fn insert_float(&mut self, name: String, value: f64) {
407        self.map.insert(
408            name,
409            Value::Number(serde_json::Number::from_f64(value).unwrap()),
410        );
411    }
412
413    /// Inserts a `bool` into the `Variables`.
414    ///
415    /// # Examples
416    ///
417    /// ```
418    /// # use doc_assert::Variables;
419    /// let mut variables = Variables::new();
420    /// variables.insert_bool("is_adult".to_string(), true);
421    /// ```
422    pub fn insert_bool(&mut self, name: String, value: bool) {
423        self.map.insert(name, Value::Bool(value));
424    }
425
426    /// Inserts a `null` into the `Variables`.
427    ///
428    /// # Examples
429    ///
430    /// ```
431    /// # use doc_assert::Variables;
432    /// let mut variables = Variables::new();
433    /// variables.insert_null("name".to_string());
434    /// ```
435    pub fn insert_null(&mut self, name: String) {
436        self.map.insert(name, Value::Null);
437    }
438
439    pub(crate) fn obtain_from_response(
440        &mut self,
441        response: &Value,
442        variable_templates: &HashMap<String, Path>,
443    ) -> Result<(), String> {
444        for (name, path) in variable_templates {
445            let value = extract_value(path, response).ok_or_else(|| {
446                format!("variable template {} not found in the response body", name)
447            })?;
448
449            self.map.insert(name.clone(), value);
450        }
451
452        Ok(())
453    }
454
455    fn replace_placeholders(&self, input: &mut String, trim_quotes: bool) -> Result<(), String> {
456        for (name, value) in &self.map {
457            let placeholder = format!("`{}`", name);
458            let value_str = value.to_string();
459
460            let value = if trim_quotes {
461                value_str.trim_matches('"')
462            } else {
463                value_str.as_str()
464            };
465
466            *input = input.replace(&placeholder, value);
467        }
468
469        if input.contains('`') {
470            return Err(format!("unresolved variable placeholders in {}", input));
471        }
472
473        Ok(())
474    }
475
476    pub(crate) fn replace_request_placeholders(&self, input: &mut Request) -> Result<(), String> {
477        self.replace_placeholders(&mut input.uri, true)?;
478
479        if let Some(body) = &mut input.body {
480            self.replace_placeholders(body, false)?;
481        }
482
483        for (_, value) in &mut input.headers.iter_mut() {
484            self.replace_placeholders(value, true)?;
485        }
486
487        Ok(())
488    }
489
490    pub(crate) fn replace_response_placeholders(&self, input: &mut Response) -> Result<(), String> {
491        if let Some(body) = &mut input.body {
492            self.replace_placeholders(body, false)?;
493        }
494
495        for (_, value) in &mut input.headers.iter_mut() {
496            self.replace_placeholders(value, true)?;
497        }
498
499        Ok(())
500    }
501}
502
503fn extract_value(path: &Path, value: &Value) -> Option<Value> {
504    match path {
505        Path::Root => None,
506        Path::Keys(keys) => {
507            let mut current = value;
508            for key in keys {
509                match key {
510                    Key::Field(field) => current = current.get(field)?,
511                    Key::Idx(index) => current = current.get(index)?,
512                    _ => return None,
513                }
514            }
515            Some(current.clone())
516        }
517    }
518}