1use std::collections::BTreeMap;
11
12use serde_json::Value;
13
14use crate::compat::{Change, Direction, SchemaDiff, Severity, compare_keys, deref, sort};
15
16pub fn diff(old: &Value, new: &Value) -> Vec<Change> {
18 let mut changes = Vec::new();
19 let (old_ops, new_ops) = (operations(old), operations(new));
20
21 compare_keys(
22 &mut changes,
23 &old_ops,
24 &new_ops,
25 "operation",
26 |ch, at, o, n| {
27 let (oa, na) = (action(o), action(n));
28 if oa != na {
29 ch.push(Change::new(
30 Severity::Breaking,
31 at,
32 format!("action {oa} -> {na}"),
33 ));
34 return; }
36
37 let (oc, nc) = (address(old, o), address(new, n));
38 if oc != nc {
39 ch.push(Change::new(
40 Severity::Breaking,
41 at,
42 format!("channel address {oc} -> {nc}"),
43 ));
44 }
45
46 let dir = match na {
47 "send" => Direction::Response,
49 _ => Direction::Request,
50 };
51 match (payload(old, o), payload(new, n)) {
52 (Some(op), Some(np)) => {
53 SchemaDiff::new(old, new).compare(ch, &format!("{at} payload"), &op, &np, dir);
54 }
55 (Some(_), None) => ch.push(Change::new(
56 Severity::Breaking,
57 format!("{at} payload"),
58 "message payload removed",
59 )),
60 (None, Some(_)) => ch.push(Change::new(
61 Severity::Additive,
62 format!("{at} payload"),
63 "message payload added",
64 )),
65 (None, None) => {}
66 }
67 },
68 );
69
70 sort(&mut changes);
71 changes
72}
73
74fn operations(doc: &Value) -> BTreeMap<String, Value> {
75 doc["operations"]
76 .as_object()
77 .map(|ops| ops.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
78 .unwrap_or_default()
79}
80
81fn action(op: &Value) -> &str {
82 op["action"].as_str().unwrap_or("?")
83}
84
85fn address<'a>(doc: &'a Value, op: &'a Value) -> &'a str {
89 follow(doc, &op["channel"])["address"]
90 .as_str()
91 .unwrap_or("?")
92}
93
94fn payload(doc: &Value, op: &Value) -> Option<Value> {
100 let first = op["messages"].as_array()?.first()?;
101 let message = follow(doc, first);
102 let payload = &message["payload"];
103 (!payload.is_null()).then(|| payload.clone())
104}
105
106fn follow<'a>(doc: &'a Value, mut node: &'a Value) -> &'a Value {
109 for _ in 0..8 {
112 if !node["$ref"].is_string() {
113 break;
114 }
115 let next = deref(doc, node);
116 if std::ptr::eq(next, node) {
117 break; }
119 node = next;
120 }
121 node
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use crate::compat::is_compatible;
128 use serde_json::json;
129
130 fn doc(action: &str, payload: Value) -> Value {
132 json!({
133 "asyncapi": "3.0.0",
134 "channels": {
135 "orders": {
136 "address": "orders",
137 "messages": { "Order": { "$ref": "#/components/messages/Order" } },
138 },
139 },
140 "operations": {
141 "onOrder": {
142 "action": action,
143 "channel": { "$ref": "#/channels/orders" },
144 "messages": [ { "$ref": "#/channels/orders/messages/Order" } ],
145 },
146 },
147 "components": { "messages": { "Order": { "payload": payload } } },
148 })
149 }
150
151 fn object(props: Value, required: &[&str]) -> Value {
152 json!({ "type": "object", "properties": props, "required": required })
153 }
154
155 #[test]
156 fn an_unchanged_document_is_compatible_with_itself() {
157 let d = doc(
158 "send",
159 object(json!({ "id": { "type": "string" } }), &["id"]),
160 );
161 assert!(is_compatible(&diff(&d, &d)));
162 assert!(diff(&d, &d).is_empty());
163 }
164
165 #[test]
166 fn a_sent_field_that_stops_being_guaranteed_breaks_consumers() {
167 let old = doc(
168 "send",
169 object(json!({ "id": { "type": "string" } }), &["id"]),
170 );
171 let new = doc("send", object(json!({ "id": { "type": "string" } }), &[]));
172 let changes = diff(&old, &new);
173 assert!(!is_compatible(&changes), "got {changes:?}");
174 assert!(changes[0].detail.contains("no longer guaranteed"));
175 }
176
177 #[test]
178 fn a_new_field_on_a_sent_message_is_additive() {
179 let old = doc(
180 "send",
181 object(json!({ "id": { "type": "string" } }), &["id"]),
182 );
183 let new = doc(
184 "send",
185 object(
186 json!({ "id": { "type": "string" }, "note": { "type": "string" } }),
187 &["id", "note"],
188 ),
189 );
190 assert!(is_compatible(&diff(&old, &new)), "{:?}", diff(&old, &new));
191 }
192
193 #[test]
194 fn the_same_new_field_on_a_received_message_breaks_producers() {
195 let old = doc(
196 "receive",
197 object(json!({ "id": { "type": "string" } }), &["id"]),
198 );
199 let new = doc(
200 "receive",
201 object(
202 json!({ "id": { "type": "string" }, "note": { "type": "string" } }),
203 &["id", "note"],
204 ),
205 );
206 let changes = diff(&old, &new);
207 assert!(!is_compatible(&changes), "got {changes:?}");
208 assert_eq!(changes[0].severity, Severity::Breaking);
209 }
210
211 #[test]
212 fn reversing_the_flow_is_breaking_on_its_own() {
213 let payload = object(json!({ "id": { "type": "string" } }), &["id"]);
214 let changes = diff(&doc("send", payload.clone()), &doc("receive", payload));
215 assert_eq!(changes.len(), 1, "got {changes:?}");
216 assert!(changes[0].detail.contains("send -> receive"));
217 }
218
219 #[test]
220 fn moving_a_channel_to_another_address_is_breaking() {
221 let payload = object(json!({}), &[]);
222 let old = doc("send", payload.clone());
223 let mut new = doc("send", payload);
224 new["channels"]["orders"]["address"] = json!("orders.v2");
225 let changes = diff(&old, &new);
226 assert_eq!(changes.len(), 1, "got {changes:?}");
227 assert!(changes[0].detail.contains("orders -> orders.v2"));
228 }
229
230 #[test]
231 fn a_removed_operation_breaks_consumers_and_a_new_one_does_not() {
232 let payload = object(json!({}), &[]);
233 let old = doc("send", payload.clone());
234 let mut new = doc("send", payload);
235 new["operations"] = json!({ "onOther": new["operations"]["onOrder"].clone() });
236 let changes = diff(&old, &new);
237 assert_eq!(changes.len(), 2, "got {changes:?}");
238 assert_eq!(changes[0].severity, Severity::Breaking);
239 assert_eq!(changes[0].at, "onOrder");
240 assert_eq!(changes[1].severity, Severity::Additive);
241 }
242
243 #[test]
244 fn payloads_resolve_through_the_channel_and_components_chain() {
245 let old = doc(
249 "send",
250 object(json!({ "id": { "type": "string" } }), &["id"]),
251 );
252 let new = doc(
253 "send",
254 object(json!({ "id": { "type": "integer" } }), &["id"]),
255 );
256 let changes = diff(&old, &new);
257 assert_eq!(changes.len(), 1, "got {changes:?}");
258 assert!(changes[0].detail.contains("string -> integer"));
259 }
260
261 #[test]
262 fn a_dangling_reference_stops_rather_than_hanging() {
263 let mut d = doc("send", object(json!({}), &[]));
264 d["operations"]["onOrder"]["messages"] = json!([{ "$ref": "#/components/messages/Gone" }]);
265 assert!(diff(&d, &d).is_empty());
266 }
267}