chematic_core/
extension.rs1use crate::Molecule;
10
11#[derive(Debug, Clone, PartialEq)]
13#[non_exhaustive]
14pub enum ExtensionValue {
15 Scalar(f64),
17 Vector(Vec<f64>),
19 Text(String),
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25#[non_exhaustive]
26pub enum ExtensionError {
27 InvalidInput(String),
29 Failed(String),
31}
32
33impl core::fmt::Display for ExtensionError {
34 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
35 match self {
36 Self::InvalidInput(message) => write!(f, "invalid extension input: {message}"),
37 Self::Failed(message) => write!(f, "extension failed: {message}"),
38 }
39 }
40}
41
42impl std::error::Error for ExtensionError {}
43
44pub trait MoleculeExtension: Send + Sync {
46 fn id(&self) -> &str;
48
49 fn version(&self) -> u32;
51
52 fn run(&self, molecule: &Molecule) -> Result<ExtensionValue, ExtensionError>;
54}
55
56#[derive(Default)]
58pub struct ExtensionRegistry {
59 extensions: Vec<Box<dyn MoleculeExtension>>,
60}
61
62impl ExtensionRegistry {
63 pub fn new() -> Self {
65 Self::default()
66 }
67
68 pub fn register<E>(&mut self, extension: E) -> Result<(), ExtensionError>
70 where
71 E: MoleculeExtension + 'static,
72 {
73 if self
74 .extensions
75 .iter()
76 .any(|existing| existing.id() == extension.id())
77 {
78 return Err(ExtensionError::Failed(format!(
79 "duplicate extension id: {}",
80 extension.id()
81 )));
82 }
83 self.extensions.push(Box::new(extension));
84 Ok(())
85 }
86
87 pub fn extensions(&self) -> impl Iterator<Item = &dyn MoleculeExtension> {
89 self.extensions.iter().map(Box::as_ref)
90 }
91
92 pub fn run(&self, id: &str, molecule: &Molecule) -> Result<ExtensionValue, ExtensionError> {
94 self.extensions
95 .iter()
96 .find(|extension| extension.id() == id)
97 .ok_or_else(|| ExtensionError::Failed(format!("unknown extension id: {id}")))?
98 .run(molecule)
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use crate::{Atom, Element, MoleculeBuilder};
106
107 struct AtomCount;
108
109 impl MoleculeExtension for AtomCount {
110 fn id(&self) -> &str {
111 "test.atom-count.v1"
112 }
113
114 fn version(&self) -> u32 {
115 1
116 }
117
118 fn run(&self, molecule: &Molecule) -> Result<ExtensionValue, ExtensionError> {
119 Ok(ExtensionValue::Scalar(molecule.atom_count() as f64))
120 }
121 }
122
123 fn ethanol() -> Molecule {
124 let mut builder = MoleculeBuilder::new();
125 let c1 = builder.add_atom(Atom::new(Element::C));
126 let c2 = builder.add_atom(Atom::new(Element::C));
127 builder.add_bond(c1, c2, crate::BondOrder::Single).unwrap();
128 builder.build()
129 }
130
131 #[test]
132 fn registry_runs_extension_and_preserves_order() {
133 let mut registry = ExtensionRegistry::new();
134 registry.register(AtomCount).unwrap();
135 assert_eq!(
136 registry
137 .extensions()
138 .map(|extension| extension.id())
139 .collect::<Vec<_>>(),
140 ["test.atom-count.v1"]
141 );
142 assert_eq!(
143 registry.run("test.atom-count.v1", ðanol()),
144 Ok(ExtensionValue::Scalar(2.0))
145 );
146 }
147
148 #[test]
149 fn registry_rejects_duplicate_and_unknown_ids() {
150 let mut registry = ExtensionRegistry::new();
151 registry.register(AtomCount).unwrap();
152 assert!(matches!(
153 registry.register(AtomCount),
154 Err(ExtensionError::Failed(message)) if message.contains("duplicate")
155 ));
156 assert!(matches!(
157 registry.run("missing.v1", ðanol()),
158 Err(ExtensionError::Failed(message)) if message.contains("unknown")
159 ));
160 }
161}