Skip to main content

panopticon_core/operation/
registry.rs

1use crate::imports::*;
2
3/// The draft-time collection of [`Operation`] types known to a
4/// pipeline.
5///
6/// Every call to
7/// [`Pipeline::step`](crate::prelude::Pipeline#method.step) registers
8/// the operation type (idempotently), validates its
9/// [`OperationMetadata`], and records a factory that the runtime uses
10/// to dispatch execution. Child pipelines created by the `*_from_child`
11/// methods merge their registries into the parent. End users do not
12/// typically touch the registry directly — it is exposed primarily so
13/// tooling that walks compiled pipelines can inspect what operations
14/// are available.
15#[derive(Default)]
16pub struct Registry {
17    entries: HashMap<std::any::TypeId, OperationEntry>,
18}
19
20pub(crate) struct OperationEntry {
21    pub metadata: OperationMetadata,
22    pub factory: fn(&mut Context) -> Result<(), OperationError>,
23}
24
25impl OperationEntry {
26    pub fn execute(&self, context: &mut Context) -> Result<(), OperationError> {
27        (self.factory)(context)
28    }
29}
30
31impl Registry {
32    /// Constructs an empty registry.
33    pub fn new() -> Self {
34        Self::default()
35    }
36
37    /// Registers an [`Operation`] type. Idempotent — subsequent calls
38    /// for the same type are no-ops. Validates the returned
39    /// [`OperationMetadata`] and fails with
40    /// [`DraftError::InvalidMetadata`] if any declared output or
41    /// extension uses a [`NameSpec::DerivedFrom`] that references a
42    /// non-existent or wrongly-typed input.
43    pub fn register<O: Operation + 'static>(&mut self) -> Result<(), DraftError> {
44        let id = std::any::TypeId::of::<O>();
45        if self.entries.contains_key(&id) {
46            return Ok(());
47        }
48        let metadata = O::metadata();
49        Self::validate_metadata(&metadata)?;
50        self.entries.insert(
51            id,
52            OperationEntry {
53                metadata,
54                factory: |context| O::execute(context),
55            },
56        );
57        Ok(())
58    }
59
60    fn validate_metadata(metadata: &OperationMetadata) -> Result<(), DraftError> {
61        for output in metadata.outputs {
62            Self::validate_derived_name(metadata, &output.name)?;
63        }
64        for ext in metadata.requires_extensions {
65            Self::validate_derived_name(metadata, &ext.name)?;
66        }
67        Ok(())
68    }
69
70    fn validate_derived_name(
71        metadata: &OperationMetadata,
72        name: &NameSpec,
73    ) -> Result<(), DraftError> {
74        if let NameSpec::DerivedFrom(input_name)
75        | NameSpec::DerivedWithDefault { input_name, .. } = name
76        {
77            let input = metadata.inputs.iter().find(|i| i.name == *input_name);
78            match input {
79                None => {
80                    return Err(DraftError::InvalidMetadata {
81                        operation: metadata.name,
82                        reason: format!(
83                            "DerivedFrom('{}') references a non-existent input",
84                            input_name
85                        ),
86                    });
87                }
88                Some(spec) if spec.ty != Type::Text => {
89                    return Err(DraftError::InvalidMetadata {
90                        operation: metadata.name,
91                        reason: format!(
92                            "DerivedFrom('{}') requires input type Text, found {}",
93                            input_name, spec.ty
94                        ),
95                    });
96                }
97                _ => {}
98            }
99        }
100        if let NameSpec::DerivedWithDefault { input_name, .. } = name {
101            let input = metadata.inputs.iter().find(|i| i.name == *input_name);
102            if let Some(input) = input
103                && input.required
104            {
105                return Err(DraftError::InvalidMetadata {
106                    operation: metadata.name,
107                    reason: format!(
108                        "DerivedWithDefault('{}') requires an optional input, but '{}' is required",
109                        input_name, input_name
110                    ),
111                });
112            }
113        }
114        Ok(())
115    }
116
117    pub(crate) fn get(&self, id: &std::any::TypeId) -> Option<&OperationEntry> {
118        self.entries.get(id)
119    }
120
121    pub(crate) fn merge(&mut self, other: Registry) {
122        for (id, entry) in other.entries {
123            self.entries.entry(id).or_insert(entry);
124        }
125    }
126}