saluki_core/topology/
ids.rs

1use core::fmt;
2use std::{borrow::Cow, ops::Deref};
3
4use crate::{
5    components::{ComponentContext, ComponentType},
6    data_model::event::EventType,
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/// A component identifier.
15#[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/// A component output identifier.
45#[derive(Clone, Debug, Hash, Eq, Ord, PartialEq, PartialOrd)]
46pub struct ComponentOutputId(Cow<'static, str>);
47
48impl ComponentOutputId {
49    /// Creates a new `ComponentOutputId` from an identifier and output definition.
50    ///
51    /// # Errors
52    ///
53    /// If generated component output ID is not valid (identifier or output definition containing invalid characters,
54    /// etc), an error is returned.
55    pub fn from_definition(
56        component_id: ComponentId, output_def: &OutputDefinition,
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    /// Returns the component ID.
73    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    /// Returns the output name.
82    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    /// Returns `true` if this is a default output.
91    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    // Identifiers cannot be empty strings.
118    if id_bytes.is_empty() {
119        return false;
120    }
121
122    // Keep track of whether or not we've seen a period yet. If we have, we track its index, which serves two purposes:
123    // figure out if we see _another_ period (can only have one), and ensure that either side of the string (when split
124    // by the separator) isn't empty.
125    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                // Found our period separator.
133                separator_idx = idx;
134            } else {
135                // We're not validating as an output ID, or we already saw a period separator, which means this is
136                // invalid.
137                return false;
138            }
139        }
140
141        idx += 1;
142    }
143
144    if as_output_id && (separator_idx == 0 || separator_idx == end - 1) {
145        // Can't have the separator as the first or last character.
146        return false;
147    }
148
149    true
150}
151
152/// An output name.
153///
154/// Components must always have at least one output, but an output can either be the default output or a named output.
155/// This allows for components to have multiple outputs, potentially with one (the default) acting as a catch-all.
156///
157/// `OutputName` is used to differentiate between a default output and named outputs.
158#[derive(Clone, Debug, Eq, Hash, PartialEq)]
159pub enum OutputName {
160    /// Default output.
161    Default,
162
163    /// Named output.
164    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/// An output definition.
177///
178/// Outputs are a combination of the output name and data type, which defines the data type (or types) of events that
179/// can be emitted from a particular component output.
180#[derive(Clone, Debug)]
181pub struct OutputDefinition {
182    name: OutputName,
183    event_ty: EventType,
184}
185
186impl OutputDefinition {
187    /// Creates a default output with the given data type.
188    pub const fn default_output(event_ty: EventType) -> Self {
189        Self {
190            name: OutputName::Default,
191            event_ty,
192        }
193    }
194
195    /// Creates a named output with the given name and data type.
196    pub fn named_output<S>(name: S, event_ty: EventType) -> Self
197    where
198        S: Into<Cow<'static, str>>,
199    {
200        Self {
201            name: OutputName::Given(name.into()),
202            event_ty,
203        }
204    }
205
206    /// Returns the output name.
207    ///
208    /// If this is a default output, `None` is returned.
209    pub fn output_name(&self) -> Option<&str> {
210        match &self.name {
211            OutputName::Default => None,
212            OutputName::Given(name) => Some(name.as_ref()),
213        }
214    }
215
216    /// Returns the event type.
217    pub fn event_ty(&self) -> EventType {
218        self.event_ty
219    }
220}
221
222/// A component identifier that specifies the component type.
223#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
224pub struct TypedComponentId {
225    id: ComponentId,
226    ty: ComponentType,
227}
228
229impl TypedComponentId {
230    /// Creates a new `TypedComponentId` from the given component ID and component type.
231    pub fn new(id: ComponentId, ty: ComponentType) -> Self {
232        Self { id, ty }
233    }
234
235    /// Returns a reference to the component ID.
236    pub fn component_id(&self) -> &ComponentId {
237        &self.id
238    }
239
240    /// Returns the component type.
241    pub fn component_type(&self) -> ComponentType {
242        self.ty
243    }
244
245    /// Returns the component context.
246    pub fn component_context(&self) -> ComponentContext {
247        match self.ty {
248            ComponentType::Source => ComponentContext::source(self.id.clone()),
249            ComponentType::Transform => ComponentContext::transform(self.id.clone()),
250            ComponentType::Destination => ComponentContext::destination(self.id.clone()),
251            ComponentType::Encoder => ComponentContext::encoder(self.id.clone()),
252            ComponentType::Forwarder => ComponentContext::forwarder(self.id.clone()),
253            ComponentType::Relay => ComponentContext::relay(self.id.clone()),
254        }
255    }
256
257    /// Consumes the `TypedComponentId` and returns its component ID, component type, and component context.
258    pub fn into_parts(self) -> (ComponentId, ComponentType, ComponentContext) {
259        let component_context = self.component_context();
260        (self.id, self.ty, component_context)
261    }
262}
263
264/// Unique identifier for a specified output of a component, including the data type of the output.
265#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
266pub struct TypedComponentOutputId {
267    component_output: ComponentOutputId,
268    output_ty: EventType,
269}
270
271impl TypedComponentOutputId {
272    /// Creates a new `TypedComponentOutputId` from the given component output ID and output data type.
273    pub fn new(component_output: ComponentOutputId, output_ty: EventType) -> Self {
274        Self {
275            component_output,
276            output_ty,
277        }
278    }
279
280    /// Gets a reference to the component output ID.
281    pub fn component_output(&self) -> &ComponentOutputId {
282        &self.component_output
283    }
284
285    /// Returns the output data type.
286    pub fn output_ty(&self) -> EventType {
287        self.output_ty
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    #[test]
296    fn component_id() {
297        let id = ComponentId::try_from("component").unwrap();
298        assert_eq!(id, ComponentId::try_from("component").unwrap());
299        assert_eq!(&*id, "component");
300
301        let id = ComponentId::try_from("component_1").unwrap();
302        assert_eq!(id, ComponentId::try_from("component_1").unwrap());
303        assert_eq!(&*id, "component_1");
304    }
305
306    #[test]
307    fn component_id_invalid() {
308        assert!(ComponentId::try_from("").is_err());
309        assert!(ComponentId::try_from("non_alphanumeric_$#!").is_err());
310        assert!(ComponentId::try_from("cant_have_periods_for_non_component_output_id.foo").is_err());
311    }
312
313    #[test]
314    fn component_output_id_default() {
315        let id = ComponentOutputId::try_from("component").unwrap();
316        assert_eq!(id.component_id(), ComponentId::try_from("component").unwrap());
317        assert_eq!(id.output(), OutputName::Default);
318        assert!(id.is_default());
319    }
320
321    #[test]
322    fn component_output_id_named() {
323        let id = ComponentOutputId::try_from("component.metrics").unwrap();
324        assert_eq!(id.component_id(), ComponentId::try_from("component").unwrap());
325        assert_eq!(id.output(), OutputName::Given("metrics".into()));
326        assert!(!id.is_default());
327    }
328
329    #[test]
330    fn component_output_id_invalid() {
331        assert!(ComponentOutputId::try_from("").is_err());
332        assert!(ComponentOutputId::try_from("non_alphanumeric_$#!").is_err());
333        assert!(ComponentOutputId::try_from("too.many.periods").is_err());
334        assert!(ComponentOutputId::try_from(".one_side_of_named_output_is_empty").is_err());
335        assert!(ComponentOutputId::try_from("one_side_of_named_output_is_empty.").is_err());
336    }
337}