schemadoc_diff/exporters/
markdown.rs

1use indexmap::IndexMap;
2use std::cell::RefCell;
3
4use crate::core::DiffResult;
5use crate::exporters::{display_method, display_uri, Exporter, Markdown};
6
7use crate::checker::ValidationIssue;
8use crate::path_pointer::PathPointer;
9use crate::schema_diff::{HttpSchemaDiff, OperationDiff};
10
11use crate::visitor::{dispatch_visitor, DiffVisitor};
12
13struct PathToMarkdownVisitor<'s, 'v> {
14    invalid_only: bool,
15    endpoints: Option<&'v [String]>,
16    validations: Option<&'v [ValidationIssue]>,
17
18    added: RefCell<Vec<(PathPointer, &'s OperationDiff, bool)>>,
19    updated: RefCell<Vec<(PathPointer, &'s OperationDiff, bool)>>,
20    removed: RefCell<Vec<(PathPointer, &'s OperationDiff, bool)>>,
21}
22
23impl<'s, 'v> DiffVisitor<'s> for PathToMarkdownVisitor<'s, 'v> {
24    fn visit_operation(
25        &self,
26        pointer: &PathPointer,
27        _method: &str,
28        operation_diff_result: &'s DiffResult<OperationDiff>,
29    ) -> bool {
30        if let Some(endpoints) = self.endpoints {
31            if !endpoints.is_empty() {
32                let is_matches =
33                    endpoints.iter().any(|filter| pointer.matches(filter));
34                if !is_matches {
35                    return false;
36                }
37            }
38        }
39
40        let mut has_breaking = false;
41        if let Some(validations) = self.validations {
42            let is_invalid = validations
43                .iter()
44                .any(|validation| validation.path.startswith(pointer));
45            if self.invalid_only && !is_invalid {
46                return false;
47            }
48
49            has_breaking = validations.iter().any(|validation| {
50                validation.path.startswith(pointer) && validation.breaking
51            });
52        }
53
54        match operation_diff_result {
55            DiffResult::None => {}
56            DiffResult::Same(_) => {}
57            DiffResult::Added(value) => {
58                self.added.borrow_mut().push((
59                    pointer.clone(),
60                    value,
61                    has_breaking,
62                ));
63            }
64            DiffResult::Updated(value, _) => {
65                self.updated.borrow_mut().push((
66                    pointer.clone(),
67                    value,
68                    has_breaking,
69                ));
70            }
71            DiffResult::Removed(value) => {
72                self.removed.borrow_mut().push((
73                    pointer.clone(),
74                    value,
75                    has_breaking,
76                ));
77            }
78        };
79
80        false
81    }
82}
83
84impl Exporter<Markdown> for HttpSchemaDiff {
85    fn export(
86        &self,
87        info: IndexMap<&str, &str>,
88        version_url: &str,
89        invalid_only: bool,
90        endpoints: Option<&[String]>,
91        validations: Option<&[ValidationIssue]>,
92    ) -> Markdown {
93        let visitor = PathToMarkdownVisitor {
94            invalid_only,
95            endpoints,
96            validations,
97            added: RefCell::new(vec![]),
98            updated: RefCell::new(vec![]),
99            removed: RefCell::new(vec![]),
100        };
101
102        dispatch_visitor(self, &visitor);
103
104        let mut markdown = String::new();
105
106        let added = visitor.added.borrow();
107        let updated = visitor.updated.borrow();
108        let removed = visitor.removed.borrow();
109
110        let is_unchanged =
111            added.is_empty() && updated.is_empty() && removed.is_empty();
112        if !is_unchanged {
113            markdown.push_str("*API Schema diff*\n");
114
115            info.iter().for_each(|(field, value)| {
116                markdown.push_str(&format!("{field}: *{value}*\n"))
117            });
118
119            let now = chrono::Utc::now()
120                .to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
121            markdown.push_str(&format!("Generated at: *{now} UTC*\n"));
122        }
123
124        if added.len() > 0 {
125            markdown.push_str(&format!("\n*Added ({})*\n", added.len()));
126            for (path, _, breaking) in added.iter() {
127                markdown.push_str(&format_path(path, *breaking, version_url));
128            }
129        }
130
131        if updated.len() > 0 {
132            markdown.push_str(&format!("\n*Updated ({})*\n", updated.len()));
133            for (path, _, breaking) in updated.iter() {
134                markdown.push_str(&format_path(path, *breaking, version_url));
135            }
136        }
137
138        if removed.len() > 0 {
139            markdown.push_str(&format!("\n*Removed ({})*\n", removed.len()));
140            for (path, _, breaking) in removed.iter() {
141                markdown.push_str(&format_path(path, *breaking, version_url));
142            }
143        }
144
145        Markdown::new(markdown, is_unchanged)
146    }
147}
148
149fn format_path(
150    path: &PathPointer,
151    breaking: bool,
152    version_url: &str,
153) -> String {
154    let breaking = if breaking { "!" } else { "-" };
155
156    let url = format!("{}#{}", version_url, path.get_path());
157
158    let method = display_method(path).to_uppercase();
159    let uri = display_uri(path);
160
161    format!(" {breaking} `{method:^8}` `{uri}` <{url}|view>\n")
162}