1use std::str::FromStr;
18
19#[path = "envelope/domain.rs"]
20mod domain;
21#[path = "envelope/profile.rs"]
22mod profile;
23#[path = "envelope/ref_codec.rs"]
24mod ref_codec;
25
26use sim_kernel::{Error, Expr, Result, Symbol, Tick};
27use sim_value::access;
28
29use crate::buffer::{expr_kind, field, string_field, symbol_field};
30use crate::{StreamDirection, StreamItem, StreamMedia, StreamMetadata, StreamPacket};
31pub use domain::ClockDomain;
32pub use profile::{LatencyClass, StreamCapability, TransportProfile};
33use ref_codec::{ref_expr, ref_from_expr};
34
35pub const STREAM_ENVELOPE_VERSION: u32 = 1;
40
41#[derive(Clone, Debug, PartialEq, Eq)]
51pub struct StreamEnvelope {
52 version: u32,
53 stream_id: Symbol,
54 packet_id: Symbol,
55 media: StreamMedia,
56 direction: StreamDirection,
57 sequence: u64,
58 ticks: Vec<Tick>,
59 clock_domain: ClockDomain,
60 clock_domains: Vec<ClockDomain>,
61 profile: TransportProfile,
62 diagnostics: Vec<Symbol>,
63 packet: StreamPacket,
64}
65
66impl StreamEnvelope {
67 #[allow(clippy::too_many_arguments)]
73 pub fn new(
74 stream_id: Symbol,
75 packet_id: Symbol,
76 media: StreamMedia,
77 direction: StreamDirection,
78 sequence: u64,
79 ticks: Vec<Tick>,
80 clock_domain: ClockDomain,
81 profile: TransportProfile,
82 diagnostics: Vec<Symbol>,
83 packet: StreamPacket,
84 ) -> Result<Self> {
85 Self::new_with_clock_domains(
86 stream_id,
87 packet_id,
88 media,
89 direction,
90 sequence,
91 ticks,
92 clock_domain,
93 vec![clock_domain],
94 profile,
95 diagnostics,
96 packet,
97 )
98 }
99
100 #[allow(clippy::too_many_arguments)]
110 pub fn new_with_clock_domains(
111 stream_id: Symbol,
112 packet_id: Symbol,
113 media: StreamMedia,
114 direction: StreamDirection,
115 sequence: u64,
116 ticks: Vec<Tick>,
117 clock_domain: ClockDomain,
118 clock_domains: Vec<ClockDomain>,
119 profile: TransportProfile,
120 diagnostics: Vec<Symbol>,
121 packet: StreamPacket,
122 ) -> Result<Self> {
123 sim_kernel::validate_ticks(&ticks)?;
124 let packet_media = packet.media();
125 if packet_media != media {
126 return Err(Error::Eval(format!(
127 "stream envelope media {} does not match packet media {}",
128 media.symbol(),
129 packet_media.symbol()
130 )));
131 }
132 let mut all_clock_domains = clock_domains;
133 all_clock_domains.extend(
134 ticks
135 .iter()
136 .filter_map(|tick| ClockDomain::from_symbol(&tick.clock).ok()),
137 );
138 let clock_domains = normalize_clock_domains(clock_domain, all_clock_domains);
139 Ok(Self {
140 version: STREAM_ENVELOPE_VERSION,
141 stream_id,
142 packet_id,
143 media,
144 direction,
145 sequence,
146 ticks,
147 clock_domain,
148 clock_domains,
149 profile,
150 diagnostics,
151 packet,
152 })
153 }
154
155 pub fn from_item(metadata: &StreamMetadata, sequence: u64, item: &StreamItem) -> Result<Self> {
162 Self::from_item_with_profile(metadata, sequence, item, TransportProfile::memory_local())
163 }
164
165 pub fn from_item_with_profile(
173 metadata: &StreamMetadata,
174 sequence: u64,
175 item: &StreamItem,
176 profile: TransportProfile,
177 ) -> Result<Self> {
178 Self::new(
179 metadata.id().clone(),
180 packet_id(metadata.id(), sequence),
181 metadata.media(),
182 metadata.direction(),
183 sequence,
184 item.ticks().to_vec(),
185 ClockDomain::for_stream_clock(metadata.clock())?,
186 profile,
187 Vec::new(),
188 item.packet().clone(),
189 )
190 }
191
192 pub fn version(&self) -> u32 {
194 self.version
195 }
196
197 pub fn stream_id(&self) -> &Symbol {
199 &self.stream_id
200 }
201
202 pub fn packet_id(&self) -> &Symbol {
204 &self.packet_id
205 }
206
207 pub fn media(&self) -> StreamMedia {
209 self.media
210 }
211
212 pub fn direction(&self) -> StreamDirection {
214 self.direction
215 }
216
217 pub fn sequence(&self) -> u64 {
219 self.sequence
220 }
221
222 pub fn ticks(&self) -> &[Tick] {
224 &self.ticks
225 }
226
227 pub fn clock_domain(&self) -> ClockDomain {
229 self.clock_domain
230 }
231
232 pub fn clock_domains(&self) -> &[ClockDomain] {
238 &self.clock_domains
239 }
240
241 pub fn profile(&self) -> &TransportProfile {
244 &self.profile
245 }
246
247 pub fn diagnostics(&self) -> &[Symbol] {
249 &self.diagnostics
250 }
251
252 pub fn packet(&self) -> &StreamPacket {
254 &self.packet
255 }
256
257 pub fn to_expr(&self) -> Expr {
262 Expr::Map(vec![
263 (
264 Expr::Symbol(Symbol::new("envelope")),
265 Expr::Symbol(stream_envelope_tag_symbol()),
266 ),
267 (
268 Expr::Symbol(Symbol::new("version")),
269 Expr::String(self.version.to_string()),
270 ),
271 (
272 Expr::Symbol(Symbol::new("stream-id")),
273 Expr::Symbol(self.stream_id.clone()),
274 ),
275 (
276 Expr::Symbol(Symbol::new("packet-id")),
277 Expr::Symbol(self.packet_id.clone()),
278 ),
279 (
280 Expr::Symbol(Symbol::new("media")),
281 Expr::Symbol(self.media.symbol()),
282 ),
283 (
284 Expr::Symbol(Symbol::new("direction")),
285 Expr::Symbol(self.direction.symbol()),
286 ),
287 (
288 Expr::Symbol(Symbol::new("sequence")),
289 Expr::String(self.sequence.to_string()),
290 ),
291 (
292 Expr::Symbol(Symbol::new("ticks")),
293 Expr::List(self.ticks.iter().map(tick_expr).collect()),
294 ),
295 (
296 Expr::Symbol(Symbol::new("clock-domain")),
297 Expr::Symbol(self.clock_domain.symbol()),
298 ),
299 (
300 Expr::Symbol(Symbol::new("clock-domains")),
301 Expr::List(
302 self.clock_domains
303 .iter()
304 .map(|domain| Expr::Symbol(domain.symbol()))
305 .collect(),
306 ),
307 ),
308 (Expr::Symbol(Symbol::new("profile")), self.profile.to_expr()),
309 (
310 Expr::Symbol(Symbol::new("diagnostics")),
311 Expr::List(self.diagnostics.iter().cloned().map(Expr::Symbol).collect()),
312 ),
313 (Expr::Symbol(Symbol::new("packet")), self.packet.to_expr()),
314 ])
315 }
316}
317
318impl TryFrom<Expr> for StreamEnvelope {
319 type Error = Error;
320
321 fn try_from(expr: Expr) -> Result<Self> {
322 let Expr::Map(entries) = &expr else {
323 return Err(Error::TypeMismatch {
324 expected: "stream envelope map",
325 found: expr_kind(&expr),
326 });
327 };
328 ensure_fields(
329 entries,
330 &[
331 "envelope",
332 "version",
333 "stream-id",
334 "packet-id",
335 "media",
336 "direction",
337 "sequence",
338 "ticks",
339 "clock-domain",
340 "clock-domains",
341 "profile",
342 "diagnostics",
343 "packet",
344 ],
345 )?;
346 let tag = symbol_field(entries, "envelope")?;
347 if *tag != stream_envelope_tag_symbol() {
348 return Err(Error::Eval(format!(
349 "unknown stream envelope tag {}",
350 tag.as_qualified_str()
351 )));
352 }
353 let version = parse_string_field::<u32>(entries, "version")?;
354 if version != STREAM_ENVELOPE_VERSION {
355 return Err(Error::Eval(format!(
356 "unsupported stream envelope version {version}"
357 )));
358 }
359 let packet = StreamPacket::try_from(field(entries, "packet")?.clone())?;
360 let ticks = tick_list(entries, "ticks")?;
361 Self::new_with_clock_domains(
362 symbol_field(entries, "stream-id")?.clone(),
363 symbol_field(entries, "packet-id")?.clone(),
364 StreamMedia::from_symbol(symbol_field(entries, "media")?)?,
365 StreamDirection::from_symbol(symbol_field(entries, "direction")?)?,
366 parse_string_field::<u64>(entries, "sequence")?,
367 ticks,
368 ClockDomain::from_symbol(symbol_field(entries, "clock-domain")?)?,
369 clock_domain_list(entries, "clock-domains")?,
370 TransportProfile::from_expr(field(entries, "profile")?)?,
371 symbol_list(entries, "diagnostics")?.to_vec(),
372 packet,
373 )
374 }
375}
376
377pub fn stream_envelope_tag_symbol() -> Symbol {
382 Symbol::qualified("stream/envelope", "v1")
383}
384
385fn packet_id(stream_id: &Symbol, sequence: u64) -> Symbol {
386 Symbol::qualified(
387 "stream/packet-id",
388 format!("{}#{sequence}", stream_id.as_qualified_str()),
389 )
390}
391
392fn tick_expr(tick: &Tick) -> Expr {
393 Expr::Map(vec![
394 (
395 Expr::Symbol(Symbol::new("clock")),
396 Expr::Symbol(tick.clock.clone()),
397 ),
398 (Expr::Symbol(Symbol::new("index")), ref_expr(&tick.index)),
399 ])
400}
401
402fn tick_from_expr(expr: &Expr) -> Result<Tick> {
403 let Expr::Map(entries) = expr else {
404 return Err(Error::TypeMismatch {
405 expected: "stream tick map",
406 found: expr_kind(expr),
407 });
408 };
409 ensure_fields(entries, &["clock", "index"])?;
410 Ok(Tick::new(
411 symbol_field(entries, "clock")?.clone(),
412 ref_from_expr(field(entries, "index")?)?,
413 ))
414}
415
416fn parse_string_field<T>(entries: &[(Expr, Expr)], name: &str) -> Result<T>
417where
418 T: FromStr,
419 T::Err: std::fmt::Display,
420{
421 string_field(entries, name)?
422 .parse::<T>()
423 .map_err(|err| Error::Eval(format!("invalid stream envelope {name}: {err}")))
424}
425
426fn tick_list(entries: &[(Expr, Expr)], name: &str) -> Result<Vec<Tick>> {
427 list_field(entries, name)?
428 .iter()
429 .map(tick_from_expr)
430 .collect()
431}
432
433fn clock_domain_list(entries: &[(Expr, Expr)], name: &str) -> Result<Vec<ClockDomain>> {
434 symbol_list(entries, name)?
435 .iter()
436 .map(ClockDomain::from_symbol)
437 .collect()
438}
439
440fn symbol_list(entries: &[(Expr, Expr)], name: &str) -> Result<Vec<Symbol>> {
441 list_field(entries, name)?
442 .iter()
443 .map(|expr| match expr {
444 Expr::Symbol(symbol) => Ok(symbol.clone()),
445 other => Err(Error::TypeMismatch {
446 expected: "symbol list item",
447 found: expr_kind(other),
448 }),
449 })
450 .collect()
451}
452
453fn list_field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Result<&'a [Expr]> {
454 access::entry_required_list(entries, name, "list field")
455}
456
457fn ensure_fields(entries: &[(Expr, Expr)], allowed: &[&str]) -> Result<()> {
458 for (key, _) in entries {
459 let Expr::Symbol(symbol) = key else {
460 return Err(Error::TypeMismatch {
461 expected: "symbol stream envelope field",
462 found: expr_kind(key),
463 });
464 };
465 if symbol.namespace.is_none() && allowed.contains(&symbol.name.as_ref()) {
466 continue;
467 }
468 return Err(Error::Eval(format!(
469 "unknown stream envelope field {}",
470 symbol.as_qualified_str()
471 )));
472 }
473 Ok(())
474}
475
476fn normalize_clock_domains(
477 primary: ClockDomain,
478 clock_domains: Vec<ClockDomain>,
479) -> Vec<ClockDomain> {
480 let mut domains = vec![primary];
481 for domain in clock_domains {
482 if !domains.contains(&domain) {
483 domains.push(domain);
484 }
485 }
486 domains
487}