1use std::collections::BTreeMap;
14
15use crate::store::{Store, StoreError};
16
17pub type PublicApi = BTreeMap<String, String>;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum ApiChange {
23 Breaking(String),
26 Additive(String),
29 None,
31}
32
33pub fn public_api_at_op(store: &Store, op_id: &str) -> Result<PublicApi, StoreError> {
37 let head = crate::render::package_head_at_op(store, op_id)?;
38 let pairs: Vec<(String, String)> =
39 head.map.iter().map(|(s, st)| (s.clone(), st.clone())).collect();
40 let mut api = PublicApi::new();
41 for ast in store.get_asts_for_sigs_bulk(&pairs) {
42 match ast? {
43 lex_ast::Stage::FnDecl(fd) => {
44 let param_types: Vec<&lex_ast::TypeExpr> = fd.params.iter().map(|p| &p.ty).collect();
48 let sig = serde_json::to_string(&(¶m_types, &fd.return_type, &fd.effects))
49 .unwrap_or_default();
50 api.insert(fd.name.clone(), format!("fn:{sig}"));
51 }
52 lex_ast::Stage::TypeDecl(td) => {
53 let sig = serde_json::to_string(&td.definition).unwrap_or_default();
54 api.insert(td.name.clone(), format!("type:{sig}"));
55 }
56 lex_ast::Stage::Import(_) => {}
57 }
58 }
59 Ok(api)
60}
61
62fn bare(name: &str) -> &str {
64 name.split_once('.').map(|(_, n)| n).unwrap_or(name)
65}
66
67pub fn classify_api_change(prev: &PublicApi, new: &PublicApi) -> ApiChange {
70 for (name, sig) in prev {
71 match new.get(name) {
72 None => return ApiChange::Breaking(format!("`{}` was removed", bare(name))),
73 Some(new_sig) if new_sig != sig => {
74 return ApiChange::Breaking(format!("signature of `{}` changed", bare(name)))
75 }
76 _ => {}
77 }
78 }
79 if let Some(added) = new.keys().find(|k| !prev.contains_key(*k)) {
80 return ApiChange::Additive(format!("`{}` was added", bare(added)));
81 }
82 ApiChange::None
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
90pub struct Rename {
91 pub old: String,
92 pub new: String,
93}
94
95pub fn detect_renames(prev: &PublicApi, new: &PublicApi) -> Vec<Rename> {
100 let removed: Vec<(&String, &String)> =
102 prev.iter().filter(|(k, _)| !new.contains_key(*k)).collect();
103 let added: Vec<(&String, &String)> =
104 new.iter().filter(|(k, _)| !prev.contains_key(*k)).collect();
105
106 let mut renames = Vec::new();
107 let mut used_added = std::collections::BTreeSet::new();
108 for (old_name, old_sig) in &removed {
109 let removed_same = removed.iter().filter(|(_, s)| s == old_sig).count();
111 let matches: Vec<&(&String, &String)> = added
112 .iter()
113 .filter(|(n, s)| s == old_sig && !used_added.contains(*n))
114 .collect();
115 if removed_same == 1 && matches.len() == 1 {
116 let (new_name, _) = matches[0];
117 used_added.insert((*new_name).clone());
118 renames.push(Rename {
119 old: bare(old_name).to_string(),
120 new: bare(new_name).to_string(),
121 });
122 }
123 }
124 renames
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 fn api(pairs: &[(&str, &str)]) -> PublicApi {
132 pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
133 }
134
135 #[test]
136 fn removal_is_breaking() {
137 let prev = api(&[("foo", "fn:A"), ("bar", "fn:B")]);
138 let new = api(&[("foo", "fn:A")]);
139 assert!(matches!(classify_api_change(&prev, &new), ApiChange::Breaking(_)));
140 }
141
142 #[test]
143 fn signature_change_is_breaking() {
144 let prev = api(&[("foo", "fn:A")]);
145 let new = api(&[("foo", "fn:B")]);
146 assert!(matches!(classify_api_change(&prev, &new), ApiChange::Breaking(_)));
147 }
148
149 #[test]
150 fn pure_addition_is_additive() {
151 let prev = api(&[("foo", "fn:A")]);
152 let new = api(&[("foo", "fn:A"), ("bar", "fn:B")]);
153 assert!(matches!(classify_api_change(&prev, &new), ApiChange::Additive(_)));
154 }
155
156 #[test]
157 fn no_signature_change_is_none() {
158 let prev = api(&[("foo", "fn:A"), ("bar", "type:T")]);
160 let new = api(&[("foo", "fn:A"), ("bar", "type:T")]);
161 assert_eq!(classify_api_change(&prev, &new), ApiChange::None);
162 }
163
164 #[test]
165 fn removal_plus_addition_is_breaking() {
166 let prev = api(&[("foo", "fn:A")]);
167 let new = api(&[("bar", "fn:B")]);
168 assert!(matches!(classify_api_change(&prev, &new), ApiChange::Breaking(_)));
169 }
170
171 #[test]
172 fn detects_a_same_signature_rename() {
173 let prev = api(&[("m_a1.gcd", "fn:SIG"), ("m_a1.other", "fn:X")]);
175 let new = api(&[("m_a1.euclidean_gcd", "fn:SIG"), ("m_a1.other", "fn:X")]);
176 let renames = detect_renames(&prev, &new);
177 assert_eq!(renames, vec![Rename { old: "gcd".into(), new: "euclidean_gcd".into() }]);
178 }
179
180 #[test]
181 fn does_not_infer_rename_when_signature_differs() {
182 let prev = api(&[("m.foo", "fn:A")]);
184 let new = api(&[("m.bar", "fn:B")]);
185 assert!(detect_renames(&prev, &new).is_empty());
186 }
187
188 #[test]
189 fn does_not_infer_rename_when_ambiguous() {
190 let prev = api(&[("m.a", "fn:S"), ("m.b", "fn:S")]);
192 let new = api(&[("m.c", "fn:S"), ("m.d", "fn:S")]);
193 assert!(detect_renames(&prev, &new).is_empty());
194 }
195}