1use std::collections::{BTreeMap, BTreeSet};
9
10use crate::error::DiagnosticKind;
11
12#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
24pub struct Import {
25 pub module: String,
27 pub export: String,
29}
30
31impl Import {
32 pub fn new(module: impl Into<String>, export: impl Into<String>) -> Self {
34 Self {
35 module: module.into(),
36 export: export.into(),
37 }
38 }
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Hash)]
47pub enum CodecExpr {
48 Runtime(&'static str),
51 Import(Import),
54 TypeRef(String),
57 Call(Box<CodecExpr>, Vec<CodecExpr>),
59 Object(Vec<(String, CodecExpr)>),
62 Array(Vec<CodecExpr>),
64 LitInt(u64),
66 Param(usize),
69 Raw(String),
72}
73
74impl CodecExpr {
75 pub fn runtime(name: &'static str) -> Self {
77 CodecExpr::Runtime(name)
78 }
79
80 pub fn import_from(module: impl Into<String>, export: impl Into<String>) -> Self {
82 CodecExpr::Import(Import::new(module, export))
83 }
84
85 pub fn type_ref(name: impl Into<String>) -> Self {
87 CodecExpr::TypeRef(name.into())
88 }
89
90 pub fn call(callee: CodecExpr, args: impl IntoIterator<Item = CodecExpr>) -> Self {
92 CodecExpr::Call(Box::new(callee), args.into_iter().collect())
93 }
94
95 pub fn object(
97 entries: impl IntoIterator<Item = (impl Into<String>, CodecExpr)>,
98 ) -> Self {
99 CodecExpr::Object(entries.into_iter().map(|(k, v)| (k.into(), v)).collect())
100 }
101
102 pub fn array(elements: impl IntoIterator<Item = CodecExpr>) -> Self {
104 CodecExpr::Array(elements.into_iter().collect())
105 }
106
107 pub fn raw(ts: impl Into<String>) -> Self {
109 CodecExpr::Raw(ts.into())
110 }
111
112 pub fn substitute(&self, args: &[CodecExpr]) -> CodecExpr {
117 match self {
118 CodecExpr::Param(i) => args.get(*i).cloned().unwrap_or(CodecExpr::Param(*i)),
119 CodecExpr::Call(callee, call_args) => CodecExpr::Call(
120 Box::new(callee.substitute(args)),
121 call_args.iter().map(|a| a.substitute(args)).collect(),
122 ),
123 CodecExpr::Object(entries) => CodecExpr::Object(
124 entries
125 .iter()
126 .map(|(k, v)| (k.clone(), v.substitute(args)))
127 .collect(),
128 ),
129 CodecExpr::Array(elements) => {
130 CodecExpr::Array(elements.iter().map(|e| e.substitute(args)).collect())
131 }
132 other => other.clone(),
133 }
134 }
135
136 pub fn visit(&self, f: &mut impl FnMut(&CodecExpr)) {
141 f(self);
142 match self {
143 CodecExpr::Call(callee, args) => {
144 callee.visit(f);
145 for arg in args {
146 arg.visit(f);
147 }
148 }
149 CodecExpr::Object(entries) => {
150 for (_, v) in entries {
151 v.visit(f);
152 }
153 }
154 CodecExpr::Array(elements) => {
155 for element in elements {
156 element.visit(f);
157 }
158 }
159 _ => {}
160 }
161 }
162
163 pub fn max_param(&self) -> Option<usize> {
165 let mut max: Option<usize> = None;
166 self.visit(&mut |node| {
167 if let CodecExpr::Param(i) = node {
168 max = Some(max.map_or(*i, |m| m.max(*i)));
169 }
170 });
171 max
172 }
173
174 pub(crate) fn collect_imports(&self, into: &mut BTreeSet<Import>) {
176 self.visit(&mut |node| {
177 if let CodecExpr::Import(import) = node {
178 into.insert(import.clone());
179 }
180 });
181 }
182
183 pub(crate) fn collect_type_refs(&self, into: &mut BTreeSet<String>) {
185 self.visit(&mut |node| {
186 if let CodecExpr::TypeRef(name) = node {
187 into.insert(name.clone());
188 }
189 });
190 }
191
192 pub fn render(
198 &self,
199 archived_names: &BTreeMap<String, String>,
200 ) -> Result<String, DiagnosticKind> {
201 match self {
202 CodecExpr::Runtime(name) => Ok(format!("r.{name}")),
203 CodecExpr::Import(import) => Ok(import.export.clone()),
204 CodecExpr::TypeRef(name) => archived_names.get(name).cloned().ok_or_else(|| {
205 DiagnosticKind::UnresolvedTypeRef { name: name.clone() }
206 }),
207 CodecExpr::Call(callee, args) => {
208 let callee = callee.render(archived_names)?;
209 let args = args
210 .iter()
211 .map(|a| a.render(archived_names))
212 .collect::<Result<Vec<_>, _>>()?;
213 Ok(format!("{}({})", callee, args.join(", ")))
214 }
215 CodecExpr::Object(entries) => {
216 if entries.is_empty() {
217 return Ok("{}".to_string());
218 }
219 let entries = entries
220 .iter()
221 .map(|(k, v)| Ok(format!("{}: {}", k, v.render(archived_names)?)))
222 .collect::<Result<Vec<_>, DiagnosticKind>>()?;
223 Ok(format!("{{ {} }}", entries.join(", ")))
224 }
225 CodecExpr::Array(elements) => {
226 let elements = elements
227 .iter()
228 .map(|e| e.render(archived_names))
229 .collect::<Result<Vec<_>, _>>()?;
230 Ok(format!("[{}]", elements.join(", ")))
231 }
232 CodecExpr::LitInt(n) => Ok(n.to_string()),
233 CodecExpr::Param(i) => panic!(
234 "CodecExpr::Param({i}) escaped template instantiation; registry templates \
235 must be instantiated before rendering"
236 ),
237 CodecExpr::Raw(ts) => Ok(ts.clone()),
238 }
239 }
240}
241
242pub mod codec {
254 use super::CodecExpr;
255
256 pub fn u8() -> CodecExpr {
258 CodecExpr::runtime("u8")
259 }
260 pub fn i8() -> CodecExpr {
262 CodecExpr::runtime("i8")
263 }
264 pub fn u16() -> CodecExpr {
266 CodecExpr::runtime("u16")
267 }
268 pub fn i16() -> CodecExpr {
270 CodecExpr::runtime("i16")
271 }
272 pub fn u32() -> CodecExpr {
274 CodecExpr::runtime("u32")
275 }
276 pub fn i32() -> CodecExpr {
278 CodecExpr::runtime("i32")
279 }
280 pub fn u64() -> CodecExpr {
282 CodecExpr::runtime("u64")
283 }
284 pub fn i64() -> CodecExpr {
286 CodecExpr::runtime("i64")
287 }
288 pub fn f32() -> CodecExpr {
290 CodecExpr::runtime("f32")
291 }
292 pub fn f64() -> CodecExpr {
294 CodecExpr::runtime("f64")
295 }
296 pub fn bool_() -> CodecExpr {
298 CodecExpr::runtime("bool")
299 }
300 pub fn char_() -> CodecExpr {
302 CodecExpr::runtime("char")
303 }
304 pub fn unit() -> CodecExpr {
306 CodecExpr::runtime("unit")
307 }
308 pub fn string() -> CodecExpr {
310 CodecExpr::runtime("string")
311 }
312 pub fn vec(inner: CodecExpr) -> CodecExpr {
314 CodecExpr::call(CodecExpr::runtime("vec"), [inner])
315 }
316 pub fn option(inner: CodecExpr) -> CodecExpr {
318 CodecExpr::call(CodecExpr::runtime("option"), [inner])
319 }
320 pub fn boxed(inner: CodecExpr) -> CodecExpr {
322 CodecExpr::call(CodecExpr::runtime("box"), [inner])
323 }
324 pub fn rc(inner: CodecExpr) -> CodecExpr {
326 CodecExpr::call(CodecExpr::runtime("rc"), [inner])
327 }
328 pub fn weak(inner: CodecExpr) -> CodecExpr {
330 CodecExpr::call(CodecExpr::runtime("weak"), [inner])
331 }
332 pub fn array(inner: CodecExpr, len: u64) -> CodecExpr {
334 CodecExpr::call(CodecExpr::runtime("array"), [inner, CodecExpr::LitInt(len)])
335 }
336 pub fn tuple(elems: impl IntoIterator<Item = CodecExpr>) -> CodecExpr {
338 let elems: Vec<_> = elems.into_iter().collect();
339 if elems.is_empty() {
340 unit()
341 } else {
342 CodecExpr::call(CodecExpr::runtime("tuple"), elems)
343 }
344 }
345 pub fn named(rust_name: impl Into<String>) -> CodecExpr {
348 CodecExpr::type_ref(rust_name)
349 }
350}
351
352pub fn generate_import_block<'a>(
361 exprs: impl IntoIterator<Item = &'a CodecExpr>,
362) -> Result<String, Vec<DiagnosticKind>> {
363 let mut imports: BTreeSet<Import> = BTreeSet::new();
364 for expr in exprs {
365 expr.collect_imports(&mut imports);
366 }
367
368 let mut export_modules: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
370 for import in &imports {
371 export_modules
372 .entry(&import.export)
373 .or_default()
374 .insert(&import.module);
375 }
376 let conflicts: Vec<DiagnosticKind> = export_modules
377 .iter()
378 .filter(|(_, modules)| modules.len() > 1)
379 .map(|(export, modules)| DiagnosticKind::ImportConflict {
380 export: export.to_string(),
381 modules: modules.iter().map(|m| m.to_string()).collect(),
382 })
383 .collect();
384 if !conflicts.is_empty() {
385 return Err(conflicts);
386 }
387
388 let mut by_module: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
389 for import in &imports {
390 by_module
391 .entry(&import.module)
392 .or_default()
393 .push(&import.export);
394 }
395
396 let mut output = String::from("import * as r from 'rkyv-js';\n");
397 for (module, exports) in by_module {
398 output.push_str(&format!(
399 "import {{ {} }} from '{}';\n",
400 exports.join(", "),
401 module
402 ));
403 }
404 Ok(output)
405}
406
407#[cfg(test)]
408mod tests {
409 use super::*;
410
411 fn render(expr: &CodecExpr) -> String {
412 expr.render(&BTreeMap::new()).unwrap()
413 }
414
415 #[test]
416 fn renders_primitives() {
417 assert_eq!(render(&codec::u8()), "r.u8");
418 assert_eq!(render(&codec::u64()), "r.u64");
419 assert_eq!(render(&codec::bool_()), "r.bool");
420 assert_eq!(render(&codec::char_()), "r.char");
421 assert_eq!(render(&codec::unit()), "r.unit");
422 assert_eq!(render(&codec::string()), "r.string");
423 }
424
425 #[test]
426 fn renders_containers() {
427 assert_eq!(render(&codec::vec(codec::u32())), "r.vec(r.u32)");
428 assert_eq!(render(&codec::option(codec::string())), "r.option(r.string)");
429 assert_eq!(render(&codec::boxed(codec::u64())), "r.box(r.u64)");
430 assert_eq!(render(&codec::rc(codec::string())), "r.rc(r.string)");
431 assert_eq!(render(&codec::weak(codec::u32())), "r.weak(r.u32)");
432 assert_eq!(render(&codec::array(codec::u16(), 4)), "r.array(r.u16, 4)");
433 assert_eq!(
434 render(&codec::tuple([codec::u8(), codec::string()])),
435 "r.tuple(r.u8, r.string)"
436 );
437 assert_eq!(render(&codec::tuple([])), "r.unit");
438 }
439
440 #[test]
441 fn renders_nested() {
442 let expr = codec::vec(codec::option(codec::vec(codec::u16())));
443 assert_eq!(render(&expr), "r.vec(r.option(r.vec(r.u16)))");
444 }
445
446 #[test]
447 fn renders_object() {
448 let expr = CodecExpr::object([("a", codec::u8()), ("b", codec::u32())]);
449 assert_eq!(render(&expr), "{ a: r.u8, b: r.u32 }");
450 assert_eq!(render(&CodecExpr::Object(Vec::new())), "{}");
451 }
452
453 #[test]
454 fn renders_array() {
455 let expr = CodecExpr::array([codec::u8(), codec::string()]);
456 assert_eq!(render(&expr), "[r.u8, r.string]");
457 assert_eq!(render(&CodecExpr::Array(Vec::new())), "[]");
458 let template = CodecExpr::array([CodecExpr::Param(0)]);
459 assert_eq!(render(&template.substitute(&[codec::u32()])), "[r.u32]");
460 }
461
462 #[test]
463 fn renders_import_and_raw() {
464 let expr = CodecExpr::call(
465 CodecExpr::import_from("rkyv-js/lib/hashmap", "hashMap"),
466 [codec::string(), codec::u32()],
467 );
468 assert_eq!(render(&expr), "hashMap(r.string, r.u32)");
469 assert_eq!(render(&CodecExpr::raw("myCustom(r.u8)")), "myCustom(r.u8)");
470 }
471
472 #[test]
473 fn renders_type_ref_via_map() {
474 let mut names = BTreeMap::new();
475 names.insert("Point".to_string(), "ArchivedPoint".to_string());
476 let expr = codec::vec(codec::named("Point"));
477 assert_eq!(expr.render(&names).unwrap(), "r.vec(ArchivedPoint)");
478 }
479
480 #[test]
481 fn missing_type_ref_is_an_error() {
482 let expr = codec::named("Missing");
483 let err = expr.render(&BTreeMap::new()).unwrap_err();
484 assert!(matches!(
485 err,
486 DiagnosticKind::UnresolvedTypeRef { ref name } if name == "Missing"
487 ));
488 }
489
490 #[test]
491 fn substitute_replaces_params() {
492 let template = codec::vec(CodecExpr::Param(0));
493 let out = template.substitute(&[codec::u32()]);
494 assert_eq!(render(&out), "r.vec(r.u32)");
495
496 let template = CodecExpr::call(
497 CodecExpr::import_from("m", "pair"),
498 [CodecExpr::Param(0), CodecExpr::Param(1)],
499 );
500 let out = template.substitute(&[codec::string(), codec::u8()]);
501 assert_eq!(render(&out), "pair(r.string, r.u8)");
502 }
503
504 #[test]
505 fn substitute_inside_objects() {
506 let template = CodecExpr::object([("inner", CodecExpr::Param(0))]);
507 let out = template.substitute(&[codec::u8()]);
508 assert_eq!(render(&out), "{ inner: r.u8 }");
509 }
510
511 #[test]
512 fn max_param_walks_the_tree() {
513 assert_eq!(codec::u8().max_param(), None);
514 let expr = CodecExpr::call(
515 CodecExpr::runtime("x"),
516 [CodecExpr::Param(0), codec::vec(CodecExpr::Param(3))],
517 );
518 assert_eq!(expr.max_param(), Some(3));
519 }
520
521 #[test]
522 fn raw_is_never_inspected() {
523 let expr = CodecExpr::raw("hashMap(r.u8)");
524 let mut imports = BTreeSet::new();
525 expr.collect_imports(&mut imports);
526 assert!(imports.is_empty());
527 let mut refs = BTreeSet::new();
528 expr.collect_type_refs(&mut refs);
529 assert!(refs.is_empty());
530 assert_eq!(expr.max_param(), None);
531 }
532
533 #[test]
534 fn import_block_groups_and_sorts() {
535 let exprs = [
536 CodecExpr::import_from("rkyv-js/lib/indexmap", "indexSet"),
537 CodecExpr::import_from("rkyv-js/lib/indexmap", "indexMap"),
538 CodecExpr::import_from("rkyv-js/lib/bytes", "bytes"),
539 codec::u8(),
540 ];
541 let block = generate_import_block(exprs.iter()).unwrap();
542 assert_eq!(
543 block,
544 "import * as r from 'rkyv-js';\n\
545 import { bytes } from 'rkyv-js/lib/bytes';\n\
546 import { indexMap, indexSet } from 'rkyv-js/lib/indexmap';\n"
547 );
548 }
549
550 #[test]
551 fn import_block_dedups() {
552 let exprs = [
553 CodecExpr::import_from("rkyv-js/lib/uuid", "uuid"),
554 CodecExpr::import_from("rkyv-js/lib/uuid", "uuid"),
555 ];
556 let block = generate_import_block(exprs.iter()).unwrap();
557 assert_eq!(
558 block,
559 "import * as r from 'rkyv-js';\nimport { uuid } from 'rkyv-js/lib/uuid';\n"
560 );
561 }
562
563 #[test]
564 fn import_block_detects_conflicts() {
565 let exprs = [
566 CodecExpr::import_from("pkg-a", "codec"),
567 CodecExpr::import_from("pkg-b", "codec"),
568 ];
569 let errs = generate_import_block(exprs.iter()).unwrap_err();
570 assert_eq!(errs.len(), 1);
571 assert!(matches!(
572 &errs[0],
573 DiagnosticKind::ImportConflict { export, modules }
574 if export == "codec" && modules == &vec!["pkg-a".to_string(), "pkg-b".to_string()]
575 ));
576 }
577
578 #[test]
579 #[should_panic(expected = "escaped template instantiation")]
580 fn rendering_a_param_panics() {
581 let _ = CodecExpr::Param(0).render(&BTreeMap::new());
582 }
583}