Skip to main content

wyrd/runtime_impl/
recipe.rs

1//! Reusable recipe contracts over validated weaves and bound runtime ports.
2
3use std::string::String;
4use std::vec::Vec;
5
6use crate::authoring::{BuildError, Weave};
7use crate::foundation::{KnotKind, NumericPath, SignalDomain};
8use crate::runtime_impl::error::{RecipeError, RecipeResolveError};
9use crate::runtime_impl::{BindOpts, PortWriter, Runtime};
10
11#[cfg(feature = "schema")]
12use schemars::JsonSchema;
13#[cfg(feature = "serde")]
14use serde::{Deserialize, Serialize};
15#[cfg(feature = "schema")]
16use std::borrow::ToOwned;
17
18/// A reusable graph with statically named, runtime-resolved ports.
19///
20/// `Ports` is application-defined and normally contains dense handle types such
21/// as [`crate::SenseId`], [`crate::HostPathId`], and [`crate::CmdId`].
22pub trait Recipe: Sized {
23    /// Typed runtime handles required by this recipe's host.
24    type Ports;
25
26    /// Construct this recipe's validated weave.
27    fn weave() -> Result<Weave, BuildError>;
28
29    /// Resolve this recipe's typed ports from its freshly bound runtime.
30    ///
31    /// Implementations should use [`Runtime::required_sense`],
32    /// [`Runtime::required_path`], and [`Runtime::required_command`] so a
33    /// missing or incompatible endpoint retains its contextual name.
34    fn resolve_ports(runtime: &Runtime) -> Result<Self::Ports, RecipeResolveError>;
35
36    /// Build, bind, and resolve this recipe using default bind options.
37    fn bind() -> Result<RecipeInstance<Self>, RecipeError> {
38        Self::bind_with(BindOpts::default())
39    }
40
41    /// Build, bind, and resolve this recipe with explicit runtime bind options.
42    fn bind_with(opts: BindOpts) -> Result<RecipeInstance<Self>, RecipeError> {
43        let runtime = Runtime::bind(Self::weave()?, opts)?;
44        let ports = Self::resolve_ports(&runtime)?;
45        Ok(RecipeInstance { runtime, ports })
46    }
47
48    /// Derive this recipe's deterministic endpoint manifest from its weave.
49    fn manifest() -> Result<RecipeManifest, RecipeError> {
50        Ok(RecipeManifest::from_weave(&Self::weave()?))
51    }
52}
53
54/// One bound [`Recipe`] with its runtime and typed resolved ports.
55pub struct RecipeInstance<R: Recipe> {
56    runtime: Runtime,
57    ports: R::Ports,
58}
59
60impl<R: Recipe> RecipeInstance<R> {
61    /// Borrow the executable runtime for advanced host integration.
62    pub fn runtime(&self) -> &Runtime {
63        &self.runtime
64    }
65
66    /// Mutably borrow the executable runtime for frame setup and loom.
67    pub fn runtime_mut(&mut self) -> &mut Runtime {
68        &mut self.runtime
69    }
70
71    /// Borrow the recipe's typed, runtime-owned ports.
72    pub fn ports(&self) -> &R::Ports {
73        &self.ports
74    }
75
76    /// Borrow the typed ports and a writer for this instance's runtime together.
77    ///
78    /// This is crate-visible support for [`crate::Scenario`], which must keep
79    /// frame writes tied to the same recipe instance that resolved the ports.
80    pub(crate) fn port_writer_with_ports(&mut self) -> (&R::Ports, PortWriter<'_>) {
81        let Self { runtime, ports } = self;
82        (ports, runtime.port_writer())
83    }
84
85    /// Split this instance into its runtime and typed ports.
86    pub fn into_parts(self) -> (Runtime, R::Ports) {
87        (self.runtime, self.ports)
88    }
89}
90
91/// Deterministic, tooling-facing summary of a validated recipe topology.
92#[derive(Clone, Debug, PartialEq, Eq)]
93#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
94#[cfg_attr(feature = "schema", derive(JsonSchema))]
95pub struct RecipeManifest {
96    /// Validated weave id.
97    pub weave_id: String,
98    /// Numeric path declared by the weave.
99    pub numeric: NumericPath,
100    /// Host-writable `SignalIn` endpoints, ordered by knot id.
101    pub signal_inputs: Vec<SignalInManifest>,
102    /// Host-applied `SignalOut` endpoints, ordered by path then knot id.
103    pub signal_outputs: Vec<SignalOutManifest>,
104    /// Host-applied `EmitCommand` endpoints, ordered by name then knot id.
105    pub emit_commands: Vec<EmitCommandManifest>,
106}
107
108impl RecipeManifest {
109    /// Extract a manifest from an already validated weave.
110    pub fn from_weave(weave: &Weave) -> Self {
111        let mut signal_inputs = Vec::new();
112        let mut signal_outputs = Vec::new();
113        let mut emit_commands = Vec::new();
114
115        for knot in weave.knots() {
116            match &knot.kind {
117                KnotKind::SignalIn { domain } => signal_inputs.push(SignalInManifest {
118                    knot: knot.id.clone(),
119                    domain: *domain,
120                }),
121                KnotKind::SignalOut { path, domain } => signal_outputs.push(SignalOutManifest {
122                    knot: knot.id.clone(),
123                    path: path.clone(),
124                    domain: *domain,
125                }),
126                KnotKind::EmitCommand { name } => emit_commands.push(EmitCommandManifest {
127                    knot: knot.id.clone(),
128                    name: name.clone(),
129                }),
130                _ => {}
131            }
132        }
133
134        signal_inputs.sort_by(|left, right| left.knot.cmp(&right.knot));
135        signal_outputs.sort_by(|left, right| {
136            left.path
137                .cmp(&right.path)
138                .then_with(|| left.knot.cmp(&right.knot))
139        });
140        emit_commands.sort_by(|left, right| {
141            left.name
142                .cmp(&right.name)
143                .then_with(|| left.knot.cmp(&right.knot))
144        });
145
146        Self {
147            weave_id: String::from(weave.id()),
148            numeric: weave.numeric(),
149            signal_inputs,
150            signal_outputs,
151            emit_commands,
152        }
153    }
154}
155
156/// A named host-writable `SignalIn` endpoint in a [`RecipeManifest`].
157#[derive(Clone, Debug, PartialEq, Eq)]
158#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
159#[cfg_attr(feature = "schema", derive(JsonSchema))]
160pub struct SignalInManifest {
161    /// Author knot id used to resolve the [`crate::SenseId`].
162    pub knot: String,
163    /// Required host signal domain.
164    pub domain: SignalDomain,
165}
166
167/// A named host-applied `SignalOut` endpoint in a [`RecipeManifest`].
168#[derive(Clone, Debug, PartialEq, Eq)]
169#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
170#[cfg_attr(feature = "schema", derive(JsonSchema))]
171pub struct SignalOutManifest {
172    /// Author knot id associated with this output path.
173    pub knot: String,
174    /// Host path resolved to a [`crate::HostPathId`] after bind.
175    pub path: String,
176    /// Signal domain sent to the host.
177    pub domain: SignalDomain,
178}
179
180/// A named host-applied `EmitCommand` endpoint in a [`RecipeManifest`].
181#[derive(Clone, Debug, PartialEq, Eq)]
182#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
183#[cfg_attr(feature = "schema", derive(JsonSchema))]
184pub struct EmitCommandManifest {
185    /// Author knot id associated with this command.
186    pub knot: String,
187    /// Command name resolved to a [`crate::CmdId`] after bind.
188    pub name: String,
189}