1use alloc::format;
17use alloc::string::{String, ToString};
18use alloc::vec::Vec;
19
20use zerodds_idl::ast::{
21 AttrDecl, ComponentDef, ComponentExport, EventDef, Export, HomeDef, Identifier, InterfaceDef,
22 InterfaceKind, OpDecl, ParamAttribute, ParamDecl, PrimitiveType, ScopedName, StringType,
23 TypeSpec, ValueKind,
24};
25use zerodds_idl::errors::Span;
26
27#[derive(Debug, Clone, PartialEq)]
31pub struct ComponentEquivalent {
32 pub equivalent_interface: InterfaceDef,
34 pub event_consumer_interfaces: Vec<InterfaceDef>,
38}
39
40#[derive(Debug, Clone, PartialEq)]
43pub struct HomeEquivalent {
44 pub explicit: InterfaceDef,
46 pub implicit: InterfaceDef,
49 pub equivalent: InterfaceDef,
51}
52
53#[derive(Debug, Clone, PartialEq)]
62pub struct EventTypeEquivalent {
63 pub valuetype_name: Identifier,
66 pub valuetype_bases: Vec<ScopedName>,
70 pub consumer_interface: InterfaceDef,
72}
73
74#[must_use]
85pub fn transform_component(comp: &ComponentDef) -> ComponentEquivalent {
86 let span = Span::SYNTHETIC;
87
88 let bases = component_bases(comp, span);
90
91 let mut exports: Vec<Export> = Vec::new();
92 let mut event_consumer_ifaces: Vec<InterfaceDef> = Vec::new();
93 let mut emitted_consumers: Vec<String> = Vec::new();
94
95 for export in &comp.body {
96 match export {
97 ComponentExport::Provides {
98 type_spec, name, ..
99 } => {
100 exports.push(Export::Op(provide_facet_op(name, type_spec, span)));
101 }
102 ComponentExport::Uses {
103 type_spec,
104 name,
105 multiple,
106 ..
107 } => {
108 exports.extend(uses_ops(name, type_spec, *multiple, span));
109 }
110 ComponentExport::Attribute(attr) => {
111 exports.push(Export::Attr(attr_to_attr_decl(attr, span)));
112 }
113 ComponentExport::Emits {
114 type_spec, name, ..
115 } => {
116 ensure_consumer_interface(
117 type_spec,
118 span,
119 &mut event_consumer_ifaces,
120 &mut emitted_consumers,
121 );
122 exports.extend(emits_ops(name, type_spec, span));
123 }
124 ComponentExport::Publishes {
125 type_spec, name, ..
126 } => {
127 ensure_consumer_interface(
128 type_spec,
129 span,
130 &mut event_consumer_ifaces,
131 &mut emitted_consumers,
132 );
133 exports.extend(publishes_ops(name, type_spec, span));
134 }
135 ComponentExport::Consumes {
136 type_spec, name, ..
137 } => {
138 ensure_consumer_interface(
139 type_spec,
140 span,
141 &mut event_consumer_ifaces,
142 &mut emitted_consumers,
143 );
144 exports.push(Export::Op(consumes_op(name, type_spec, span)));
145 }
146 ComponentExport::Port { .. } => {
147 }
154 }
155 }
156
157 let equivalent_interface = InterfaceDef {
158 kind: InterfaceKind::Plain,
159 name: comp.name.clone(),
160 bases,
161 exports,
162 annotations: Vec::new(),
163 span,
164 };
165
166 ComponentEquivalent {
167 equivalent_interface,
168 event_consumer_interfaces: event_consumer_ifaces,
169 }
170}
171
172#[must_use]
175pub fn transform_home(home: &HomeDef) -> HomeEquivalent {
176 let span = Span::SYNTHETIC;
177 let h = &home.name.text;
178
179 let mut explicit_bases = alloc::vec![scoped(&["Components", "CCMHome"], span)];
181 explicit_bases.extend(home.supports.iter().cloned());
182 if let Some(base) = &home.base {
183 explicit_bases = alloc::vec![base_explicit_name(base, span)];
186 explicit_bases.extend(home.supports.iter().cloned());
187 }
188
189 let explicit = InterfaceDef {
190 kind: InterfaceKind::Plain,
191 name: Identifier::new(format!("{h}Explicit"), span),
192 bases: explicit_bases,
193 exports: Vec::new(),
200 annotations: Vec::new(),
201 span,
202 };
203
204 let implicit = if let Some(pk) = &home.primary_key {
206 build_keyed_implicit(h, &home.manages, pk, span)
207 } else {
208 build_keyless_implicit(h, &home.manages, span)
209 };
210
211 let equivalent = InterfaceDef {
213 kind: InterfaceKind::Plain,
214 name: home.name.clone(),
215 bases: alloc::vec![
216 ScopedName::single(Identifier::new(format!("{h}Explicit"), span)),
217 ScopedName::single(Identifier::new(format!("{h}Implicit"), span)),
218 ],
219 exports: Vec::new(),
220 annotations: Vec::new(),
221 span,
222 };
223
224 HomeEquivalent {
225 explicit,
226 implicit,
227 equivalent,
228 }
229}
230
231#[must_use]
233pub fn transform_event_type(et: &EventDef) -> EventTypeEquivalent {
234 let span = Span::SYNTHETIC;
235
236 let mut bases: Vec<ScopedName> = Vec::new();
241 let mut has_inheritance = false;
242 if let Some(inherit) = &et.inheritance {
243 if !inherit.bases.is_empty() {
244 has_inheritance = true;
245 bases.extend(inherit.bases.iter().cloned());
246 }
247 }
248 if !has_inheritance {
249 bases.push(scoped(&["Components", "EventBase"], span));
251 }
252
253 let consumer_iface_name = format!("{}Consumer", et.name.text);
255 let push_op = OpDecl {
256 name: Identifier::new(format!("push_{}", et.name.text), span),
257 oneway: false,
258 context: Vec::new(),
259 return_type: None,
260 params: alloc::vec![ParamDecl {
261 attribute: ParamAttribute::In,
262 type_spec: TypeSpec::Scoped(ScopedName::single(et.name.clone())),
263 name: Identifier::new(format!("the_{}", lowercase_first(&et.name.text)), span),
264 annotations: Vec::new(),
265 span,
266 }],
267 raises: Vec::new(),
268 annotations: Vec::new(),
269 span,
270 };
271 let consumer_bases = if has_inheritance {
272 et.inheritance.as_ref().map_or_else(
275 || alloc::vec![scoped(&["Components", "EventConsumerBase"], span)],
276 |inh| {
277 inh.bases
278 .iter()
279 .map(|b| {
280 let last = b
281 .parts
282 .last()
283 .map_or("Base", |i| i.text.as_str())
284 .to_string();
285 ScopedName::single(Identifier::new(format!("{last}Consumer"), span))
286 })
287 .collect()
288 },
289 )
290 } else {
291 alloc::vec![scoped(&["Components", "EventConsumerBase"], span)]
292 };
293
294 let consumer_iface = InterfaceDef {
295 kind: InterfaceKind::Plain,
296 name: Identifier::new(consumer_iface_name, span),
297 bases: consumer_bases,
298 exports: alloc::vec![Export::Op(push_op)],
299 annotations: Vec::new(),
300 span,
301 };
302
303 let _ = ValueKind::Concrete; EventTypeEquivalent {
306 valuetype_name: et.name.clone(),
307 valuetype_bases: bases,
308 consumer_interface: consumer_iface,
309 }
310}
311
312fn component_bases(comp: &ComponentDef, span: Span) -> Vec<ScopedName> {
317 let mut bases = Vec::new();
319 if let Some(b) = &comp.base {
320 bases.push(b.clone());
321 } else {
322 bases.push(scoped(&["Components", "CCMObject"], span));
323 }
324 bases.extend(comp.supports.iter().cloned());
325 bases
326}
327
328fn provide_facet_op(name: &Identifier, iface_type: &ScopedName, span: Span) -> OpDecl {
330 OpDecl {
331 name: Identifier::new(format!("provide_{}", name.text), span),
332 oneway: false,
333 context: Vec::new(),
334 return_type: Some(TypeSpec::Scoped(iface_type.clone())),
335 params: Vec::new(),
336 raises: Vec::new(),
337 annotations: Vec::new(),
338 span,
339 }
340}
341
342fn uses_ops(name: &Identifier, iface_type: &ScopedName, multiple: bool, span: Span) -> Vec<Export> {
345 let n = &name.text;
346 let mut out = Vec::new();
347 if multiple {
348 out.push(Export::Op(OpDecl {
351 name: Identifier::new(format!("connect_{n}"), span),
352 oneway: false,
353 context: Vec::new(),
354 return_type: Some(TypeSpec::Scoped(scoped(&["Components", "Cookie"], span))),
355 params: alloc::vec![ParamDecl {
356 attribute: ParamAttribute::In,
357 type_spec: TypeSpec::Scoped(iface_type.clone()),
358 name: Identifier::new("connection", span),
359 annotations: Vec::new(),
360 span,
361 }],
362 raises: alloc::vec![
363 scoped(&["Components", "ExceededConnectionLimit"], span),
364 scoped(&["Components", "InvalidConnection"], span),
365 ],
366 annotations: Vec::new(),
367 span,
368 }));
369 out.push(Export::Op(OpDecl {
371 name: Identifier::new(format!("disconnect_{n}"), span),
372 oneway: false,
373 context: Vec::new(),
374 return_type: Some(TypeSpec::Scoped(iface_type.clone())),
375 params: alloc::vec![ParamDecl {
376 attribute: ParamAttribute::In,
377 type_spec: TypeSpec::Scoped(scoped(&["Components", "Cookie"], span)),
378 name: Identifier::new("ck", span),
379 annotations: Vec::new(),
380 span,
381 }],
382 raises: alloc::vec![scoped(&["Components", "InvalidConnection"], span)],
383 annotations: Vec::new(),
384 span,
385 }));
386 out.push(Export::Op(OpDecl {
388 name: Identifier::new(format!("get_connections_{n}"), span),
389 oneway: false,
390 context: Vec::new(),
391 return_type: Some(TypeSpec::Scoped(ScopedName::single(Identifier::new(
392 format!("{n}Connections"),
393 span,
394 )))),
395 params: Vec::new(),
396 raises: Vec::new(),
397 annotations: Vec::new(),
398 span,
399 }));
400 } else {
401 out.push(Export::Op(OpDecl {
403 name: Identifier::new(format!("connect_{n}"), span),
404 oneway: false,
405 context: Vec::new(),
406 return_type: None,
407 params: alloc::vec![ParamDecl {
408 attribute: ParamAttribute::In,
409 type_spec: TypeSpec::Scoped(iface_type.clone()),
410 name: Identifier::new("conxn", span),
411 annotations: Vec::new(),
412 span,
413 }],
414 raises: alloc::vec![
415 scoped(&["Components", "AlreadyConnected"], span),
416 scoped(&["Components", "InvalidConnection"], span),
417 ],
418 annotations: Vec::new(),
419 span,
420 }));
421 out.push(Export::Op(OpDecl {
422 name: Identifier::new(format!("disconnect_{n}"), span),
423 oneway: false,
424 context: Vec::new(),
425 return_type: Some(TypeSpec::Scoped(iface_type.clone())),
426 params: Vec::new(),
427 raises: alloc::vec![scoped(&["Components", "NoConnection"], span)],
428 annotations: Vec::new(),
429 span,
430 }));
431 out.push(Export::Op(OpDecl {
432 name: Identifier::new(format!("get_connection_{n}"), span),
433 oneway: false,
434 context: Vec::new(),
435 return_type: Some(TypeSpec::Scoped(iface_type.clone())),
436 params: Vec::new(),
437 raises: Vec::new(),
438 annotations: Vec::new(),
439 span,
440 }));
441 }
442 out
443}
444
445fn emits_ops(name: &Identifier, event_type: &ScopedName, span: Span) -> Vec<Export> {
448 let n = &name.text;
449 let consumer_type = consumer_type_of(event_type, span);
450 alloc::vec![
451 Export::Op(OpDecl {
452 name: Identifier::new(format!("connect_{n}"), span),
453 oneway: false,
454 context: Vec::new(),
455 return_type: None,
456 params: alloc::vec![ParamDecl {
457 attribute: ParamAttribute::In,
458 type_spec: TypeSpec::Scoped(consumer_type.clone()),
459 name: Identifier::new("consumer", span),
460 annotations: Vec::new(),
461 span,
462 }],
463 raises: alloc::vec![scoped(&["Components", "AlreadyConnected"], span)],
464 annotations: Vec::new(),
465 span,
466 }),
467 Export::Op(OpDecl {
468 name: Identifier::new(format!("disconnect_{n}"), span),
469 oneway: false,
470 context: Vec::new(),
471 return_type: Some(TypeSpec::Scoped(consumer_type)),
472 params: Vec::new(),
473 raises: alloc::vec![scoped(&["Components", "NoConnection"], span)],
474 annotations: Vec::new(),
475 span,
476 }),
477 ]
478}
479
480fn publishes_ops(name: &Identifier, event_type: &ScopedName, span: Span) -> Vec<Export> {
484 let n = &name.text;
485 let consumer_type = consumer_type_of(event_type, span);
486 alloc::vec![
487 Export::Op(OpDecl {
488 name: Identifier::new(format!("subscribe_{n}"), span),
489 oneway: false,
490 context: Vec::new(),
491 return_type: Some(TypeSpec::Scoped(scoped(&["Components", "Cookie"], span))),
492 params: alloc::vec![ParamDecl {
493 attribute: ParamAttribute::In,
494 type_spec: TypeSpec::Scoped(consumer_type.clone()),
495 name: Identifier::new("consumer", span),
496 annotations: Vec::new(),
497 span,
498 }],
499 raises: alloc::vec![scoped(&["Components", "ExceededConnectionLimit"], span)],
500 annotations: Vec::new(),
501 span,
502 }),
503 Export::Op(OpDecl {
504 name: Identifier::new(format!("unsubscribe_{n}"), span),
505 oneway: false,
506 context: Vec::new(),
507 return_type: Some(TypeSpec::Scoped(consumer_type)),
508 params: alloc::vec![ParamDecl {
509 attribute: ParamAttribute::In,
510 type_spec: TypeSpec::Scoped(scoped(&["Components", "Cookie"], span)),
511 name: Identifier::new("ck", span),
512 annotations: Vec::new(),
513 span,
514 }],
515 raises: alloc::vec![scoped(&["Components", "InvalidConnection"], span)],
516 annotations: Vec::new(),
517 span,
518 }),
519 ]
520}
521
522fn consumes_op(name: &Identifier, event_type: &ScopedName, span: Span) -> OpDecl {
525 let n = &name.text;
526 let consumer_type = consumer_type_of(event_type, span);
527 OpDecl {
528 name: Identifier::new(format!("get_consumer_{n}"), span),
529 oneway: false,
530 context: Vec::new(),
531 return_type: Some(TypeSpec::Scoped(consumer_type)),
532 params: Vec::new(),
533 raises: Vec::new(),
534 annotations: Vec::new(),
535 span,
536 }
537}
538
539fn ensure_consumer_interface(
540 event_type: &ScopedName,
541 span: Span,
542 out: &mut Vec<InterfaceDef>,
543 emitted: &mut Vec<String>,
544) {
545 let consumer_name = consumer_simple_name(event_type);
546 if emitted.iter().any(|n| n == &consumer_name) {
547 return;
548 }
549 emitted.push(consumer_name.clone());
550 let push_op = OpDecl {
551 name: Identifier::new(
552 format!(
553 "push_{}",
554 event_type.parts.last().map_or("E", |i| i.text.as_str())
555 ),
556 span,
557 ),
558 oneway: false,
559 context: Vec::new(),
560 return_type: None,
561 params: alloc::vec![ParamDecl {
562 attribute: ParamAttribute::In,
563 type_spec: TypeSpec::Scoped(event_type.clone()),
564 name: Identifier::new(
565 format!(
566 "the_{}",
567 lowercase_first(event_type.parts.last().map_or("e", |i| i.text.as_str()))
568 ),
569 span,
570 ),
571 annotations: Vec::new(),
572 span,
573 }],
574 raises: Vec::new(),
575 annotations: Vec::new(),
576 span,
577 };
578 out.push(InterfaceDef {
579 kind: InterfaceKind::Plain,
580 name: Identifier::new(consumer_name, span),
581 bases: alloc::vec![scoped(&["Components", "EventConsumerBase"], span)],
582 exports: alloc::vec![Export::Op(push_op)],
583 annotations: Vec::new(),
584 span,
585 });
586}
587
588fn consumer_type_of(event_type: &ScopedName, span: Span) -> ScopedName {
589 let mut parts = event_type.parts.clone();
590 if let Some(last) = parts.last_mut() {
591 last.text.push_str("Consumer");
592 } else {
593 parts.push(Identifier::new("EConsumer", span));
594 }
595 ScopedName {
596 absolute: event_type.absolute,
597 parts,
598 span,
599 }
600}
601
602fn consumer_simple_name(event_type: &ScopedName) -> String {
603 event_type.parts.last().map_or_else(
604 || String::from("EConsumer"),
605 |i| format!("{}Consumer", i.text),
606 )
607}
608
609fn attr_to_attr_decl(attr: &zerodds_idl::ast::AttrDcl, span: Span) -> AttrDecl {
612 AttrDecl {
613 name: attr.name.clone(),
614 type_spec: attr.type_spec.clone(),
615 readonly: attr.readonly,
616 get_raises: Vec::new(),
617 set_raises: Vec::new(),
618 annotations: Vec::new(),
619 span,
620 }
621}
622
623fn build_keyless_implicit(
624 home_name: &str,
625 component_type: &ScopedName,
626 span: Span,
627) -> InterfaceDef {
628 InterfaceDef {
632 kind: InterfaceKind::Plain,
633 name: Identifier::new(format!("{home_name}Implicit"), span),
634 bases: alloc::vec![scoped(&["Components", "KeylessCCMHome"], span)],
635 exports: alloc::vec![Export::Op(OpDecl {
636 name: Identifier::new("create", span),
637 oneway: false,
638 context: Vec::new(),
639 return_type: Some(TypeSpec::Scoped(component_type.clone())),
640 params: Vec::new(),
641 raises: alloc::vec![scoped(&["Components", "CreateFailure"], span)],
642 annotations: Vec::new(),
643 span,
644 })],
645 annotations: Vec::new(),
646 span,
647 }
648}
649
650fn build_keyed_implicit(
651 home_name: &str,
652 component_type: &ScopedName,
653 key_type: &ScopedName,
654 span: Span,
655) -> InterfaceDef {
656 let exports = alloc::vec![
660 Export::Op(OpDecl {
662 name: Identifier::new("create", span),
663 oneway: false,
664 context: Vec::new(),
665 return_type: Some(TypeSpec::Scoped(component_type.clone())),
666 params: alloc::vec![ParamDecl {
667 attribute: ParamAttribute::In,
668 type_spec: TypeSpec::Scoped(key_type.clone()),
669 name: Identifier::new("key", span),
670 annotations: Vec::new(),
671 span,
672 }],
673 raises: alloc::vec![
674 scoped(&["Components", "CreateFailure"], span),
675 scoped(&["Components", "DuplicateKeyValue"], span),
676 scoped(&["Components", "InvalidKey"], span),
677 ],
678 annotations: Vec::new(),
679 span,
680 }),
681 Export::Op(OpDecl {
683 name: Identifier::new("find_by_primary_key", span),
684 oneway: false,
685 context: Vec::new(),
686 return_type: Some(TypeSpec::Scoped(component_type.clone())),
687 params: alloc::vec![ParamDecl {
688 attribute: ParamAttribute::In,
689 type_spec: TypeSpec::Scoped(key_type.clone()),
690 name: Identifier::new("key", span),
691 annotations: Vec::new(),
692 span,
693 }],
694 raises: alloc::vec![
695 scoped(&["Components", "FinderFailure"], span),
696 scoped(&["Components", "UnknownKeyValue"], span),
697 scoped(&["Components", "InvalidKey"], span),
698 ],
699 annotations: Vec::new(),
700 span,
701 }),
702 Export::Op(OpDecl {
704 name: Identifier::new("remove", span),
705 oneway: false,
706 context: Vec::new(),
707 return_type: None,
708 params: alloc::vec![ParamDecl {
709 attribute: ParamAttribute::In,
710 type_spec: TypeSpec::Scoped(key_type.clone()),
711 name: Identifier::new("key", span),
712 annotations: Vec::new(),
713 span,
714 }],
715 raises: alloc::vec![
716 scoped(&["Components", "RemoveFailure"], span),
717 scoped(&["Components", "UnknownKeyValue"], span),
718 scoped(&["Components", "InvalidKey"], span),
719 ],
720 annotations: Vec::new(),
721 span,
722 }),
723 Export::Op(OpDecl {
725 name: Identifier::new("get_primary_key", span),
726 oneway: false,
727 context: Vec::new(),
728 return_type: Some(TypeSpec::Scoped(key_type.clone())),
729 params: alloc::vec![ParamDecl {
730 attribute: ParamAttribute::In,
731 type_spec: TypeSpec::Scoped(component_type.clone()),
732 name: Identifier::new("comp", span),
733 annotations: Vec::new(),
734 span,
735 }],
736 raises: Vec::new(),
737 annotations: Vec::new(),
738 span,
739 }),
740 ];
741
742 InterfaceDef {
743 kind: InterfaceKind::Plain,
744 name: Identifier::new(format!("{home_name}Implicit"), span),
745 bases: Vec::new(),
748 exports,
749 annotations: Vec::new(),
750 span,
751 }
752}
753
754fn base_explicit_name(base: &ScopedName, span: Span) -> ScopedName {
755 let last_text = base
756 .parts
757 .last()
758 .map_or_else(|| String::from("BaseHome"), |i| i.text.clone());
759 ScopedName {
760 absolute: base.absolute,
761 parts: alloc::vec![Identifier::new(format!("{last_text}Explicit"), span)],
762 span,
763 }
764}
765
766fn scoped(parts: &[&str], span: Span) -> ScopedName {
767 ScopedName {
768 absolute: false,
769 parts: parts
770 .iter()
771 .map(|p| Identifier::new((*p).to_string(), span))
772 .collect(),
773 span,
774 }
775}
776
777#[must_use]
779pub fn scoped_name(parts: &[&str], span: Span) -> ScopedName {
780 scoped(parts, span)
781}
782
783fn lowercase_first(s: &str) -> String {
784 let mut chars = s.chars();
785 chars.next().map_or_else(String::new, |c| {
786 let mut out: String = c.to_lowercase().collect();
787 out.push_str(chars.as_str());
788 out
789 })
790}
791
792const _: fn() = || {
794 let _ = core::marker::PhantomData::<StringType>;
795 let _ = core::marker::PhantomData::<PrimitiveType>;
796};
797
798#[cfg(test)]
799#[allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
800mod tests {
801 use super::*;
802 use zerodds_idl::ast::{ComponentDef, ComponentExport, IntegerType};
803
804 fn span() -> Span {
805 Span::SYNTHETIC
806 }
807
808 fn ident(s: &str) -> Identifier {
809 Identifier::new(s, span())
810 }
811
812 fn sn(parts: &[&str]) -> ScopedName {
813 scoped(parts, span())
814 }
815
816 fn comp(name: &str, body: Vec<ComponentExport>) -> ComponentDef {
817 ComponentDef {
818 name: ident(name),
819 base: None,
820 supports: Vec::new(),
821 body,
822 annotations: Vec::new(),
823 span: span(),
824 }
825 }
826
827 fn comp_with_supports(
828 name: &str,
829 supports: Vec<ScopedName>,
830 body: Vec<ComponentExport>,
831 ) -> ComponentDef {
832 ComponentDef {
833 name: ident(name),
834 base: None,
835 supports,
836 body,
837 annotations: Vec::new(),
838 span: span(),
839 }
840 }
841
842 fn comp_with_base(name: &str, base: ScopedName, body: Vec<ComponentExport>) -> ComponentDef {
843 ComponentDef {
844 name: ident(name),
845 base: Some(base),
846 supports: Vec::new(),
847 body,
848 annotations: Vec::new(),
849 span: span(),
850 }
851 }
852
853 fn op_names(iface: &InterfaceDef) -> Vec<String> {
854 iface
855 .exports
856 .iter()
857 .filter_map(|e| match e {
858 Export::Op(o) => Some(o.name.text.clone()),
859 _ => None,
860 })
861 .collect()
862 }
863
864 #[test]
865 fn simple_basic_component_inherits_ccmobject() {
866 let c = comp("C", alloc::vec![]);
869 let out = transform_component(&c);
870 assert_eq!(out.equivalent_interface.name.text, "C");
871 assert_eq!(out.equivalent_interface.bases.len(), 1);
872 let parts: Vec<&str> = out.equivalent_interface.bases[0]
873 .parts
874 .iter()
875 .map(|p| p.text.as_str())
876 .collect();
877 assert_eq!(parts, alloc::vec!["Components", "CCMObject"]);
878 }
879
880 #[test]
881 fn component_with_supports_inherits_ccmobject_plus_supported() {
882 let c = comp_with_supports("C", alloc::vec![sn(&["I1"]), sn(&["I2"])], alloc::vec![]);
885 let out = transform_component(&c);
886 let names: Vec<String> = out
887 .equivalent_interface
888 .bases
889 .iter()
890 .map(|b| b.parts.last().map_or(String::new(), |i| i.text.clone()))
891 .collect();
892 assert_eq!(names, alloc::vec!["CCMObject", "I1", "I2"]);
893 }
894
895 #[test]
896 fn component_with_base_inherits_base_not_ccmobject() {
897 let c = comp_with_base("C", sn(&["B"]), alloc::vec![]);
900 let out = transform_component(&c);
901 let parts: Vec<String> = out.equivalent_interface.bases[0]
902 .parts
903 .iter()
904 .map(|p| p.text.clone())
905 .collect();
906 assert_eq!(parts, alloc::vec!["B"]);
907 }
908
909 #[test]
910 fn provides_decl_yields_provide_underscore_name_op() {
911 let c = comp(
913 "C",
914 alloc::vec![ComponentExport::Provides {
915 type_spec: sn(&["M", "I"]),
916 name: ident("foo"),
917 span: span(),
918 }],
919 );
920 let out = transform_component(&c);
921 let names = op_names(&out.equivalent_interface);
922 assert!(names.contains(&String::from("provide_foo")));
923 }
924
925 #[test]
926 fn uses_simplex_yields_three_ops_with_correct_signatures() {
927 let c = comp(
929 "C",
930 alloc::vec![ComponentExport::Uses {
931 type_spec: sn(&["I"]),
932 name: ident("manager"),
933 multiple: false,
934 span: span(),
935 }],
936 );
937 let out = transform_component(&c);
938 let names = op_names(&out.equivalent_interface);
939 assert!(names.contains(&String::from("connect_manager")));
940 assert!(names.contains(&String::from("disconnect_manager")));
941 assert!(names.contains(&String::from("get_connection_manager")));
942 }
943
944 #[test]
945 fn uses_multiple_yields_get_connections_plural_op() {
946 let c = comp(
948 "C",
949 alloc::vec![ComponentExport::Uses {
950 type_spec: sn(&["I"]),
951 name: ident("managers"),
952 multiple: true,
953 span: span(),
954 }],
955 );
956 let out = transform_component(&c);
957 let names = op_names(&out.equivalent_interface);
958 assert!(names.contains(&String::from("connect_managers")));
959 assert!(names.contains(&String::from("disconnect_managers")));
960 assert!(names.contains(&String::from("get_connections_managers")));
961 let connect = out
963 .equivalent_interface
964 .exports
965 .iter()
966 .find_map(|e| match e {
967 Export::Op(o) if o.name.text == "connect_managers" => Some(o),
968 _ => None,
969 })
970 .expect("connect");
971 let TypeSpec::Scoped(ret) = connect.return_type.as_ref().expect("return") else {
972 panic!()
973 };
974 assert_eq!(
975 ret.parts
976 .iter()
977 .map(|i| i.text.as_str())
978 .collect::<Vec<_>>(),
979 alloc::vec!["Components", "Cookie"]
980 );
981 }
982
983 #[test]
984 fn emits_decl_yields_connect_disconnect_with_consumer() {
985 let c = comp(
987 "C",
988 alloc::vec![ComponentExport::Emits {
989 type_spec: sn(&["Tick"]),
990 name: ident("ticker"),
991 span: span(),
992 }],
993 );
994 let out = transform_component(&c);
995 let names = op_names(&out.equivalent_interface);
996 assert!(names.contains(&String::from("connect_ticker")));
997 assert!(names.contains(&String::from("disconnect_ticker")));
998 assert_eq!(out.event_consumer_interfaces.len(), 1);
1000 assert_eq!(out.event_consumer_interfaces[0].name.text, "TickConsumer");
1001 }
1002
1003 #[test]
1004 fn publishes_decl_yields_subscribe_unsubscribe_with_cookie() {
1005 let c = comp(
1007 "C",
1008 alloc::vec![ComponentExport::Publishes {
1009 type_spec: sn(&["Tick"]),
1010 name: ident("ticker"),
1011 span: span(),
1012 }],
1013 );
1014 let out = transform_component(&c);
1015 let names = op_names(&out.equivalent_interface);
1016 assert!(names.contains(&String::from("subscribe_ticker")));
1017 assert!(names.contains(&String::from("unsubscribe_ticker")));
1018 let sub = out
1020 .equivalent_interface
1021 .exports
1022 .iter()
1023 .find_map(|e| match e {
1024 Export::Op(o) if o.name.text == "subscribe_ticker" => Some(o),
1025 _ => None,
1026 })
1027 .expect("subscribe");
1028 let TypeSpec::Scoped(ret) = sub.return_type.as_ref().expect("return") else {
1029 panic!()
1030 };
1031 assert_eq!(
1032 ret.parts
1033 .iter()
1034 .map(|i| i.text.as_str())
1035 .collect::<Vec<_>>(),
1036 alloc::vec!["Components", "Cookie"]
1037 );
1038 }
1039
1040 #[test]
1041 fn consumes_decl_yields_get_consumer_op() {
1042 let c = comp(
1045 "C",
1046 alloc::vec![ComponentExport::Consumes {
1047 type_spec: sn(&["Tick"]),
1048 name: ident("sink"),
1049 span: span(),
1050 }],
1051 );
1052 let out = transform_component(&c);
1053 let names = op_names(&out.equivalent_interface);
1054 assert!(names.contains(&String::from("get_consumer_sink")));
1055 let op = out
1057 .equivalent_interface
1058 .exports
1059 .iter()
1060 .find_map(|e| match e {
1061 Export::Op(o) if o.name.text == "get_consumer_sink" => Some(o),
1062 _ => None,
1063 })
1064 .expect("op");
1065 let TypeSpec::Scoped(ret) = op.return_type.as_ref().expect("return") else {
1066 panic!()
1067 };
1068 assert_eq!(
1069 ret.parts.last().expect("last").text.as_str(),
1070 "TickConsumer"
1071 );
1072 }
1073
1074 #[test]
1075 fn duplicate_event_type_yields_only_one_consumer_interface() {
1076 let c = comp(
1078 "C",
1079 alloc::vec![
1080 ComponentExport::Emits {
1081 type_spec: sn(&["Tick"]),
1082 name: ident("a"),
1083 span: span(),
1084 },
1085 ComponentExport::Publishes {
1086 type_spec: sn(&["Tick"]),
1087 name: ident("b"),
1088 span: span(),
1089 },
1090 ComponentExport::Consumes {
1091 type_spec: sn(&["Tick"]),
1092 name: ident("c"),
1093 span: span(),
1094 },
1095 ],
1096 );
1097 let out = transform_component(&c);
1098 assert_eq!(out.event_consumer_interfaces.len(), 1);
1099 }
1100
1101 #[test]
1102 fn attribute_is_propagated_to_equivalent_interface() {
1103 let c = comp(
1104 "C",
1105 alloc::vec![ComponentExport::Attribute(zerodds_idl::ast::AttrDcl {
1106 readonly: false,
1107 type_spec: TypeSpec::Primitive(PrimitiveType::Integer(IntegerType::Long)),
1108 name: ident("rate"),
1109 span: span(),
1110 })],
1111 );
1112 let out = transform_component(&c);
1113 let attr = out
1114 .equivalent_interface
1115 .exports
1116 .iter()
1117 .find_map(|e| match e {
1118 Export::Attr(a) => Some(a),
1119 _ => None,
1120 })
1121 .expect("attr");
1122 assert_eq!(attr.name.text, "rate");
1123 assert!(!attr.readonly);
1124 }
1125
1126 #[test]
1127 fn home_without_primary_key_yields_keyless_implicit() {
1128 let h = HomeDef {
1130 name: ident("CManager"),
1131 base: None,
1132 supports: Vec::new(),
1133 manages: sn(&["C"]),
1134 primary_key: None,
1135 annotations: Vec::new(),
1136 span: span(),
1137 };
1138 let out = transform_home(&h);
1139 assert_eq!(out.explicit.name.text, "CManagerExplicit");
1140 assert_eq!(out.implicit.name.text, "CManagerImplicit");
1141 assert_eq!(out.equivalent.name.text, "CManager");
1142 let parts = out.explicit.bases[0]
1144 .parts
1145 .iter()
1146 .map(|i| i.text.as_str())
1147 .collect::<Vec<_>>();
1148 assert_eq!(parts, alloc::vec!["Components", "CCMHome"]);
1149 let parts = out.implicit.bases[0]
1151 .parts
1152 .iter()
1153 .map(|i| i.text.as_str())
1154 .collect::<Vec<_>>();
1155 assert_eq!(parts, alloc::vec!["Components", "KeylessCCMHome"]);
1156 let names = op_names(&out.implicit);
1158 assert_eq!(names, alloc::vec!["create"]);
1159 }
1160
1161 #[test]
1162 fn home_with_primary_key_yields_keyed_implicit_with_four_ops() {
1163 let h = HomeDef {
1165 name: ident("CManager"),
1166 base: None,
1167 supports: Vec::new(),
1168 manages: sn(&["C"]),
1169 primary_key: Some(sn(&["CKey"])),
1170 annotations: Vec::new(),
1171 span: span(),
1172 };
1173 let out = transform_home(&h);
1174 let names = op_names(&out.implicit);
1175 for expected in ["create", "find_by_primary_key", "remove", "get_primary_key"] {
1176 assert!(
1177 names.contains(&String::from(expected)),
1178 "missing {expected} in {names:?}"
1179 );
1180 }
1181 assert!(out.implicit.bases.is_empty());
1183 }
1184
1185 #[test]
1186 fn derived_home_inherits_base_explicit() {
1187 let h = HomeDef {
1191 name: ident("CManagerExt"),
1192 base: Some(sn(&["CManagerBase"])),
1193 supports: Vec::new(),
1194 manages: sn(&["C"]),
1195 primary_key: None,
1196 annotations: Vec::new(),
1197 span: span(),
1198 };
1199 let out = transform_home(&h);
1200 assert_eq!(out.explicit.name.text, "CManagerExtExplicit");
1204 assert_eq!(
1205 out.explicit.bases.len(),
1206 1,
1207 "derived home explicit must inherit exactly the base's Explicit"
1208 );
1209 let parts = out.explicit.bases[0]
1210 .parts
1211 .iter()
1212 .map(|i| i.text.as_str())
1213 .collect::<Vec<_>>();
1214 assert_eq!(
1215 parts,
1216 alloc::vec!["CManagerBaseExplicit"],
1217 "expected CManagerBaseExplicit, got {parts:?}"
1218 );
1219 }
1220
1221 #[test]
1222 fn derived_home_with_supports_extends_base_explicit() {
1223 let h = HomeDef {
1226 name: ident("CManagerExt"),
1227 base: Some(sn(&["CManagerBase"])),
1228 supports: alloc::vec![sn(&["IExtraIface"])],
1229 manages: sn(&["C"]),
1230 primary_key: None,
1231 annotations: Vec::new(),
1232 span: span(),
1233 };
1234 let out = transform_home(&h);
1235 let names: Vec<String> = out
1236 .explicit
1237 .bases
1238 .iter()
1239 .map(|b| b.parts.last().map_or(String::new(), |i| i.text.clone()))
1240 .collect();
1241 assert_eq!(
1242 names,
1243 alloc::vec![
1244 String::from("CManagerBaseExplicit"),
1245 String::from("IExtraIface")
1246 ]
1247 );
1248 }
1249
1250 #[test]
1251 fn equivalent_home_inherits_explicit_and_implicit() {
1252 let h = HomeDef {
1253 name: ident("CManager"),
1254 base: None,
1255 supports: Vec::new(),
1256 manages: sn(&["C"]),
1257 primary_key: None,
1258 annotations: Vec::new(),
1259 span: span(),
1260 };
1261 let out = transform_home(&h);
1262 let names: Vec<String> = out
1263 .equivalent
1264 .bases
1265 .iter()
1266 .map(|b| b.parts.last().map_or(String::new(), |i| i.text.clone()))
1267 .collect();
1268 assert_eq!(names, alloc::vec!["CManagerExplicit", "CManagerImplicit"]);
1269 }
1270
1271 #[test]
1272 fn event_type_first_in_chain_inherits_event_base() {
1273 let et = EventDef {
1275 name: ident("Tick"),
1276 kind: ValueKind::Concrete,
1277 inheritance: None,
1278 elements: Vec::new(),
1279 annotations: Vec::new(),
1280 span: span(),
1281 };
1282 let out = transform_event_type(&et);
1283 let parts = out.valuetype_bases[0]
1285 .parts
1286 .iter()
1287 .map(|i| i.text.as_str())
1288 .collect::<Vec<_>>();
1289 assert_eq!(parts, alloc::vec!["Components", "EventBase"]);
1290 assert_eq!(out.consumer_interface.name.text, "TickConsumer");
1292 let cb = out.consumer_interface.bases[0]
1293 .parts
1294 .iter()
1295 .map(|i| i.text.as_str())
1296 .collect::<Vec<_>>();
1297 assert_eq!(cb, alloc::vec!["Components", "EventConsumerBase"]);
1298 let push = out.consumer_interface.exports.iter().find_map(|e| match e {
1300 Export::Op(o) => Some(o),
1301 _ => None,
1302 });
1303 let push = push.expect("push op");
1304 assert_eq!(push.name.text, "push_Tick");
1305 assert_eq!(push.params[0].name.text, "the_tick");
1306 }
1307
1308 #[test]
1309 fn full_stockmanager_component_yields_all_expected_ops() {
1310 let c = comp(
1312 "Trader",
1313 alloc::vec![
1314 ComponentExport::Provides {
1315 type_spec: sn(&["StockManager"]),
1316 name: ident("manager"),
1317 span: span(),
1318 },
1319 ComponentExport::Uses {
1320 type_spec: sn(&["Bank"]),
1321 name: ident("bank"),
1322 multiple: false,
1323 span: span(),
1324 },
1325 ComponentExport::Uses {
1326 type_spec: sn(&["Feed"]),
1327 name: ident("feeds"),
1328 multiple: true,
1329 span: span(),
1330 },
1331 ComponentExport::Emits {
1332 type_spec: sn(&["Tick"]),
1333 name: ident("ticker"),
1334 span: span(),
1335 },
1336 ComponentExport::Publishes {
1337 type_spec: sn(&["Tick"]),
1338 name: ident("public_ticker"),
1339 span: span(),
1340 },
1341 ComponentExport::Consumes {
1342 type_spec: sn(&["Order"]),
1343 name: ident("order_sink"),
1344 span: span(),
1345 },
1346 ],
1347 );
1348 let out = transform_component(&c);
1349 let names = op_names(&out.equivalent_interface);
1350 for expected in [
1351 "provide_manager",
1352 "connect_bank",
1353 "disconnect_bank",
1354 "get_connection_bank",
1355 "connect_feeds",
1356 "disconnect_feeds",
1357 "get_connections_feeds",
1358 "connect_ticker",
1359 "disconnect_ticker",
1360 "subscribe_public_ticker",
1361 "unsubscribe_public_ticker",
1362 "get_consumer_order_sink",
1363 ] {
1364 assert!(
1365 names.contains(&String::from(expected)),
1366 "missing {expected} in {names:?}"
1367 );
1368 }
1369 let consumer_names: Vec<String> = out
1371 .event_consumer_interfaces
1372 .iter()
1373 .map(|i| i.name.text.clone())
1374 .collect();
1375 assert!(consumer_names.contains(&String::from("TickConsumer")));
1376 assert!(consumer_names.contains(&String::from("OrderConsumer")));
1377 }
1378}