schemadoc_diff/exporters/
markdown.rs1use 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}