1use crate::model::DiffResult;
2
3pub fn format_diff_text(diff: &DiffResult, breaking_only: bool) -> String {
4 let mut out = String::new();
5 if !breaking_only {
6 out.push_str(&format!(
7 "Summary: {} entity, {} axiom, {} annotation, {} import, {} inference, {} breaking\n\n",
8 diff.entity_changes.len(),
9 diff.axiom_changes.len(),
10 diff.annotation_changes.len(),
11 diff.import_changes.len(),
12 diff.inference_changes.len(),
13 diff.breaking_changes.len(),
14 ));
15 }
16 if !diff.breaking_changes.is_empty() {
17 out.push_str("Breaking changes:\n");
18 for b in &diff.breaking_changes {
19 out.push_str(&format!(" - [{}] {}\n", reason_label(b.reason), b.message));
20 }
21 out.push('\n');
22 }
23 if breaking_only {
24 return out;
25 }
26 if !diff.entity_changes.is_empty() {
27 out.push_str("Entity changes:\n");
28 for e in &diff.entity_changes {
29 out.push_str(&format!(" - {:?} {}\n", e.kind, e.iri));
30 }
31 out.push('\n');
32 }
33 if !diff.axiom_changes.is_empty() {
34 out.push_str("Axiom changes:\n");
35 for a in &diff.axiom_changes {
36 out.push_str(&format!(
37 " - {} {} {} {} {}\n",
38 a.change, a.axiom_kind, a.subject, a.predicate, a.object
39 ));
40 }
41 out.push('\n');
42 }
43 if !diff.annotation_changes.is_empty() {
44 out.push_str("Annotation changes:\n");
45 for a in &diff.annotation_changes {
46 out.push_str(&format!(" - {} {} {} {}\n", a.change, a.subject, a.predicate, a.object));
47 }
48 out.push('\n');
49 }
50 if !diff.import_changes.is_empty() {
51 out.push_str("Import changes:\n");
52 for i in &diff.import_changes {
53 out.push_str(&format!(" - {} {}\n", i.change, i.import_iri));
54 }
55 out.push('\n');
56 }
57 out
58}
59
60pub fn format_diff_markdown(diff: &DiffResult, breaking_only: bool) -> String {
61 let mut out = String::from("# Ontology semantic diff\n\n");
62 if !diff.breaking_changes.is_empty() {
63 out.push_str("## Breaking changes\n\n");
64 for b in &diff.breaking_changes {
65 out.push_str(&format!("- **{}**: {}\n", reason_label(b.reason), b.message));
66 }
67 out.push('\n');
68 }
69 if breaking_only {
70 return out;
71 }
72 out.push_str(&format!(
73 "## Summary\n\n| Category | Count |\n|---|---|\n| Entities | {} |\n| Axioms | {} |\n| Annotations | {} |\n| Imports | {} |\n| Inferences | {} |\n| Breaking | {} |\n\n",
74 diff.entity_changes.len(),
75 diff.axiom_changes.len(),
76 diff.annotation_changes.len(),
77 diff.import_changes.len(),
78 diff.inference_changes.len(),
79 diff.breaking_changes.len(),
80 ));
81 if !diff.entity_changes.is_empty() {
82 out.push_str("## Entity changes\n\n");
83 for e in &diff.entity_changes {
84 match &e.previous_iri {
85 Some(prev) => {
86 out.push_str(&format!("- **{:?}** `{}` ← `{}`\n", e.kind, e.iri, prev))
87 }
88 None => out.push_str(&format!("- **{:?}** `{}`\n", e.kind, e.iri)),
89 }
90 }
91 out.push('\n');
92 }
93 if !diff.axiom_changes.is_empty() {
94 out.push_str("## Axiom changes\n\n");
95 for a in &diff.axiom_changes {
96 out.push_str(&format!(
97 "- **{}** `{}` — {} {} {}\n",
98 a.change, a.axiom_kind, a.subject, a.predicate, a.object
99 ));
100 }
101 out.push('\n');
102 }
103 if !diff.annotation_changes.is_empty() {
104 out.push_str("## Annotation changes\n\n");
105 for a in &diff.annotation_changes {
106 out.push_str(&format!(
107 "- **{}** `{}` {} {}\n",
108 a.change, a.subject, a.predicate, a.object
109 ));
110 }
111 out.push('\n');
112 }
113 if !diff.import_changes.is_empty() {
114 out.push_str("## Import changes\n\n");
115 for i in &diff.import_changes {
116 out.push_str(&format!("- **{}** {}\n", i.change, i.import_iri));
117 }
118 out.push('\n');
119 }
120 if !diff.inference_changes.is_empty() {
121 out.push_str("## Inference changes\n\n");
122 for i in &diff.inference_changes {
123 out.push_str(&format!("- **{}** `{}` — {}\n", i.change, i.class_iri, i.detail));
124 }
125 out.push('\n');
126 }
127 out
128}
129
130pub fn format_diff_pr_summary(diff: &DiffResult) -> String {
131 let mut out = String::from("## Ontology changes\n\n");
132 if !diff.breaking_changes.is_empty() {
133 out.push_str("> **Breaking changes detected** — review carefully before merge.\n\n");
134 for b in &diff.breaking_changes {
135 out.push_str(&format!("- **{}**: {}\n", reason_label(b.reason), b.message));
136 }
137 out.push('\n');
138 }
139 let counts = format!(
140 "| Entities | Axioms | Annotations | Imports | Inferences |\n|---|---|---|---|---|\n| {} | {} | {} | {} | {} |\n\n",
141 diff.entity_changes.len(),
142 diff.axiom_changes.len(),
143 diff.annotation_changes.len(),
144 diff.import_changes.len(),
145 diff.inference_changes.len(),
146 );
147 out.push_str(&counts);
148
149 if !diff.entity_changes.is_empty() {
150 out.push_str("### Entities\n\n");
151 for e in diff.entity_changes.iter().take(50) {
152 out.push_str(&format!("- `{:?}` {}\n", e.kind, e.iri));
153 }
154 if diff.entity_changes.len() > 50 {
155 out.push_str(&format!(
156 "\n_…and {} more entity changes_\n",
157 diff.entity_changes.len() - 50
158 ));
159 }
160 out.push('\n');
161 }
162 if !diff.axiom_changes.is_empty() {
163 out.push_str("### Axioms\n\n");
164 for a in diff.axiom_changes.iter().take(30) {
165 out.push_str(&format!(
166 "- **{}** `{}` — {} {} {}\n",
167 a.change, a.axiom_kind, a.subject, a.predicate, a.object
168 ));
169 }
170 if diff.axiom_changes.len() > 30 {
171 out.push_str(&format!(
172 "\n_…and {} more axiom changes_\n",
173 diff.axiom_changes.len() - 30
174 ));
175 }
176 out.push('\n');
177 }
178 if !diff.import_changes.is_empty() {
179 out.push_str("### Imports\n\n");
180 for i in &diff.import_changes {
181 out.push_str(&format!("- **{}** {}\n", i.change, i.import_iri));
182 }
183 out.push('\n');
184 }
185 out.push_str("---\n_Generated by `ontocore diff --pr-summary`_\n");
186 out
187}
188
189pub fn format_diff_json(diff: &DiffResult) -> String {
190 serde_json::to_string_pretty(diff).unwrap_or_else(|_| "{}".to_string())
191}
192
193fn reason_label(reason: crate::model::BreakingReason) -> &'static str {
194 use crate::model::BreakingReason::*;
195 match reason {
196 RemovedEntity => "removed_entity",
197 RenamedIri => "renamed_iri",
198 RemovedSuperclass => "removed_superclass",
199 RemovedImport => "removed_import",
200 UnsatisfiableClass => "unsatisfiable_class",
201 DomainRangeChange => "domain_range_change",
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use crate::model::{AxiomChange, BreakingChange, DiffResult, EntityChange, EntityChangeKind};
209
210 #[test]
211 fn pr_summary_includes_breaking_callout() {
212 let diff = DiffResult {
213 entity_changes: vec![EntityChange {
214 iri: "http://ex#A".into(),
215 kind: EntityChangeKind::Removed,
216 previous_iri: None,
217 labels: vec![],
218 }],
219 axiom_changes: vec![],
220 annotation_changes: vec![],
221 import_changes: vec![],
222 inference_changes: vec![],
223 breaking_changes: vec![BreakingChange {
224 reason: crate::model::BreakingReason::RemovedEntity,
225 message: "class removed".into(),
226 entity_iri: Some("http://ex#A".into()),
227 }],
228 };
229 let md = format_diff_pr_summary(&diff);
230 assert!(md.contains("Breaking changes"));
231 assert!(md.contains("http://ex#A"));
232 assert!(md.contains("ontocore diff --pr-summary"));
233 }
234
235 #[test]
236 fn pr_summary_lists_axiom_rows() {
237 let diff = DiffResult {
238 entity_changes: vec![],
239 axiom_changes: vec![AxiomChange {
240 change: "added".into(),
241 axiom_kind: "SubClassOf".into(),
242 subject: "http://ex#Child".into(),
243 predicate: "rdfs:subClassOf".into(),
244 object: "http://ex#Parent".into(),
245 }],
246 annotation_changes: vec![],
247 import_changes: vec![],
248 inference_changes: vec![],
249 breaking_changes: vec![],
250 };
251 let md = format_diff_pr_summary(&diff);
252 assert!(md.contains("### Axioms"));
253 assert!(md.contains("SubClassOf"));
254 }
255
256 #[test]
257 fn markdown_includes_entity_and_axiom_sections() {
258 let diff = DiffResult {
259 entity_changes: vec![EntityChange {
260 iri: "http://ex#A".into(),
261 kind: EntityChangeKind::Added,
262 previous_iri: None,
263 labels: vec![],
264 }],
265 axiom_changes: vec![AxiomChange {
266 change: "added".into(),
267 axiom_kind: "SubClassOf".into(),
268 subject: "http://ex#Child".into(),
269 predicate: "rdfs:subClassOf".into(),
270 object: "http://ex#Parent".into(),
271 }],
272 annotation_changes: vec![],
273 import_changes: vec![],
274 inference_changes: vec![],
275 breaking_changes: vec![],
276 };
277 let md = format_diff_markdown(&diff, false);
278 assert!(md.contains("## Entity changes"));
279 assert!(md.contains("http://ex#A"));
280 assert!(md.contains("## Axiom changes"));
281 assert!(md.contains("SubClassOf"));
282 }
283}