1use core::fmt;
3use std::{borrow::Cow, ops::Deref};
4
5use crate::{components::ComponentType, support::SubsystemIdentifier, topology::graph::DataType};
6
7const INVALID_COMPONENT_ID: &str = "component IDs may only contain alphanumerics (a-z, A-Z, or 0-9) and underscores, \
8 and must start and end with an alphanumeric character";
9const INVALID_COMPONENT_OUTPUT_ID: &str =
10 "component output IDs may only contain alphanumerics (a-z, A-Z, or 0-9), underscores, and up to one period \
11 separator, where each side of the separator must start and end with an alphanumeric character";
12
13#[derive(Clone, Debug, Hash, Eq, Ord, PartialEq, PartialOrd)]
18pub struct ComponentId(Cow<'static, str>);
19
20impl TryFrom<&str> for ComponentId {
21 type Error = &'static str;
22
23 fn try_from(value: &str) -> Result<Self, Self::Error> {
24 if !validate_component_id(value, false) {
25 Err(INVALID_COMPONENT_ID)
26 } else {
27 Ok(Self(value.to_string().into()))
28 }
29 }
30}
31
32impl Deref for ComponentId {
33 type Target = str;
34
35 fn deref(&self) -> &Self::Target {
36 self.0.as_ref()
37 }
38}
39
40impl fmt::Display for ComponentId {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 self.0.fmt(f)
43 }
44}
45
46#[derive(Clone, Debug, Hash, Eq, Ord, PartialEq, PartialOrd)]
48pub struct ComponentOutputId(Cow<'static, str>);
49
50impl ComponentOutputId {
51 pub fn from_definition<T: Copy>(
58 component_id: ComponentId, output_def: &OutputDefinition<T>,
59 ) -> Result<Self, (String, &'static str)> {
60 match output_def.output_name() {
61 None => Ok(Self(component_id.0)),
62 Some(output_name) => {
63 let output_id = format!("{}.{}", component_id.0, output_name);
64
65 if validate_component_id(&output_id, true) {
66 Ok(Self(output_id.into()))
67 } else {
68 Err((output_id, INVALID_COMPONENT_OUTPUT_ID))
69 }
70 }
71 }
72 }
73
74 pub fn component_id(&self) -> ComponentId {
76 if let Some((component_id, _)) = self.0.split_once('.') {
77 ComponentId(component_id.to_string().into())
78 } else {
79 ComponentId(self.0.clone())
80 }
81 }
82
83 pub fn output(&self) -> OutputName {
85 if let Some((_, output_name)) = self.0.split_once('.') {
86 OutputName::Given(output_name.to_string().into())
87 } else {
88 OutputName::Default
89 }
90 }
91
92 pub fn is_default(&self) -> bool {
94 self.0.split_once('.').is_none()
95 }
96}
97
98impl TryFrom<&str> for ComponentOutputId {
99 type Error = &'static str;
100
101 fn try_from(value: &str) -> Result<Self, Self::Error> {
102 if !validate_component_id(value, true) {
103 Err(INVALID_COMPONENT_OUTPUT_ID)
104 } else {
105 Ok(Self(value.to_string().into()))
106 }
107 }
108}
109
110impl fmt::Display for ComponentOutputId {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 self.0.fmt(f)
113 }
114}
115
116const fn validate_component_id(id: &str, as_output_id: bool) -> bool {
122 let id_bytes = id.as_bytes();
123 let end = id_bytes.len();
124
125 if end == 0 {
127 return false;
128 }
129
130 let mut idx = 0;
134 let mut segment_start = 0;
135 let mut seen_separator = false;
136 while idx < end {
137 let b = id_bytes[idx];
138 if b == b'.' {
139 if !as_output_id || seen_separator {
140 return false;
142 }
143 seen_separator = true;
144
145 if !is_valid_component_id_segment(id_bytes, segment_start, idx) {
147 return false;
148 }
149 segment_start = idx + 1;
150 } else if !b.is_ascii_alphanumeric() && b != b'_' {
151 return false;
153 }
154
155 idx += 1;
156 }
157
158 is_valid_component_id_segment(id_bytes, segment_start, end)
160}
161
162const fn is_valid_component_id_segment(bytes: &[u8], start: usize, end: usize) -> bool {
168 if start >= end {
170 return false;
171 }
172
173 bytes[start].is_ascii_alphanumeric() && bytes[end - 1].is_ascii_alphanumeric()
175}
176
177#[derive(Clone, Debug, Eq, Hash, PartialEq)]
184pub enum OutputName {
185 Default,
187
188 Given(Cow<'static, str>),
190}
191
192impl fmt::Display for OutputName {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 match self {
195 OutputName::Default => write!(f, "_default"),
196 OutputName::Given(name) => write!(f, "{}", name),
197 }
198 }
199}
200
201#[derive(Clone, Debug)]
206pub struct OutputDefinition<T> {
207 name: OutputName,
208 data_ty: T,
209}
210
211impl<T> OutputDefinition<T>
212where
213 T: Copy,
214{
215 pub const fn default_output(data_ty: T) -> Self {
217 Self {
218 name: OutputName::Default,
219 data_ty,
220 }
221 }
222
223 pub fn named_output<S>(name: S, data_ty: T) -> Self
225 where
226 S: Into<Cow<'static, str>>,
227 {
228 Self {
229 name: OutputName::Given(name.into()),
230 data_ty,
231 }
232 }
233
234 pub fn output_name(&self) -> Option<&str> {
238 match &self.name {
239 OutputName::Default => None,
240 OutputName::Given(name) => Some(name.as_ref()),
241 }
242 }
243
244 pub fn data_ty(&self) -> T {
246 self.data_ty
247 }
248}
249
250#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
252pub struct TypedComponentId {
253 id: ComponentId,
254 ty: ComponentType,
255}
256
257impl TypedComponentId {
258 pub fn new(id: ComponentId, ty: ComponentType) -> Self {
260 Self { id, ty }
261 }
262
263 pub fn component_id(&self) -> &ComponentId {
265 &self.id
266 }
267
268 pub fn component_type(&self) -> ComponentType {
270 self.ty
271 }
272
273 pub fn into_parts(self) -> (ComponentId, ComponentType) {
275 (self.id, self.ty)
276 }
277}
278
279#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
281pub struct TypedComponentOutputId {
282 component_output: ComponentOutputId,
283 output_ty: DataType,
284}
285
286impl TypedComponentOutputId {
287 pub fn new(component_output: ComponentOutputId, output_ty: DataType) -> Self {
289 Self {
290 component_output,
291 output_ty,
292 }
293 }
294
295 pub fn component_output(&self) -> &ComponentOutputId {
297 &self.component_output
298 }
299
300 pub fn output_ty(&self) -> DataType {
302 self.output_ty
303 }
304}
305
306pub struct Single;
308
309pub struct Multiple;
311
312pub trait AsComponentIds<Marker> {
318 fn as_component_ids(&self) -> impl Iterator<Item: AsRef<str>>;
324}
325
326impl<T> AsComponentIds<Single> for T
327where
328 T: AsRef<str>,
329{
330 fn as_component_ids(&self) -> impl Iterator<Item: AsRef<str>> {
331 std::iter::once(self)
332 }
333}
334
335impl<I> AsComponentIds<Multiple> for I
336where
337 for<'a> &'a I: IntoIterator<Item: AsRef<str>>,
338{
339 fn as_component_ids(&self) -> impl Iterator<Item: AsRef<str>> {
340 self.into_iter()
341 }
342}
343
344pub(super) fn get_component_relative_identifier(
345 component_type: ComponentType, component_id: &ComponentId,
346) -> SubsystemIdentifier {
347 SubsystemIdentifier::from_segments([component_type.as_category_str(), component_id])
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353
354 #[test]
355 fn component_id() {
356 let id = ComponentId::try_from("component").unwrap();
357 assert_eq!(id, ComponentId::try_from("component").unwrap());
358 assert_eq!(&*id, "component");
359
360 let id = ComponentId::try_from("component_1").unwrap();
361 assert_eq!(id, ComponentId::try_from("component_1").unwrap());
362 assert_eq!(&*id, "component_1");
363 }
364
365 #[test]
366 fn component_id_invalid() {
367 assert!(ComponentId::try_from("").is_err());
368 assert!(ComponentId::try_from("non_alphanumeric_$#!").is_err());
369 assert!(ComponentId::try_from("cant_have_periods_for_non_component_output_id.foo").is_err());
370
371 assert!(ComponentId::try_from("dsd-in").is_err());
374 assert!(ComponentId::try_from("foo-bar").is_err());
375
376 assert!(ComponentId::try_from("_foo").is_err());
379 assert!(ComponentId::try_from("foo_").is_err());
380 assert!(ComponentId::try_from("--").is_err());
381 }
382
383 #[test]
384 fn component_id_is_sanitization_fixed_point() {
385 use crate::runtime::get_sanitized_name;
390
391 let cases = [
392 "component",
393 "component_1",
394 "foo__bar",
395 "a",
396 "0",
397 "dsd-mapper",
398 "foo-bar",
399 "_foo",
400 "foo_",
401 "_foo_",
402 "--",
403 "",
404 "foo.bar",
405 "foo bar",
406 "foo$bar",
407 ];
408
409 for case in cases {
410 let is_valid = ComponentId::try_from(case).is_ok();
411
412 let is_canonical = !case.is_empty() && &*get_sanitized_name(case) == case;
415
416 assert_eq!(
417 is_valid, is_canonical,
418 "ComponentId validity must match the sanitization fixed point for {case:?}: valid={is_valid}, canonical={is_canonical}"
419 );
420 }
421 }
422
423 #[test]
424 fn component_output_id_default() {
425 let id = ComponentOutputId::try_from("component").unwrap();
426 assert_eq!(id.component_id(), ComponentId::try_from("component").unwrap());
427 assert_eq!(id.output(), OutputName::Default);
428 assert!(id.is_default());
429 }
430
431 #[test]
432 fn component_output_id_named() {
433 let id = ComponentOutputId::try_from("component.metrics").unwrap();
434 assert_eq!(id.component_id(), ComponentId::try_from("component").unwrap());
435 assert_eq!(id.output(), OutputName::Given("metrics".into()));
436 assert!(!id.is_default());
437 }
438
439 #[test]
440 fn component_output_id_invalid() {
441 assert!(ComponentOutputId::try_from("").is_err());
442 assert!(ComponentOutputId::try_from("non_alphanumeric_$#!").is_err());
443 assert!(ComponentOutputId::try_from("too.many.periods").is_err());
444 assert!(ComponentOutputId::try_from(".one_side_of_named_output_is_empty").is_err());
445 assert!(ComponentOutputId::try_from("one_side_of_named_output_is_empty.").is_err());
446 }
447
448 #[test]
449 fn component_output_id_from_definition() {
450 use crate::data_model::event::EventType;
451
452 let component_id = ComponentId::try_from("comp").expect("component ID should be valid");
453
454 let default_def = OutputDefinition::default_output(EventType::EventD);
456 let default_id =
457 ComponentOutputId::from_definition(component_id.clone(), &default_def).expect("default output is valid");
458 assert_eq!(default_id, ComponentOutputId::try_from("comp").unwrap());
459 assert!(default_id.is_default());
460
461 let named_def = OutputDefinition::named_output("errors", EventType::EventD);
463 let named_id =
464 ComponentOutputId::from_definition(component_id.clone(), &named_def).expect("named output is valid");
465 assert_eq!(named_id, ComponentOutputId::try_from("comp.errors").unwrap());
466 assert!(!named_id.is_default());
467
468 let invalid_def = OutputDefinition::named_output("bad name", EventType::EventD);
471 let err = ComponentOutputId::from_definition(component_id, &invalid_def)
472 .expect_err("an invalid generated output ID must be rejected");
473 assert_eq!(err, ("comp.bad name".to_string(), INVALID_COMPONENT_OUTPUT_ID));
474 }
475}
476
477#[cfg(test)]
478mod property_tests {
479 use proptest::prelude::*;
480
481 use super::ComponentId;
482 use crate::runtime::get_sanitized_name;
483
484 proptest! {
485 #[test]
486 fn property_test_component_id_sanitized_name_equality_ascii(s in "[A-Za-z0-9_.\\- ]{0,16}") {
487 let is_valid = ComponentId::try_from(s.as_str()).is_ok();
493 let is_canonical = !s.is_empty() && &*get_sanitized_name(&s) == s.as_str();
494 prop_assert_eq!(is_valid, is_canonical, "ComponentId validity must match canonical form for {:?}", s);
495 }
496
497 #[test]
498 fn property_test_valid_component_id_always_canonical(s in ".{0,16}") {
499 if ComponentId::try_from(s.as_str()).is_ok() {
507 prop_assert!(!s.is_empty(), "an accepted ComponentId must be non-empty");
508 prop_assert_eq!(&*get_sanitized_name(&s), s.as_str(), "an accepted ComponentId must already be canonical");
509 }
510 }
511 }
512}