Skip to main content

panopticon_core/operation/
context.rs

1use crate::imports::*;
2
3/// The runtime handle passed to [`Operation::execute`].
4///
5/// A `Context` is a per-step view of the pipeline: it borrows the
6/// operation's [`OperationMetadata`], the runtime [`Store`], the
7/// registered [`Extensions`](crate::extend::Extension), and a scoped
8/// prefix for looking up parameters. Operations use it to read
9/// declared inputs, fetch extensions, emit errors, and record outputs.
10/// Outputs are staged in two internal stores (operation-scoped and
11/// global) and merged back into the runtime store after the step
12/// finishes.
13///
14/// A `Context` validates every access against its metadata — calling
15/// [`input`](Self::input), [`set_static_output`](Self::set_static_output),
16/// [`set_derived_output`](Self::set_derived_output), or
17/// [`extension`](Self::extension) with a name the operation did not
18/// declare returns an [`OperationError`] rather than silently
19/// succeeding.
20pub struct Context<'a> {
21    metadata: OperationMetadata,
22    param_prefix: &'a str,
23    store: &'a Store<StoreEntry>,
24    operation_outputs: Store<StoreEntry>,
25    global_outputs: Store<StoreEntry>,
26    extensions: &'a Extensions,
27}
28
29impl Context<'_> {
30    /// Constructs a fresh `Context` with empty staged-output stores.
31    /// Called by the runtime immediately before a step executes; not
32    /// typically invoked from operation code.
33    pub fn new<'a>(
34        metadata: OperationMetadata,
35        param_prefix: &'a str,
36        store: &'a Store<StoreEntry>,
37        extensions: &'a Extensions,
38    ) -> Context<'a> {
39        Context {
40            metadata,
41            param_prefix,
42            store,
43            operation_outputs: Store::<StoreEntry>::new(),
44            global_outputs: Store::<StoreEntry>::new(),
45            extensions,
46        }
47    }
48
49    /// Looks up a declared input by name, applying the step's parameter
50    /// prefix automatically. Fails with
51    /// [`OperationError::UndeclaredInput`] if the name is not in the
52    /// operation's metadata.
53    pub fn input(&self, name: &str) -> Result<&StoreEntry, OperationError> {
54        let _spec = self
55            .metadata
56            .inputs
57            .iter()
58            .find(|s| s.name == name)
59            .ok_or_else(|| OperationError::UndeclaredInput {
60                operation: self.metadata.name,
61                input: name.to_string(),
62            })?;
63
64        let path = format!("{}.{}", self.param_prefix, name);
65        Ok(self.store.get(&path)?)
66    }
67
68    /// Records a statically-named output declared on the operation's
69    /// metadata as [`NameSpec::Static`]. Fails with
70    /// [`OperationError::UndeclaredOutput`] if no matching output is
71    /// declared.
72    pub fn set_static_output(
73        &mut self,
74        name: &'static str,
75        entry: impl Into<StoreEntry>,
76    ) -> Result<(), OperationError> {
77        let spec = self
78            .metadata
79            .outputs
80            .iter()
81            .find(|s| matches!(&s.name, NameSpec::Static(n) if *n == name))
82            .ok_or_else(|| OperationError::UndeclaredOutput {
83                operation: self.metadata.name,
84                output: name.to_string(),
85            })?;
86
87        self.insert_by_scope(&spec.scope, name.to_string(), entry.into())
88    }
89
90    /// Records an output whose name is derived from a text input,
91    /// declared on the operation's metadata as [`NameSpec::DerivedFrom`]
92    /// or [`NameSpec::DerivedWithDefault`]. Fails with
93    /// [`OperationError::UndeclaredDerivedOutput`] if no matching output
94    /// is declared.
95    pub fn set_derived_output(
96        &mut self,
97        input_name: &str,
98        entry: impl Into<StoreEntry>,
99    ) -> Result<(), OperationError> {
100        let spec = self
101            .metadata
102            .outputs
103            .iter()
104            .find(|s| matches!(&s.name, NameSpec::DerivedFrom(n) | NameSpec::DerivedWithDefault { input_name: n, .. } if *n == input_name))
105            .ok_or_else(|| OperationError::UndeclaredDerivedOutput {
106                operation: self.metadata.name,
107                input: input_name.to_string(),
108            })?;
109
110        let output_key = match &spec.name {
111            NameSpec::DerivedFrom(input_name) => {
112                let path = format!("{}.{}", self.param_prefix, input_name);
113                self.store.get(&path)?.get_value()?.as_text()?.to_string()
114            }
115            NameSpec::DerivedWithDefault {
116                input_name,
117                default,
118            } => {
119                let path = format!("{}.{}", self.param_prefix, input_name);
120                match self.store.get(&path) {
121                    Ok(entry) => entry.get_value()?.as_text()?.to_string(),
122                    Err(_) => default.to_string(),
123                }
124            }
125            NameSpec::Static(_) => unreachable!(),
126        };
127
128        self.insert_by_scope(&spec.scope, output_key, entry.into())
129    }
130
131    fn insert_by_scope(
132        &mut self,
133        scope: &OutputScope,
134        name: String,
135        entry: StoreEntry,
136    ) -> Result<(), OperationError> {
137        let store = match scope {
138            OutputScope::Operation => &mut self.operation_outputs,
139            OutputScope::Global => &mut self.global_outputs,
140        };
141        Ok(store.insert(name, entry)?)
142    }
143
144    /// Looks up a registered [`Extension`] of type `T` under a
145    /// declared extension spec. Fails with
146    /// [`OperationError::ExtensionNotFound`] if the spec is undeclared
147    /// or no extension was registered under the resolved name.
148    pub fn extension<T: Extension>(&self, spec_name: &str) -> Result<&T, OperationError> {
149        let spec = self
150            .metadata
151            .requires_extensions
152            .iter()
153            .find(|s| match &s.name {
154                NameSpec::Static(n) => *n == spec_name,
155                NameSpec::DerivedFrom(n) => *n == spec_name,
156                NameSpec::DerivedWithDefault { input_name, .. } => *input_name == spec_name,
157            })
158            .ok_or_else(|| OperationError::ExtensionNotFound {
159                operation: self.metadata.name.to_string(),
160                extension: spec_name.to_string(),
161            })?;
162
163        let resolved_name = match &spec.name {
164            NameSpec::Static(name) => name.to_string(),
165            NameSpec::DerivedFrom(input_name) => {
166                let path = format!("{}.{}", self.param_prefix, input_name);
167                self.store.get(&path)?.get_value()?.as_text()?.to_string()
168            }
169            NameSpec::DerivedWithDefault {
170                input_name,
171                default,
172            } => {
173                let path = format!("{}.{}", self.param_prefix, input_name);
174                match self.store.get(&path) {
175                    Ok(entry) => entry.get_value()?.as_text()?.to_string(),
176                    Err(_) => default.to_string(),
177                }
178            }
179        };
180
181        self.extensions
182            .get::<T>(&resolved_name)
183            .ok_or_else(|| OperationError::ExtensionNotFound {
184                operation: self.metadata.name.to_string(),
185                extension: resolved_name,
186            })
187    }
188
189    /// Builds an [`OperationError::Custom`] tagged with the operation's
190    /// name for ad-hoc failure reporting. Prefer the
191    /// [`op_error!`](crate::op_error) macro for `format!`-style
192    /// construction.
193    pub fn error(&self, message: impl Into<String>) -> OperationError {
194        OperationError::Custom {
195            operation: self.metadata.name.into(),
196            message: message.into(),
197        }
198    }
199
200    pub(crate) fn consume(self) -> (Store<StoreEntry>, Store<StoreEntry>) {
201        (self.operation_outputs, self.global_outputs)
202    }
203}