Skip to main content

phoxal_bundle/
reader.rs

1//! Bundle readers for supervisors and participant processes.
2
3use std::collections::BTreeMap;
4use std::path::Path;
5use std::sync::Arc;
6
7use phoxal_model::Robot;
8use phoxal_runtime_contract::identity::{ParticipantArtifactId, ParticipantId};
9
10use crate::{
11    BinaryReference, BundleError, BundleRoot, ParticipantAssets, RuntimeDocument,
12    RuntimeParticipant, SelectionError, read_runtime_document, require_layout_directories,
13    validate_layout,
14};
15
16/// One selected participant and the immutable runtime inputs it consumes.
17#[derive(Clone, Debug)]
18pub struct ParticipantRuntimeInputs {
19    robot: Arc<Robot>,
20    participant: RuntimeParticipant,
21    /// The reusable artifact selected by `participant.artifact`.
22    artifact: BinaryReference,
23    assets: ParticipantAssets,
24}
25
26impl ParticipantRuntimeInputs {
27    /// The canonical robot selected with this participant.
28    #[must_use]
29    pub fn robot(&self) -> &Robot {
30        self.robot.as_ref()
31    }
32
33    /// The exact persisted participant record selected for this process.
34    #[must_use]
35    pub const fn participant(&self) -> &RuntimeParticipant {
36        &self.participant
37    }
38
39    /// The reusable executable artifact selected by the participant record.
40    #[must_use]
41    pub const fn artifact(&self) -> &BinaryReference {
42        &self.artifact
43    }
44
45    /// Participant-readable, digest-checked assets from the same bundle.
46    #[must_use]
47    pub const fn assets(&self) -> &ParticipantAssets {
48        &self.assets
49    }
50}
51
52/// A loaded, integrity-checked runtime bundle.
53#[derive(Clone, Debug)]
54pub struct RuntimeBundle {
55    root: BundleRoot,
56    document: RuntimeDocument,
57    assets: ParticipantAssets,
58}
59
60impl RuntimeBundle {
61    /// Open and verify every indexed file in one installed bundle.
62    ///
63    /// This is the supervisor/builder boundary: it rejects an unrelated
64    /// changed asset or executable before an execution is created.
65    pub fn open_verified(root: impl AsRef<Path>) -> Result<Self, BundleError> {
66        let root = BundleRoot::open(root.as_ref())?;
67        let document = read_runtime_document(&root)?;
68        validate_layout(&root, document.runtime())?;
69        Ok(Self {
70            assets: ParticipantAssets::new(root.clone(), &document.runtime().assets),
71            root,
72            document,
73        })
74    }
75
76    /// The requested installed root path, retained for diagnostics.
77    #[must_use]
78    pub fn root(&self) -> &Path {
79        self.root.path()
80    }
81
82    /// The validated persisted document.
83    #[must_use]
84    pub const fn document(&self) -> &RuntimeDocument {
85        &self.document
86    }
87
88    /// The canonical robot loaded from runtime.json, with no source parser.
89    #[must_use]
90    pub fn robot(&self) -> &Robot {
91        self.document.robot()
92    }
93
94    /// The sole persisted RobotId.
95    #[must_use]
96    pub fn robot_id(&self) -> &phoxal_model::identity::RobotId {
97        self.document.robot_id()
98    }
99
100    /// The final persisted participant set.
101    #[must_use]
102    pub fn participants(&self) -> &[RuntimeParticipant] {
103        self.document.participants()
104    }
105
106    /// The reusable executable artifacts retained by this bundle.
107    #[must_use]
108    pub fn artifacts(&self) -> &BTreeMap<ParticipantArtifactId, BinaryReference> {
109        self.document.artifacts()
110    }
111
112    /// Participant-readable digest-checked assets.
113    #[must_use]
114    pub const fn assets(&self) -> &ParticipantAssets {
115        &self.assets
116    }
117
118    /// Select one exact participant record before opening any bus session.
119    pub fn participant(&self, id: &ParticipantId) -> Result<&RuntimeParticipant, SelectionError> {
120        self.document.participant(id)
121    }
122
123    /// Build one selected runtime-input object.
124    pub fn participant_inputs(
125        &self,
126        id: &ParticipantId,
127    ) -> Result<ParticipantRuntimeInputs, SelectionError> {
128        selected_inputs(&self.document, self.assets.clone(), id)
129    }
130
131    pub(crate) fn relocated(mut self, path: std::path::PathBuf) -> Self {
132        self.root.relocate(path.clone());
133        self.assets.relocate(path);
134        self
135    }
136}
137
138/// The exact runtime record a participant process was launched to consume.
139#[derive(Clone, Debug)]
140pub struct ParticipantBundle {
141    root: BundleRoot,
142    inputs: ParticipantRuntimeInputs,
143}
144
145impl ParticipantBundle {
146    /// Open one participant's selected runtime inputs without hashing unrelated
147    /// indexed files. Binary integrity is the supervisor's concern: the daemon
148    /// digest-verifies every staged executable when it opens the bundle, and a
149    /// launched participant proves its identity through its embedded contract.
150    pub fn open(root: impl AsRef<Path>, id: &ParticipantId) -> Result<Self, BundleError> {
151        let root = BundleRoot::open(root.as_ref())?;
152        let document = read_runtime_document(&root)?;
153        require_layout_directories(&root)?;
154        let assets = ParticipantAssets::new(root.clone(), &document.runtime().assets);
155        let inputs = selected_inputs(&document, assets, id)?;
156        Ok(Self { root, inputs })
157    }
158
159    /// The selected, coherent runtime inputs.
160    #[must_use]
161    pub const fn inputs(&self) -> &ParticipantRuntimeInputs {
162        &self.inputs
163    }
164
165    /// Consume this selected bundle into its runtime inputs.
166    #[must_use]
167    pub fn into_inputs(self) -> ParticipantRuntimeInputs {
168        self.inputs
169    }
170
171    /// The bundle root retained for diagnostics.
172    #[must_use]
173    pub fn root(&self) -> &Path {
174        self.root.path()
175    }
176}
177
178fn selected_inputs(
179    document: &RuntimeDocument,
180    assets: ParticipantAssets,
181    id: &ParticipantId,
182) -> Result<ParticipantRuntimeInputs, SelectionError> {
183    let participant = document.participant(id)?.clone();
184    let artifact = document
185        .artifacts()
186        .get(&participant.artifact)
187        .cloned()
188        .ok_or_else(|| SelectionError::MissingArtifact {
189            participant: participant.id.clone(),
190            artifact: participant.artifact.clone(),
191        })?;
192    Ok(ParticipantRuntimeInputs {
193        robot: Arc::new(document.robot().clone()),
194        participant,
195        artifact,
196        assets,
197    })
198}