Skip to main content

relon_eval_api/
decorator.rs

1//! Decorator plugin protocol.
2//!
3//! Hosts extend Relon's `@name(...)` syntax by implementing
4//! [`DecoratorPlugin`] and registering an instance under the decorator's
5//! full dotted path name. The trait lives in `relon-eval-api` so any
6//! backend implementing [`crate::Evaluator`] can dispatch through it; the
7//! built-in decorators (`#import`, `#schema`, `#ensure`, ...) and any
8//! host-side plugin live in their respective backend / host crates.
9
10use crate::error::RuntimeError;
11use crate::native_fn::EvaluatedArg;
12use crate::scope::Scope;
13use crate::value::{SchemaField, Value};
14use crate::Evaluator;
15use relon_parser::{CallArg, Node, TokenRange};
16use std::sync::Arc;
17
18/// The outcome of a decorator's pre-evaluation hook.
19///
20/// A decorator runs *before* the node it annotates is evaluated. It can
21/// either step aside, swap the active scope, or take over the value entirely.
22///
23/// `Override` boxes its `Value` to keep the enum small — `Value` is by far
24/// the largest variant payload and most decorators choose `Pass`.
25pub enum PreEvalOutcome {
26    /// Run the default evaluation path with the existing scope.
27    Pass,
28
29    /// Run the default evaluation path, but swap in this scope first.
30    /// Used by `#import` to inject the imported module's bindings.
31    Rescope(Arc<Scope>),
32
33    /// Skip the default evaluation path; use this value as the result.
34    /// Used by `#schema` to interpret the body as a schema definition
35    /// rather than as data.
36    Override(Box<Value>),
37}
38
39/// Hosts extend Relon's `@name(...)` syntax by implementing this trait and
40/// registering an instance under the decorator's full dotted path name (e.g.
41/// `"import"`, `"ensure.int"`, `"my_org.audit"`).
42///
43/// Three independent hooks are exposed:
44///
45/// * [`pre_eval`](Self::pre_eval) runs before the decorated node is evaluated.
46///   Use it to inject locals into scope (`#import`) or take over the value
47///   entirely (`#schema`).
48/// * [`wrap`](Self::wrap) runs after the node is evaluated. Use it to validate
49///   or transform the value (`@ensure.int`, `@currency("USD")`).
50/// * [`schema_field_meta`](Self::schema_field_meta) runs while extracting
51///   fields from a `#schema`-annotated dict. Use it to attach per-field
52///   metadata such as defaults or custom error messages (`#expect`, `#default`).
53///
54/// All hooks default to no-op / identity, so plugins only override what they
55/// actually need.
56pub trait DecoratorPlugin: Send + Sync {
57    fn pre_eval(
58        &self,
59        _eval: &dyn Evaluator,
60        _node: &Node,
61        _scope: &Arc<Scope>,
62        _args: &[CallArg],
63        _range: TokenRange,
64    ) -> Result<PreEvalOutcome, RuntimeError> {
65        Ok(PreEvalOutcome::Pass)
66    }
67
68    fn wrap(
69        &self,
70        _eval: &dyn Evaluator,
71        value: Value,
72        _scope: &Arc<Scope>,
73        _args: &[EvaluatedArg],
74        _range: TokenRange,
75    ) -> Result<Value, RuntimeError> {
76        Ok(value)
77    }
78
79    /// AST-level wrap hook. Called *before* [`Self::wrap`]: receives the
80    /// decorator's raw [`CallArg`] AST nodes (un-evaluated) along with the
81    /// host [`Node`] so plugins that need to inspect type expressions,
82    /// paths, or sibling decorators / type hints can do so without losing
83    /// them to evaluation.
84    ///
85    /// Returns `Ok(None)` to fall through to the standard `wrap` path.
86    /// Returning `Ok(Some(val))` short-circuits: `wrap` is not called and
87    /// `val` becomes the decorator's output. `@brand(Type)` uses this to
88    /// extract the type name from the un-evaluated AST so a bareword like
89    /// `Weather` doesn't get resolved to a `Value::Schema` first.
90    fn wrap_with_ast(
91        &self,
92        _eval: &dyn Evaluator,
93        _node: &Node,
94        _value: &Value,
95        _scope: &Arc<Scope>,
96        _ast_args: &[CallArg],
97        _range: TokenRange,
98    ) -> Result<Option<Value>, RuntimeError> {
99        Ok(None)
100    }
101
102    fn schema_field_meta(
103        &self,
104        _eval: &dyn Evaluator,
105        _field: &mut SchemaField,
106        _scope: &Arc<Scope>,
107        _args: &[EvaluatedArg],
108        _range: TokenRange,
109    ) -> Result<(), RuntimeError> {
110        Ok(())
111    }
112}