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 alphanumeric characters (a-z, A-Z, or 0-9), underscores, and hyphens";
11const INVALID_COMPONENT_OUTPUT_ID: &str = "component IDs may only contain alphanumeric characters (a-z, A-Z, or 0-9), underscores, hyphens, and an optional period";
12
13/// A component identifier.
14#[derive(Clone, Debug, Hash, Eq, Ord, PartialEq, PartialOrd)]
15pub struct ComponentId(Cow<'static, str>);
16
17impl TryFrom<&str> for ComponentId {
18    type Error = &'static str;
19
20    fn try_from(value: &str) -> Result<Self, Self::Error> {
21        if !validate_component_id(value, false) {
22            Err(INVALID_COMPONENT_ID)
23        } else {
24            Ok(Self(value.to_string().into()))
25        }
26    }
27}
28
29impl Deref for ComponentId {
30    type Target = str;
31
32    fn deref(&self) -> &Self::Target {
33        self.0.as_ref()
34    }
35}
36
37impl fmt::Display for ComponentId {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        self.0.fmt(f)
40    }
41}
42
43/// A component output identifier.
44#[derive(Clone, Debug, Hash, Eq, Ord, PartialEq, PartialOrd)]
45pub struct ComponentOutputId(Cow<'static, str>);
46
47impl ComponentOutputId {
48    /// Creates a new `ComponentOutputId` from an identifier and output definition.
49    ///
50    /// ## Errors
51    ///
52    /// If generated component output ID is not valid, such as due the identifier or output definition containing
53    /// invalid characters, an error is returned.
54    pub fn from_definition(component_id: ComponentId, output_def: &OutputDefinition) -> Result<Self, (String, String)> {
55        match output_def.output_name() {
56            None => Ok(Self(component_id.0)),
57            Some(output_name) => {
58                let output_id = format!("{}.{}", component_id.0, output_name);
59
60                if validate_component_id(&output_id, true) {
61                    Ok(Self(output_id.into()))
62                } else {
63                    // TODO: make the error reason better
64                    Err((output_id, "invalid".to_string()))
65                }
66            }
67        }
68    }
69
70    /// Returns the component ID.
71    pub fn component_id(&self) -> ComponentId {
72        if let Some((component_id, _)) = self.0.split_once('.') {
73            ComponentId(component_id.to_string().into())
74        } else {
75            ComponentId(self.0.clone())
76        }
77    }
78
79    /// Returns the output name.
80    pub fn output(&self) -> OutputName {
81        if let Some((_, output_name)) = self.0.split_once('.') {
82            OutputName::Given(output_name.to_string().into())
83        } else {
84            OutputName::Default
85        }
86    }
87
88    /// Returns `true` if this is a default output.
89    pub fn is_default(&self) -> bool {
90        self.0.split_once('.').is_none()
91    }
92}
93
94impl TryFrom<&str> for ComponentOutputId {
95    type Error = &'static str;
96
97    fn try_from(value: &str) -> Result<Self, Self::Error> {
98        if !validate_component_id(value, true) {
99            Err(INVALID_COMPONENT_OUTPUT_ID)
100        } else {
101            Ok(Self(value.to_string().into()))
102        }
103    }
104}
105
106impl fmt::Display for ComponentOutputId {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        self.0.fmt(f)
109    }
110}
111
112const fn validate_component_id(id: &str, as_output_id: bool) -> bool {
113    let id_bytes = id.as_bytes();
114
115    // Identifiers cannot be empty strings.
116    if id_bytes.is_empty() {
117        return false;
118    }
119
120    // Keep track of whether or not we've seen a period yet. If we have, we track its index, which serves two purposes:
121    // figure out if we see _another_ period (can only have one), and ensure that either side of the string (when split
122    // by the separator) isn't empty.
123    let mut idx = 0;
124    let end = id_bytes.len();
125    let mut separator_idx = end;
126    while idx < end {
127        let b = id_bytes[idx];
128        if !b.is_ascii_alphanumeric() && b != b'_' && b != b'-' {
129            if as_output_id && b == b'.' && separator_idx == end {
130                // Found our period separator.
131                separator_idx = idx;
132            } else {
133                // We're not validating as an output ID, or we already saw a period separator, which means this is
134                // invalid.
135                return false;
136            }
137        }
138
139        idx += 1;
140    }
141
142    if as_output_id && (separator_idx == 0 || separator_idx == end - 1) {
143        // Can't have the separator as the first or last character.
144        return false;
145    }
146
147    true
148}
149
150/// An output name.
151///
152/// Components must always have at least one output, but an output can either be the default output or a named output.
153/// This allows for components to have multiple outputs, potentially with one (the default) acting as a catch-all.
154///
155/// `OutputName` is used to differentiate between a default output and named outputs.
156#[derive(Clone, Debug, Eq, Hash, PartialEq)]
157pub enum OutputName {
158    /// Default output.
159    Default,
160
161    /// Named output.
162    Given(Cow<'static, str>),
163}
164
165impl fmt::Display for OutputName {
166    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167        match self {
168            OutputName::Default => write!(f, "_default"),
169            OutputName::Given(name) => write!(f, "{}", name),
170        }
171    }
172}
173
174/// An output definition.
175///
176/// Outputs are a combination of the output name and data type, which defines the data type (or types) of events that
177/// can be emitted from a particular component output.
178#[derive(Clone, Debug)]
179pub struct OutputDefinition {
180    name: OutputName,
181    event_ty: EventType,
182}
183
184impl OutputDefinition {
185    /// Creates a default output with the given data type.
186    pub const fn default_output(event_ty: EventType) -> Self {
187        Self {
188            name: OutputName::Default,
189            event_ty,
190        }
191    }
192
193    /// Creates a named output with the given name and data type.
194    pub fn named_output<S>(name: S, event_ty: EventType) -> Self
195    where
196        S: Into<Cow<'static, str>>,
197    {
198        Self {
199            name: OutputName::Given(name.into()),
200            event_ty,
201        }
202    }
203
204    /// Returns the output name.
205    ///
206    /// If this is a default output, `None` is returned.
207    pub fn output_name(&self) -> Option<&str> {
208        match &self.name {
209            OutputName::Default => None,
210            OutputName::Given(name) => Some(name.as_ref()),
211        }
212    }
213
214    /// Returns the event type.
215    pub fn event_ty(&self) -> EventType {
216        self.event_ty
217    }
218}
219
220/// A component identifier that specifies the component type.
221#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
222pub struct TypedComponentId {
223    id: ComponentId,
224    ty: ComponentType,
225}
226
227impl TypedComponentId {
228    /// Creates a new `TypedComponentId` from the given component ID and component type.
229    pub fn new(id: ComponentId, ty: ComponentType) -> Self {
230        Self { id, ty }
231    }
232
233    /// Returns a reference to the component ID.
234    pub fn component_id(&self) -> &ComponentId {
235        &self.id
236    }
237
238    /// Returns the component type.
239    pub fn component_type(&self) -> ComponentType {
240        self.ty
241    }
242
243    /// Returns the component context.
244    pub fn component_context(&self) -> ComponentContext {
245        match self.ty {
246            ComponentType::Source => ComponentContext::source(self.id.clone()),
247            ComponentType::Transform => ComponentContext::transform(self.id.clone()),
248            ComponentType::Destination => ComponentContext::destination(self.id.clone()),
249            ComponentType::Encoder => ComponentContext::encoder(self.id.clone()),
250            ComponentType::Forwarder => ComponentContext::forwarder(self.id.clone()),
251        }
252    }
253
254    /// Consumes the `TypedComponentId` and returns its component ID, component type, and component context.
255    pub fn into_parts(self) -> (ComponentId, ComponentType, ComponentContext) {
256        let component_context = self.component_context();
257        (self.id, self.ty, component_context)
258    }
259}
260
261/// Unique identifier for a specified output of a component, including the data type of the output.
262#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
263pub struct TypedComponentOutputId {
264    component_output: ComponentOutputId,
265    output_ty: EventType,
266}
267
268impl TypedComponentOutputId {
269    /// Creates a new `TypedComponentOutputId` from the given component output ID and output data type.
270    pub fn new(component_output: ComponentOutputId, output_ty: EventType) -> Self {
271        Self {
272            component_output,
273            output_ty,
274        }
275    }
276
277    /// Gets a reference to the component output ID.
278    pub fn component_output(&self) -> &ComponentOutputId {
279        &self.component_output
280    }
281
282    /// Returns the output data type.
283    pub fn output_ty(&self) -> EventType {
284        self.output_ty
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    #[test]
293    fn component_id() {
294        let id = ComponentId::try_from("component").unwrap();
295        assert_eq!(id, ComponentId::try_from("component").unwrap());
296        assert_eq!(&*id, "component");
297
298        let id = ComponentId::try_from("component_1").unwrap();
299        assert_eq!(id, ComponentId::try_from("component_1").unwrap());
300        assert_eq!(&*id, "component_1");
301    }
302
303    #[test]
304    fn component_id_invalid() {
305        assert!(ComponentId::try_from("").is_err());
306        assert!(ComponentId::try_from("non_alphanumeric_$#!").is_err());
307        assert!(ComponentId::try_from("cant_have_periods_for_non_component_output_id.foo").is_err());
308    }
309
310    #[test]
311    fn component_output_id_default() {
312        let id = ComponentOutputId::try_from("component").unwrap();
313        assert_eq!(id.component_id(), ComponentId::try_from("component").unwrap());
314        assert_eq!(id.output(), OutputName::Default);
315        assert!(id.is_default());
316    }
317
318    #[test]
319    fn component_output_id_named() {
320        let id = ComponentOutputId::try_from("component.metrics").unwrap();
321        assert_eq!(id.component_id(), ComponentId::try_from("component").unwrap());
322        assert_eq!(id.output(), OutputName::Given("metrics".into()));
323        assert!(!id.is_default());
324    }
325
326    #[test]
327    fn component_output_id_invalid() {
328        assert!(ComponentOutputId::try_from("").is_err());
329        assert!(ComponentOutputId::try_from("non_alphanumeric_$#!").is_err());
330        assert!(ComponentOutputId::try_from("too.many.periods").is_err());
331        assert!(ComponentOutputId::try_from(".one_side_of_named_output_is_empty").is_err());
332        assert!(ComponentOutputId::try_from("one_side_of_named_output_is_empty.").is_err());
333    }
334}