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}