1use std::fmt::Write;
38
39use crate::model::{FieldBinding, FnBinding, Model, ModuleBinding, ParamBinding};
40use crate::ty::Ty;
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct ContractEntry {
45 pub path: String,
48 pub id: u64,
50 pub hash: u64,
52}
53
54#[must_use]
57pub const fn fnv1a64(data: &[u8]) -> u64 {
58 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
59 let mut i = 0;
60 while i < data.len() {
61 hash ^= data[i] as u64;
62 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
63 i += 1;
64 }
65 hash
66}
67
68#[must_use]
72pub fn entries(model: &Model, root: &ModuleBinding) -> Vec<ContractEntry> {
73 let mut out = Vec::new();
74 for m in model
75 .modules
76 .iter()
77 .filter(|m| m.segments.first() == root.segments.first())
78 {
79 module_entries(m, &mut out);
80 }
81 out.sort_by_key(|e| e.id);
82 out
83}
84
85fn entry(path: String, signature: &str) -> ContractEntry {
86 ContractEntry {
87 id: fnv1a64(path.as_bytes()),
88 hash: fnv1a64(signature.as_bytes()),
89 path,
90 }
91}
92
93fn module_entries(m: &ModuleBinding, out: &mut Vec<ContractEntry>) {
94 let dot = &m.dot_path;
95 for f in &m.functions {
96 out.push(entry(
97 format!("{dot}.{}", f.name),
98 &callable_signature("function", f),
99 ));
100 }
101 for i in &m.interfaces {
102 let owner = format!("{dot}.{}", i.name);
103 out.push(entry(owner.clone(), &format!("interface {}", i.name)));
104 for (kind, members) in [
105 ("constructor", &i.constructors),
106 ("method", &i.methods),
107 ("static", &i.statics),
108 ] {
109 for f in members {
110 out.push(entry(
111 format!("{owner}.{}", f.name),
112 &callable_signature(kind, f),
113 ));
114 }
115 }
116 }
117 for s in &m.structs {
118 out.push(entry(
119 format!("{dot}.{}", s.name),
120 &format!("record {} {{{}}}", s.name, fields(&s.fields)),
121 ));
122 }
123 for e in &m.enums {
124 let variants: Vec<String> = e
125 .variants
126 .iter()
127 .map(|v| case(&v.name, v.value, &v.fields))
128 .collect();
129 out.push(entry(
130 format!("{dot}.{}", e.name),
131 &format!("enum {} {{{}}}", e.name, variants.join(", ")),
132 ));
133 }
134 for c in &m.callback_interfaces {
135 let methods: Vec<String> = c
136 .methods
137 .iter()
138 .map(|cm| {
139 let mut sig = format!(
140 "{}({}) -> {}",
141 cm.name,
142 params(&cm.params),
143 ret(cm.ret.as_ref())
144 );
145 if cm.throws {
146 sig.push_str(" throws");
147 }
148 sig
149 })
150 .collect();
151 out.push(entry(
152 format!("{dot}.{}", c.name),
153 &format!("callback {} {{{}}}", c.name, methods.join("; ")),
154 ));
155 }
156 if let Some(d) = &m.errors {
157 let codes: Vec<String> = d
158 .codes
159 .iter()
160 .map(|c| case(&c.name, c.value, &c.fields))
161 .collect();
162 out.push(entry(
163 format!("{dot}.{}", d.name),
164 &format!("errors {} {{{}}}", d.name, codes.join(", ")),
165 ));
166 }
167}
168
169fn params(params: &[ParamBinding]) -> String {
170 let all: Vec<String> = params
171 .iter()
172 .map(|p| format!("{}: {}", p.name, p.ty))
173 .collect();
174 all.join(", ")
175}
176
177fn fields(fields: &[FieldBinding]) -> String {
178 let all: Vec<String> = fields
179 .iter()
180 .map(|f| format!("{}: {}", f.name, f.ty))
181 .collect();
182 all.join(", ")
183}
184
185fn ret(ty: Option<&Ty>) -> String {
186 ty.map_or_else(|| "void".to_string(), ToString::to_string)
187}
188
189fn case(name: &str, value: i32, case_fields: &[FieldBinding]) -> String {
191 let mut out = format!("{name} = {value}");
192 if !case_fields.is_empty() {
193 let _ = write!(out, " {{{}}}", fields(case_fields));
194 }
195 out
196}
197
198fn callable_signature(kind: &str, f: &FnBinding) -> String {
199 let mut sig = format!(
200 "{kind} {}({}) -> {}",
201 f.name,
202 params(&f.params),
203 ret(f.ret.as_ref())
204 );
205 if f.throws {
206 sig.push_str(" throws");
207 }
208 if f.is_async() {
209 sig.push_str(" async");
210 }
211 if f.cancellable {
212 sig.push_str(" cancellable");
213 }
214 sig
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use crate::ir::Api;
221 use crate::pkg::Identity;
222
223 fn model(yaml: &str) -> Model {
224 let api: Api = serde_yaml::from_str(yaml).unwrap();
225 crate::validate::validate(&api, &Identity::named("kv"), None).unwrap()
226 }
227
228 const KV: &str = r#"
229version: "0.11.0"
230modules:
231 - name: kv
232 doc: Key-value storage.
233 errors:
234 name: KvError
235 codes:
236 - { name: NotFound, code: 1, message: missing, fields: [{ name: key, type: string }] }
237 structs:
238 - name: Entry
239 fields: [{ name: key, type: string }, { name: value, type: bytes }]
240 interfaces:
241 - name: Store
242 constructors: [{ name: open, params: [{ name: path, type: string }], throws: true }]
243 methods:
244 - { name: get, params: [{ name: key, type: string }], return: "bytes?", throws: true }
245 callback_interfaces:
246 - name: Listener
247 methods:
248 - { name: on_put, params: [{ name: entry, type: Entry }], return: string, throws: true }
249 functions:
250 - { name: version, params: [], return: string, doc: The version. }
251 modules:
252 - name: stats
253 enums:
254 - name: Kind
255 variants: [{ name: Hot, value: 0 }, { name: Cold, value: 1 }]
256"#;
257
258 fn paths(model: &Model) -> Vec<String> {
259 let root = model.roots().next().unwrap();
260 entries(model, root).into_iter().map(|e| e.path).collect()
261 }
262
263 #[test]
264 fn one_entry_per_declaration_sorted_by_id() {
265 let model = model(KV);
266 let root = model.roots().next().unwrap();
267 let table = entries(&model, root);
268 let mut got = paths(&model);
269 got.sort();
270 assert_eq!(
271 got,
272 [
273 "kv.Entry",
274 "kv.KvError",
275 "kv.Listener",
276 "kv.Store",
277 "kv.Store.get",
278 "kv.Store.open",
279 "kv.stats.Kind",
280 "kv.version"
281 ]
282 );
283 assert!(table.windows(2).all(|w| w[0].id < w[1].id));
284 let get = table.iter().find(|e| e.path == "kv.Store.get").unwrap();
285 assert_eq!(get.id, fnv1a64(b"kv.Store.get"));
286 assert_eq!(
287 get.hash,
288 fnv1a64(b"method get(key: string) -> bytes? throws")
289 );
290 let listener = table.iter().find(|e| e.path == "kv.Listener").unwrap();
291 assert_eq!(
292 listener.hash,
293 fnv1a64(b"callback Listener {on_put(entry: Entry) -> string throws}")
294 );
295 }
296
297 fn hash_of(model: &Model, path: &str) -> u64 {
298 let root = model.roots().next().unwrap();
299 entries(model, root)
300 .into_iter()
301 .find(|e| e.path == path)
302 .unwrap()
303 .hash
304 }
305
306 #[test]
307 fn prose_and_sibling_order_are_excluded() {
308 let a = model(KV);
309 let edited = KV
310 .replace("doc: The version.", "doc: Changed prose.")
311 .replace("message: missing", "message: gone");
312 let b = model(&edited);
313 assert_eq!(
314 entries(&a, a.roots().next().unwrap()),
315 entries(&b, b.roots().next().unwrap())
316 );
317 let swapped = KV.replace(
320 "[{ name: Hot, value: 0 }, { name: Cold, value: 1 }]",
321 "[{ name: Cold, value: 1 }, { name: Hot, value: 0 }]",
322 );
323 let c = model(&swapped);
324 assert_ne!(hash_of(&a, "kv.stats.Kind"), hash_of(&c, "kv.stats.Kind"));
325 assert_eq!(hash_of(&a, "kv.Entry"), hash_of(&c, "kv.Entry"));
326 }
327
328 #[test]
329 fn abi_changes_change_only_their_entry() {
330 let a = model(KV);
331 let b = model(&KV.replace(
332 "return: \"bytes?\", throws: true",
333 "return: bytes, throws: true",
334 ));
335 assert_ne!(hash_of(&a, "kv.Store.get"), hash_of(&b, "kv.Store.get"));
336 assert_eq!(hash_of(&a, "kv.Store.open"), hash_of(&b, "kv.Store.open"));
337 assert_eq!(hash_of(&a, "kv.Store"), hash_of(&b, "kv.Store"));
338 }
339
340 #[test]
341 fn fnv_is_pinned() {
342 assert_eq!(fnv1a64(b""), 0xcbf2_9ce4_8422_2325);
345 assert_eq!(fnv1a64(b"a"), 0xaf63_dc4c_8601_ec8c);
346 }
347}