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}