1use std::path::PathBuf;
2
3use serde::{Deserialize, Serialize};
4
5use crate::{ExecutionId, ExecutionOrigin, ProducerId};
6
7pub const DEFAULT_SHUTDOWN_GRACE_MS: u64 = 2000;
9
10pub mod env {
12 pub const PARTICIPANT_ID: &str = "PHOXAL_PARTICIPANT_ID";
13 pub const EXECUTION_ID: &str = "PHOXAL_EXECUTION_ID";
14 pub const PRODUCER_ID: &str = "PHOXAL_PRODUCER_ID";
15 pub const EXECUTION_ORIGIN: &str = "PHOXAL_EXECUTION_ORIGIN";
16 pub const ROBOT_ID: &str = "PHOXAL_ROBOT_ID";
17 pub const NAMESPACE: &str = "PHOXAL_NAMESPACE";
18 pub const BUNDLE_ROOT: &str = "PHOXAL_BUNDLE_ROOT";
19 pub const COMPONENT_INSTANCE: &str = "PHOXAL_COMPONENT_INSTANCE";
20 pub const CONNECT: &str = "PHOXAL_CONNECT";
21 pub const CONFIG: &str = "PHOXAL_CONFIG";
22 pub const CLOCK: &str = "PHOXAL_CLOCK";
23
24 pub const ALL: &[&str] = &[
25 PARTICIPANT_ID,
26 EXECUTION_ID,
27 PRODUCER_ID,
28 EXECUTION_ORIGIN,
29 ROBOT_ID,
30 NAMESPACE,
31 BUNDLE_ROOT,
32 COMPONENT_INSTANCE,
33 CONNECT,
34 CONFIG,
35 CLOCK,
36 ];
37}
38
39#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
41#[serde(deny_unknown_fields)]
42pub struct ParticipantLaunch {
43 pub participant_id: String,
44 pub execution: ExecutionId,
45 pub producer: ProducerId,
46 #[serde(default, with = "origin_serde")]
47 pub execution_origin: Option<ExecutionOrigin>,
48 pub namespace: String,
49 pub robot_id: String,
50 #[serde(default)]
51 pub bus: BusProfile,
52 #[serde(default)]
53 pub clock: ClockMode,
54 #[serde(default)]
55 pub config: Option<serde_json::Value>,
56 #[serde(default)]
57 pub bundle_root: Option<PathBuf>,
58 #[serde(default)]
59 pub component_instance: Option<String>,
60 #[serde(default = "default_grace")]
61 pub shutdown_grace_ms: u64,
62}
63
64const fn default_grace() -> u64 {
65 DEFAULT_SHUTDOWN_GRACE_MS
66}
67
68impl ParticipantLaunch {
69 pub fn local(participant_id: impl Into<String>, robot_id: impl Into<String>) -> Self {
70 Self {
71 participant_id: participant_id.into(),
72 execution: ExecutionId::mint(),
73 producer: ProducerId::mint(),
74 execution_origin: None,
75 namespace: "dev".to_string(),
76 robot_id: robot_id.into(),
77 bus: BusProfile::default(),
78 clock: ClockMode::Real,
79 config: None,
80 bundle_root: None,
81 component_instance: None,
82 shutdown_grace_ms: DEFAULT_SHUTDOWN_GRACE_MS,
83 }
84 }
85
86 pub fn in_execution(mut self, execution: ExecutionId) -> Self {
87 self.execution = execution;
88 self
89 }
90
91 pub fn with_execution_origin(mut self, origin: ExecutionOrigin) -> Self {
92 self.execution_origin = Some(origin);
93 self
94 }
95
96 pub fn encode_env(&self) -> Result<Vec<(&'static str, String)>, LaunchError> {
98 let mut values = vec![
99 (env::PARTICIPANT_ID, self.participant_id.clone()),
100 (env::EXECUTION_ID, self.execution.to_string()),
101 (env::PRODUCER_ID, self.producer.to_string()),
102 (env::ROBOT_ID, self.robot_id.clone()),
103 (env::NAMESPACE, self.namespace.clone()),
104 (env::CONNECT, self.bus.connect_endpoints.join(",")),
105 (env::CLOCK, self.clock.to_string()),
106 ];
107 if let Some(origin) = self.execution_origin {
108 values.push((env::EXECUTION_ORIGIN, origin.encode()));
109 }
110 if let Some(root) = &self.bundle_root {
111 values.push((env::BUNDLE_ROOT, root.to_string_lossy().into_owned()));
112 }
113 if let Some(instance) = &self.component_instance {
114 values.push((env::COMPONENT_INSTANCE, instance.clone()));
115 }
116 if let Some(config) = &self.config {
117 values.push((env::CONFIG, serde_json::to_string(config)?));
118 }
119 Ok(values)
120 }
121
122 pub fn decode(values: LaunchEnv) -> Result<Self, LaunchError> {
124 let mut launch = Self::local(values.participant_id, values.robot_id);
125 if let Some(value) = nonempty(values.execution_id) {
126 launch.execution = ExecutionId::parse(&value)
127 .map_err(|source| LaunchError::ExecutionId { value, source })?;
128 }
129 if let Some(value) = nonempty(values.producer_id) {
130 launch.producer = ProducerId::parse(&value)
131 .map_err(|source| LaunchError::ProducerId { value, source })?;
132 }
133 if let Some(value) = nonempty(values.execution_origin) {
134 launch.execution_origin = Some(
135 ExecutionOrigin::decode(&value)
136 .ok_or_else(|| LaunchError::ExecutionOrigin(value.clone()))?,
137 );
138 }
139 launch.namespace = nonempty(values.namespace).unwrap_or_else(|| "dev".to_string());
140 launch.bus.connect_endpoints = nonempty(values.connect)
141 .map(|endpoints| {
142 endpoints
143 .split(',')
144 .map(str::trim)
145 .filter(|endpoint| !endpoint.is_empty())
146 .map(str::to_string)
147 .collect()
148 })
149 .unwrap_or_default();
150 launch.config = match nonempty(values.config) {
151 Some(value) => Some(
152 serde_json::from_str(&value)
153 .map_err(|source| LaunchError::Config { value, source })?,
154 ),
155 None => None,
156 };
157 launch.bundle_root = values
158 .bundle_root
159 .filter(|path| !path.as_os_str().is_empty());
160 launch.component_instance = nonempty(values.component_instance);
161 launch.clock = values.clock;
162 Ok(launch)
163 }
164}
165
166fn nonempty(value: Option<String>) -> Option<String> {
167 value.filter(|value| !value.is_empty())
168}
169
170#[derive(Clone, Debug, PartialEq, Eq)]
172pub struct LaunchEnv {
173 pub participant_id: String,
174 pub execution_id: Option<String>,
175 pub producer_id: Option<String>,
176 pub execution_origin: Option<String>,
177 pub robot_id: String,
178 pub namespace: Option<String>,
179 pub bundle_root: Option<PathBuf>,
180 pub component_instance: Option<String>,
181 pub connect: Option<String>,
182 pub config: Option<String>,
183 pub clock: ClockMode,
184}
185
186#[derive(Debug, thiserror::Error)]
188pub enum LaunchError {
189 #[error("PHOXAL_EXECUTION_ID is invalid ('{value}'): {source}")]
190 ExecutionId {
191 value: String,
192 source: crate::InvalidIdentity,
193 },
194 #[error("PHOXAL_PRODUCER_ID is invalid ('{value}'): {source}")]
195 ProducerId {
196 value: String,
197 source: crate::InvalidIdentity,
198 },
199 #[error("PHOXAL_EXECUTION_ORIGIN is malformed: '{0}'")]
200 ExecutionOrigin(String),
201 #[error("PHOXAL_CONFIG is not valid JSON: {source}")]
202 Config {
203 value: String,
204 source: serde_json::Error,
205 },
206 #[error("could not encode launch JSON: {0}")]
207 Json(#[from] serde_json::Error),
208}
209
210#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
212#[serde(deny_unknown_fields)]
213pub struct BusProfile {
214 #[serde(default)]
215 pub connect_endpoints: Vec<String>,
216}
217
218#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
220#[serde(rename_all = "snake_case")]
221pub enum ClockMode {
222 #[default]
223 Real,
224 Simulation,
225 Clockless,
226}
227
228impl std::fmt::Display for ClockMode {
229 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
230 formatter.write_str(match self {
231 Self::Real => "real",
232 Self::Simulation => "simulation",
233 Self::Clockless => "clockless",
234 })
235 }
236}
237
238mod origin_serde {
239 use super::ExecutionOrigin;
240 use serde::{Deserialize, Deserializer, Serialize, Serializer};
241
242 pub fn serialize<S: Serializer>(
243 value: &Option<ExecutionOrigin>,
244 serializer: S,
245 ) -> Result<S::Ok, S::Error> {
246 value.map(ExecutionOrigin::encode).serialize(serializer)
247 }
248
249 pub fn deserialize<'de, D: Deserializer<'de>>(
250 deserializer: D,
251 ) -> Result<Option<ExecutionOrigin>, D::Error> {
252 let Some(rendered) = Option::<String>::deserialize(deserializer)? else {
253 return Ok(None);
254 };
255 ExecutionOrigin::decode(&rendered)
256 .map(Some)
257 .ok_or_else(|| serde::de::Error::custom("malformed execution origin"))
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn launch_env_round_trip_uses_bundle_root_only() {
267 let launch = ParticipantLaunch {
268 bundle_root: Some(PathBuf::from("/bundle")),
269 execution_origin: Some(ExecutionOrigin::mint()),
270 ..ParticipantLaunch::local("drive", "robot")
271 };
272 let encoded = launch.encode_env().unwrap();
273 assert!(
274 encoded
275 .iter()
276 .any(|(key, value)| { *key == env::BUNDLE_ROOT && value == "/bundle" })
277 );
278 assert!(!encoded.iter().any(|(key, _)| *key == "PHOXAL_ROBOT_ROOT"));
279 }
280}