1use core::fmt;
2use std::{borrow::Cow, ops::Deref};
3
4use crate::{
5 components::{ComponentContext, ComponentType},
6 topology::graph::DataType,
7};
8
9const INVALID_COMPONENT_ID: &str =
10 "component IDs may only contain alphanumerics (a-z, A-Z, or 0-9), underscores, and hyphens";
11const INVALID_COMPONENT_OUTPUT_ID: &str =
12 "component output IDs may only contain alphanumerics (a-z, A-Z, or 0-9), underscores, hyphens, and up to one period";
13
14#[derive(Clone, Debug, Hash, Eq, Ord, PartialEq, PartialOrd)]
16pub struct ComponentId(Cow<'static, str>);
17
18impl TryFrom<&str> for ComponentId {
19 type Error = &'static str;
20
21 fn try_from(value: &str) -> Result<Self, Self::Error> {
22 if !validate_component_id(value, false) {
23 Err(INVALID_COMPONENT_ID)
24 } else {
25 Ok(Self(value.to_string().into()))
26 }
27 }
28}
29
30impl Deref for ComponentId {
31 type Target = str;
32
33 fn deref(&self) -> &Self::Target {
34 self.0.as_ref()
35 }
36}
37
38impl fmt::Display for ComponentId {
39 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40 self.0.fmt(f)
41 }
42}
43
44#[derive(Clone, Debug, Hash, Eq, Ord, PartialEq, PartialOrd)]
46pub struct ComponentOutputId(Cow<'static, str>);
47
48impl ComponentOutputId {
49 pub fn from_definition<T: Copy>(
56 component_id: ComponentId, output_def: &OutputDefinition<T>,
57 ) -> Result<Self, (String, &'static str)> {
58 match output_def.output_name() {
59 None => Ok(Self(component_id.0)),
60 Some(output_name) => {
61 let output_id = format!("{}.{}", component_id.0, output_name);
62
63 if validate_component_id(&output_id, true) {
64 Ok(Self(output_id.into()))
65 } else {
66 Err((output_id, INVALID_COMPONENT_OUTPUT_ID))
67 }
68 }
69 }
70 }
71
72 pub fn component_id(&self) -> ComponentId {
74 if let Some((component_id, _)) = self.0.split_once('.') {
75 ComponentId(component_id.to_string().into())
76 } else {
77 ComponentId(self.0.clone())
78 }
79 }
80
81 pub fn output(&self) -> OutputName {
83 if let Some((_, output_name)) = self.0.split_once('.') {
84 OutputName::Given(output_name.to_string().into())
85 } else {
86 OutputName::Default
87 }
88 }
89
90 pub fn is_default(&self) -> bool {
92 self.0.split_once('.').is_none()
93 }
94}
95
96impl TryFrom<&str> for ComponentOutputId {
97 type Error = &'static str;
98
99 fn try_from(value: &str) -> Result<Self, Self::Error> {
100 if !validate_component_id(value, true) {
101 Err(INVALID_COMPONENT_OUTPUT_ID)
102 } else {
103 Ok(Self(value.to_string().into()))
104 }
105 }
106}
107
108impl fmt::Display for ComponentOutputId {
109 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110 self.0.fmt(f)
111 }
112}
113
114const fn validate_component_id(id: &str, as_output_id: bool) -> bool {
115 let id_bytes = id.as_bytes();
116
117 if id_bytes.is_empty() {
119 return false;
120 }
121
122 let mut idx = 0;
126 let end = id_bytes.len();
127 let mut separator_idx = end;
128 while idx < end {
129 let b = id_bytes[idx];
130 if !b.is_ascii_alphanumeric() && b != b'_' && b != b'-' {
131 if as_output_id && b == b'.' && separator_idx == end {
132 separator_idx = idx;
134 } else {
135 return false;
138 }
139 }
140
141 idx += 1;
142 }
143
144 if as_output_id && (separator_idx == 0 || separator_idx == end - 1) {
145 return false;
147 }
148
149 true
150}
151
152#[derive(Clone, Debug, Eq, Hash, PartialEq)]
159pub enum OutputName {
160 Default,
162
163 Given(Cow<'static, str>),
165}
166
167impl fmt::Display for OutputName {
168 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169 match self {
170 OutputName::Default => write!(f, "_default"),
171 OutputName::Given(name) => write!(f, "{}", name),
172 }
173 }
174}
175
176#[derive(Clone, Debug)]
181pub struct OutputDefinition<T> {
182 name: OutputName,
183 data_ty: T,
184}
185
186impl<T> OutputDefinition<T>
187where
188 T: Copy,
189{
190 pub const fn default_output(data_ty: T) -> Self {
192 Self {
193 name: OutputName::Default,
194 data_ty,
195 }
196 }
197
198 pub fn named_output<S>(name: S, data_ty: T) -> Self
200 where
201 S: Into<Cow<'static, str>>,
202 {
203 Self {
204 name: OutputName::Given(name.into()),
205 data_ty,
206 }
207 }
208
209 pub fn output_name(&self) -> Option<&str> {
213 match &self.name {
214 OutputName::Default => None,
215 OutputName::Given(name) => Some(name.as_ref()),
216 }
217 }
218
219 pub fn data_ty(&self) -> T {
221 self.data_ty
222 }
223}
224
225#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
227pub struct TypedComponentId {
228 id: ComponentId,
229 ty: ComponentType,
230}
231
232impl TypedComponentId {
233 pub fn new(id: ComponentId, ty: ComponentType) -> Self {
235 Self { id, ty }
236 }
237
238 pub fn component_id(&self) -> &ComponentId {
240 &self.id
241 }
242
243 pub fn component_type(&self) -> ComponentType {
245 self.ty
246 }
247
248 pub fn component_context(&self) -> ComponentContext {
250 match self.ty {
251 ComponentType::Source => ComponentContext::source(self.id.clone()),
252 ComponentType::Relay => ComponentContext::relay(self.id.clone()),
253 ComponentType::Decoder => ComponentContext::decoder(self.id.clone()),
254 ComponentType::Transform => ComponentContext::transform(self.id.clone()),
255 ComponentType::Encoder => ComponentContext::encoder(self.id.clone()),
256 ComponentType::Forwarder => ComponentContext::forwarder(self.id.clone()),
257 ComponentType::Destination => ComponentContext::destination(self.id.clone()),
258 }
259 }
260
261 pub fn into_parts(self) -> (ComponentId, ComponentType, ComponentContext) {
263 let component_context = self.component_context();
264 (self.id, self.ty, component_context)
265 }
266}
267
268#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
270pub struct TypedComponentOutputId {
271 component_output: ComponentOutputId,
272 output_ty: DataType,
273}
274
275impl TypedComponentOutputId {
276 pub fn new(component_output: ComponentOutputId, output_ty: DataType) -> Self {
278 Self {
279 component_output,
280 output_ty,
281 }
282 }
283
284 pub fn component_output(&self) -> &ComponentOutputId {
286 &self.component_output
287 }
288
289 pub fn output_ty(&self) -> DataType {
291 self.output_ty
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
300 fn component_id() {
301 let id = ComponentId::try_from("component").unwrap();
302 assert_eq!(id, ComponentId::try_from("component").unwrap());
303 assert_eq!(&*id, "component");
304
305 let id = ComponentId::try_from("component_1").unwrap();
306 assert_eq!(id, ComponentId::try_from("component_1").unwrap());
307 assert_eq!(&*id, "component_1");
308 }
309
310 #[test]
311 fn component_id_invalid() {
312 assert!(ComponentId::try_from("").is_err());
313 assert!(ComponentId::try_from("non_alphanumeric_$#!").is_err());
314 assert!(ComponentId::try_from("cant_have_periods_for_non_component_output_id.foo").is_err());
315 }
316
317 #[test]
318 fn component_output_id_default() {
319 let id = ComponentOutputId::try_from("component").unwrap();
320 assert_eq!(id.component_id(), ComponentId::try_from("component").unwrap());
321 assert_eq!(id.output(), OutputName::Default);
322 assert!(id.is_default());
323 }
324
325 #[test]
326 fn component_output_id_named() {
327 let id = ComponentOutputId::try_from("component.metrics").unwrap();
328 assert_eq!(id.component_id(), ComponentId::try_from("component").unwrap());
329 assert_eq!(id.output(), OutputName::Given("metrics".into()));
330 assert!(!id.is_default());
331 }
332
333 #[test]
334 fn component_output_id_invalid() {
335 assert!(ComponentOutputId::try_from("").is_err());
336 assert!(ComponentOutputId::try_from("non_alphanumeric_$#!").is_err());
337 assert!(ComponentOutputId::try_from("too.many.periods").is_err());
338 assert!(ComponentOutputId::try_from(".one_side_of_named_output_is_empty").is_err());
339 assert!(ComponentOutputId::try_from("one_side_of_named_output_is_empty.").is_err());
340 }
341}