dag_ml_core/
implementation_registry.rs1use std::collections::BTreeMap;
9
10use crate::criteria::{
11 ImplementationDescriptor, ImplementationSemanticKind, LossReference, MetricReference,
12 PortabilityClass,
13};
14use crate::error::{DagMlError, Result};
15
16struct RegisteredImplementation<T> {
17 descriptor: ImplementationDescriptor,
18 implementation: T,
19}
20
21pub struct LocalImplementationRegistry<T> {
27 entries: BTreeMap<String, RegisteredImplementation<T>>,
28}
29
30impl<T> Default for LocalImplementationRegistry<T> {
31 fn default() -> Self {
32 Self {
33 entries: BTreeMap::new(),
34 }
35 }
36}
37
38impl<T> LocalImplementationRegistry<T> {
39 pub fn new() -> Self {
40 Self::default()
41 }
42
43 pub fn register(
44 &mut self,
45 descriptor: ImplementationDescriptor,
46 implementation: T,
47 ) -> Result<()> {
48 let key = implementation_dispatch_key(&descriptor)?;
49 if self.entries.contains_key(&key) {
50 return registration_error(format!(
51 "duplicate local implementation registry key `{key}`"
52 ));
53 }
54 self.entries.insert(
55 key,
56 RegisteredImplementation {
57 descriptor,
58 implementation,
59 },
60 );
61 Ok(())
62 }
63
64 pub fn register_loss(&mut self, loss: &LossReference, implementation: T) -> Result<()> {
65 loss.validate()?;
66 self.register(loss.implementation.clone(), implementation)
67 }
68
69 pub fn register_metric(&mut self, metric: &MetricReference, implementation: T) -> Result<()> {
70 metric.validate()?;
71 self.register(metric.implementation.clone(), implementation)
72 }
73
74 pub fn resolve(&self, descriptor: &ImplementationDescriptor) -> Result<&T> {
75 let key = implementation_dispatch_key(descriptor)?;
76 let registered = self.entries.get(&key).ok_or_else(|| {
77 DagMlError::RuntimeValidation(format!(
78 "local implementation registry has no implementation for `{key}`"
79 ))
80 })?;
81 if registered.descriptor != *descriptor {
82 return resolution_error(format!(
83 "local implementation registered for `{key}` does not match the requested descriptor"
84 ));
85 }
86 Ok(®istered.implementation)
87 }
88
89 pub fn resolve_loss(&self, loss: &LossReference) -> Result<&T> {
90 loss.validate()?;
91 if loss.implementation.semantic_kind != ImplementationSemanticKind::Loss {
92 return resolution_error("local loss resolution received a non-loss descriptor");
93 }
94 self.resolve(&loss.implementation)
95 }
96
97 pub fn resolve_metric(&self, metric: &MetricReference) -> Result<&T> {
98 metric.validate()?;
99 if metric.implementation.semantic_kind != ImplementationSemanticKind::Metric {
100 return resolution_error("local metric resolution received a non-metric descriptor");
101 }
102 self.resolve(&metric.implementation)
103 }
104
105 pub fn unregister(&mut self, descriptor: &ImplementationDescriptor) -> Result<T> {
106 let key = implementation_dispatch_key(descriptor)?;
107 let registered = self.entries.get(&key).ok_or_else(|| {
108 DagMlError::RuntimeValidation(format!(
109 "local implementation registry has no implementation for `{key}`"
110 ))
111 })?;
112 if registered.descriptor != *descriptor {
113 return resolution_error(format!(
114 "local implementation registered for `{key}` does not match the requested descriptor"
115 ));
116 }
117 Ok(self
118 .entries
119 .remove(&key)
120 .expect("entry checked above")
121 .implementation)
122 }
123
124 pub fn descriptors(&self) -> impl Iterator<Item = &ImplementationDescriptor> {
125 self.entries.values().map(|entry| &entry.descriptor)
126 }
127
128 pub fn len(&self) -> usize {
129 self.entries.len()
130 }
131
132 pub fn is_empty(&self) -> bool {
133 self.entries.is_empty()
134 }
135
136 pub fn clear(&mut self) {
137 self.entries.clear();
138 }
139}
140
141pub fn implementation_dispatch_key(descriptor: &ImplementationDescriptor) -> Result<String> {
146 descriptor.validate()?;
147 match (&descriptor.registry_key, descriptor.portability) {
148 (Some(key), _) => Ok(key.clone()),
149 (None, PortabilityClass::PortableBuiltIn) => Ok(format!(
150 "portable_builtin:{}",
151 descriptor.descriptor_fingerprint
152 )),
153 (None, _) => {
154 registration_error("non-built-in implementation descriptor has no local registry key")
155 }
156 }
157}
158
159fn registration_error<T>(message: impl Into<String>) -> Result<T> {
160 Err(DagMlError::CampaignValidation(message.into()))
161}
162
163fn resolution_error<T>(message: impl Into<String>) -> Result<T> {
164 Err(DagMlError::RuntimeValidation(message.into()))
165}
166
167#[cfg(test)]
168mod tests {
169 use std::sync::{
170 atomic::{AtomicUsize, Ordering},
171 Arc,
172 };
173
174 use serde_json::Value;
175
176 use super::*;
177
178 fn custom_loss() -> LossReference {
179 let fixture: Value = serde_json::from_str(include_str!(
180 "../../../examples/fixtures/criteria/criteria_contracts.v1.json"
181 ))
182 .unwrap();
183 serde_json::from_value(fixture["valid"]["training_loss_role"]["loss"].clone()).unwrap()
184 }
185
186 #[test]
187 fn rust_closure_is_resolved_and_executed_as_a_local_loss() {
188 type LossFn = Box<dyn Fn(f64, f64) -> f64>;
189
190 let loss = custom_loss();
191 let mut registry = LocalImplementationRegistry::<LossFn>::new();
192 registry
193 .register_loss(
194 &loss,
195 Box::new(|target, prediction| (prediction - target).abs()),
196 )
197 .unwrap();
198
199 let callback = registry.resolve_loss(&loss).unwrap();
200 assert_eq!(callback(2.0, 5.5), 3.5);
201 assert_eq!(registry.len(), 1);
202 assert_eq!(registry.descriptors().next(), Some(&loss.implementation));
203 }
204
205 #[test]
206 fn resolution_requires_the_exact_descriptor_not_only_the_registry_key() {
207 let loss = custom_loss();
208 let mut registry = LocalImplementationRegistry::new();
209 registry.register_loss(&loss, "callable-a").unwrap();
210
211 let mut incompatible = loss.clone();
212 incompatible.implementation.implementation_version = "2.0.0".to_string();
213 incompatible.implementation.descriptor_fingerprint =
214 incompatible.implementation.compute_fingerprint().unwrap();
215 let error = registry
216 .resolve_loss(&incompatible)
217 .unwrap_err()
218 .to_string();
219 assert!(error.contains("does not match the requested descriptor"));
220 }
221
222 #[test]
223 fn duplicate_registry_keys_are_rejected_even_for_different_descriptors() {
224 let loss = custom_loss();
225 let mut second = loss.clone();
226 second.implementation.implementation_version = "2.0.0".to_string();
227 second.implementation.descriptor_fingerprint =
228 second.implementation.compute_fingerprint().unwrap();
229
230 let mut registry = LocalImplementationRegistry::new();
231 registry.register_loss(&loss, "callable-a").unwrap();
232 let error = registry
233 .register_loss(&second, "callable-b")
234 .unwrap_err()
235 .to_string();
236 assert!(error.contains("duplicate local implementation registry key"));
237 }
238
239 #[test]
240 fn unregister_checks_identity_and_returns_the_local_object() {
241 let loss = custom_loss();
242 let mut registry = LocalImplementationRegistry::new();
243 registry
244 .register_loss(&loss, String::from("callable-a"))
245 .unwrap();
246
247 assert_eq!(
248 registry.unregister(&loss.implementation).unwrap(),
249 "callable-a"
250 );
251 assert!(registry.is_empty());
252 assert!(registry.resolve_loss(&loss).is_err());
253 }
254
255 #[test]
256 fn clear_releases_registry_owned_implementations() {
257 struct DropProbe(Arc<AtomicUsize>);
258
259 impl Drop for DropProbe {
260 fn drop(&mut self) {
261 self.0.fetch_add(1, Ordering::SeqCst);
262 }
263 }
264
265 let loss = custom_loss();
266 let drops = Arc::new(AtomicUsize::new(0));
267 let mut registry = LocalImplementationRegistry::new();
268 registry
269 .register_loss(&loss, DropProbe(Arc::clone(&drops)))
270 .unwrap();
271
272 assert_eq!(drops.load(Ordering::SeqCst), 0);
273 registry.clear();
274 assert_eq!(drops.load(Ordering::SeqCst), 1);
275 assert!(registry.is_empty());
276 }
277}