use std::fmt::Write;
use crate::model::{FieldBinding, FnBinding, Model, ModuleBinding, ParamBinding};
use crate::ty::Ty;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContractEntry {
pub path: String,
pub id: u64,
pub hash: u64,
}
#[must_use]
pub const fn fnv1a64(data: &[u8]) -> u64 {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
let mut i = 0;
while i < data.len() {
hash ^= data[i] as u64;
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
i += 1;
}
hash
}
#[must_use]
pub fn entries(model: &Model, root: &ModuleBinding) -> Vec<ContractEntry> {
let mut out = Vec::new();
for m in model
.modules
.iter()
.filter(|m| m.segments.first() == root.segments.first())
{
module_entries(m, &mut out);
}
out.sort_by_key(|e| e.id);
out
}
fn entry(path: String, signature: &str) -> ContractEntry {
ContractEntry {
id: fnv1a64(path.as_bytes()),
hash: fnv1a64(signature.as_bytes()),
path,
}
}
fn module_entries(m: &ModuleBinding, out: &mut Vec<ContractEntry>) {
let dot = &m.dot_path;
for f in &m.functions {
out.push(entry(
format!("{dot}.{}", f.name),
&callable_signature("function", f),
));
}
for i in &m.interfaces {
let owner = format!("{dot}.{}", i.name);
out.push(entry(owner.clone(), &format!("interface {}", i.name)));
for (kind, members) in [
("constructor", &i.constructors),
("method", &i.methods),
("static", &i.statics),
] {
for f in members {
out.push(entry(
format!("{owner}.{}", f.name),
&callable_signature(kind, f),
));
}
}
}
for s in &m.structs {
out.push(entry(
format!("{dot}.{}", s.name),
&format!("record {} {{{}}}", s.name, fields(&s.fields)),
));
}
for e in &m.enums {
let variants: Vec<String> = e
.variants
.iter()
.map(|v| case(&v.name, v.value, &v.fields))
.collect();
out.push(entry(
format!("{dot}.{}", e.name),
&format!("enum {} {{{}}}", e.name, variants.join(", ")),
));
}
for c in &m.callback_interfaces {
let methods: Vec<String> = c
.methods
.iter()
.map(|cm| {
let mut sig = format!(
"{}({}) -> {}",
cm.name,
params(&cm.params),
ret(cm.ret.as_ref())
);
if cm.throws {
sig.push_str(" throws");
}
sig
})
.collect();
out.push(entry(
format!("{dot}.{}", c.name),
&format!("callback {} {{{}}}", c.name, methods.join("; ")),
));
}
if let Some(d) = &m.errors {
let codes: Vec<String> = d
.codes
.iter()
.map(|c| case(&c.name, c.value, &c.fields))
.collect();
out.push(entry(
format!("{dot}.{}", d.name),
&format!("errors {} {{{}}}", d.name, codes.join(", ")),
));
}
}
fn params(params: &[ParamBinding]) -> String {
let all: Vec<String> = params
.iter()
.map(|p| format!("{}: {}", p.name, p.ty))
.collect();
all.join(", ")
}
fn fields(fields: &[FieldBinding]) -> String {
let all: Vec<String> = fields
.iter()
.map(|f| format!("{}: {}", f.name, f.ty))
.collect();
all.join(", ")
}
fn ret(ty: Option<&Ty>) -> String {
ty.map_or_else(|| "void".to_string(), ToString::to_string)
}
fn case(name: &str, value: i32, case_fields: &[FieldBinding]) -> String {
let mut out = format!("{name} = {value}");
if !case_fields.is_empty() {
let _ = write!(out, " {{{}}}", fields(case_fields));
}
out
}
fn callable_signature(kind: &str, f: &FnBinding) -> String {
let mut sig = format!(
"{kind} {}({}) -> {}",
f.name,
params(&f.params),
ret(f.ret.as_ref())
);
if f.throws {
sig.push_str(" throws");
}
if f.is_async() {
sig.push_str(" async");
}
if f.cancellable {
sig.push_str(" cancellable");
}
sig
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::Api;
use crate::pkg::Identity;
fn model(yaml: &str) -> Model {
let api: Api = serde_yaml::from_str(yaml).unwrap();
crate::validate::validate(&api, &Identity::named("kv"), None).unwrap()
}
const KV: &str = r#"
version: "0.11.0"
modules:
- name: kv
doc: Key-value storage.
errors:
name: KvError
codes:
- { name: NotFound, code: 1, message: missing, fields: [{ name: key, type: string }] }
structs:
- name: Entry
fields: [{ name: key, type: string }, { name: value, type: bytes }]
interfaces:
- name: Store
constructors: [{ name: open, params: [{ name: path, type: string }], throws: true }]
methods:
- { name: get, params: [{ name: key, type: string }], return: "bytes?", throws: true }
callback_interfaces:
- name: Listener
methods:
- { name: on_put, params: [{ name: entry, type: Entry }], return: string, throws: true }
functions:
- { name: version, params: [], return: string, doc: The version. }
modules:
- name: stats
enums:
- name: Kind
variants: [{ name: Hot, value: 0 }, { name: Cold, value: 1 }]
"#;
fn paths(model: &Model) -> Vec<String> {
let root = model.roots().next().unwrap();
entries(model, root).into_iter().map(|e| e.path).collect()
}
#[test]
fn one_entry_per_declaration_sorted_by_id() {
let model = model(KV);
let root = model.roots().next().unwrap();
let table = entries(&model, root);
let mut got = paths(&model);
got.sort();
assert_eq!(
got,
[
"kv.Entry",
"kv.KvError",
"kv.Listener",
"kv.Store",
"kv.Store.get",
"kv.Store.open",
"kv.stats.Kind",
"kv.version"
]
);
assert!(table.windows(2).all(|w| w[0].id < w[1].id));
let get = table.iter().find(|e| e.path == "kv.Store.get").unwrap();
assert_eq!(get.id, fnv1a64(b"kv.Store.get"));
assert_eq!(
get.hash,
fnv1a64(b"method get(key: string) -> bytes? throws")
);
let listener = table.iter().find(|e| e.path == "kv.Listener").unwrap();
assert_eq!(
listener.hash,
fnv1a64(b"callback Listener {on_put(entry: Entry) -> string throws}")
);
}
fn hash_of(model: &Model, path: &str) -> u64 {
let root = model.roots().next().unwrap();
entries(model, root)
.into_iter()
.find(|e| e.path == path)
.unwrap()
.hash
}
#[test]
fn prose_and_sibling_order_are_excluded() {
let a = model(KV);
let edited = KV
.replace("doc: The version.", "doc: Changed prose.")
.replace("message: missing", "message: gone");
let b = model(&edited);
assert_eq!(
entries(&a, a.roots().next().unwrap()),
entries(&b, b.roots().next().unwrap())
);
let swapped = KV.replace(
"[{ name: Hot, value: 0 }, { name: Cold, value: 1 }]",
"[{ name: Cold, value: 1 }, { name: Hot, value: 0 }]",
);
let c = model(&swapped);
assert_ne!(hash_of(&a, "kv.stats.Kind"), hash_of(&c, "kv.stats.Kind"));
assert_eq!(hash_of(&a, "kv.Entry"), hash_of(&c, "kv.Entry"));
}
#[test]
fn abi_changes_change_only_their_entry() {
let a = model(KV);
let b = model(&KV.replace(
"return: \"bytes?\", throws: true",
"return: bytes, throws: true",
));
assert_ne!(hash_of(&a, "kv.Store.get"), hash_of(&b, "kv.Store.get"));
assert_eq!(hash_of(&a, "kv.Store.open"), hash_of(&b, "kv.Store.open"));
assert_eq!(hash_of(&a, "kv.Store"), hash_of(&b, "kv.Store"));
}
#[test]
fn fnv_is_pinned() {
assert_eq!(fnv1a64(b""), 0xcbf2_9ce4_8422_2325);
assert_eq!(fnv1a64(b"a"), 0xaf63_dc4c_8601_ec8c);
}
}