use std::collections::BTreeMap;
use crate::store::{Store, StoreError};
pub type PublicApi = BTreeMap<String, String>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ApiChange {
Breaking(String),
Additive(String),
None,
}
pub fn public_api_at_op(store: &Store, op_id: &str) -> Result<PublicApi, StoreError> {
let head = crate::render::package_head_at_op(store, op_id)?;
let pairs: Vec<(String, String)> =
head.map.iter().map(|(s, st)| (s.clone(), st.clone())).collect();
let mut api = PublicApi::new();
for ast in store.get_asts_for_sigs_bulk(&pairs) {
match ast? {
lex_ast::Stage::FnDecl(fd) => {
let param_types: Vec<&lex_ast::TypeExpr> = fd.params.iter().map(|p| &p.ty).collect();
let sig = serde_json::to_string(&(¶m_types, &fd.return_type, &fd.effects))
.unwrap_or_default();
api.insert(fd.name.clone(), format!("fn:{sig}"));
}
lex_ast::Stage::TypeDecl(td) => {
let sig = serde_json::to_string(&td.definition).unwrap_or_default();
api.insert(td.name.clone(), format!("type:{sig}"));
}
lex_ast::Stage::Import(_) => {}
}
}
Ok(api)
}
pub fn external_dependencies_at_op(store: &Store, op_id: &str) -> Result<Vec<String>, StoreError> {
let head = crate::render::package_head_at_op(store, op_id)?;
let mut deps = std::collections::BTreeSet::new();
for imports in head.file_imports.values() {
for module in imports.keys() {
if module.starts_with("std.")
|| module.starts_with("./")
|| module.starts_with("../")
|| module.starts_with('/')
{
continue;
}
let pkg = module.split('/').next().unwrap_or(module);
if !pkg.is_empty() {
deps.insert(pkg.to_string());
}
}
}
Ok(deps.into_iter().collect())
}
fn bare(name: &str) -> &str {
name.split_once('.').map(|(_, n)| n).unwrap_or(name)
}
pub fn classify_api_change(prev: &PublicApi, new: &PublicApi) -> ApiChange {
for (name, sig) in prev {
match new.get(name) {
None => return ApiChange::Breaking(format!("`{}` was removed", bare(name))),
Some(new_sig) if new_sig != sig => {
return ApiChange::Breaking(format!("signature of `{}` changed", bare(name)))
}
_ => {}
}
}
if let Some(added) = new.keys().find(|k| !prev.contains_key(*k)) {
return ApiChange::Additive(format!("`{}` was added", bare(added)));
}
ApiChange::None
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct Rename {
pub old: String,
pub new: String,
}
pub fn detect_renames(prev: &PublicApi, new: &PublicApi) -> Vec<Rename> {
let removed: Vec<(&String, &String)> =
prev.iter().filter(|(k, _)| !new.contains_key(*k)).collect();
let added: Vec<(&String, &String)> =
new.iter().filter(|(k, _)| !prev.contains_key(*k)).collect();
let mut renames = Vec::new();
let mut used_added = std::collections::BTreeSet::new();
for (old_name, old_sig) in &removed {
let removed_same = removed.iter().filter(|(_, s)| s == old_sig).count();
let matches: Vec<&(&String, &String)> = added
.iter()
.filter(|(n, s)| s == old_sig && !used_added.contains(*n))
.collect();
if removed_same == 1 && matches.len() == 1 {
let (new_name, _) = matches[0];
used_added.insert((*new_name).clone());
renames.push(Rename {
old: bare(old_name).to_string(),
new: bare(new_name).to_string(),
});
}
}
renames
}
#[cfg(test)]
mod tests {
use super::*;
fn api(pairs: &[(&str, &str)]) -> PublicApi {
pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
}
#[test]
fn removal_is_breaking() {
let prev = api(&[("foo", "fn:A"), ("bar", "fn:B")]);
let new = api(&[("foo", "fn:A")]);
assert!(matches!(classify_api_change(&prev, &new), ApiChange::Breaking(_)));
}
#[test]
fn signature_change_is_breaking() {
let prev = api(&[("foo", "fn:A")]);
let new = api(&[("foo", "fn:B")]);
assert!(matches!(classify_api_change(&prev, &new), ApiChange::Breaking(_)));
}
#[test]
fn pure_addition_is_additive() {
let prev = api(&[("foo", "fn:A")]);
let new = api(&[("foo", "fn:A"), ("bar", "fn:B")]);
assert!(matches!(classify_api_change(&prev, &new), ApiChange::Additive(_)));
}
#[test]
fn no_signature_change_is_none() {
let prev = api(&[("foo", "fn:A"), ("bar", "type:T")]);
let new = api(&[("foo", "fn:A"), ("bar", "type:T")]);
assert_eq!(classify_api_change(&prev, &new), ApiChange::None);
}
#[test]
fn removal_plus_addition_is_breaking() {
let prev = api(&[("foo", "fn:A")]);
let new = api(&[("bar", "fn:B")]);
assert!(matches!(classify_api_change(&prev, &new), ApiChange::Breaking(_)));
}
#[test]
fn detects_a_same_signature_rename() {
let prev = api(&[("m_a1.gcd", "fn:SIG"), ("m_a1.other", "fn:X")]);
let new = api(&[("m_a1.euclidean_gcd", "fn:SIG"), ("m_a1.other", "fn:X")]);
let renames = detect_renames(&prev, &new);
assert_eq!(renames, vec![Rename { old: "gcd".into(), new: "euclidean_gcd".into() }]);
}
#[test]
fn does_not_infer_rename_when_signature_differs() {
let prev = api(&[("m.foo", "fn:A")]);
let new = api(&[("m.bar", "fn:B")]);
assert!(detect_renames(&prev, &new).is_empty());
}
#[test]
fn does_not_infer_rename_when_ambiguous() {
let prev = api(&[("m.a", "fn:S"), ("m.b", "fn:S")]);
let new = api(&[("m.c", "fn:S"), ("m.d", "fn:S")]);
assert!(detect_renames(&prev, &new).is_empty());
}
}