saluki_core/topology/
ids.rs

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/// 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<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    /// 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<T> {
182    name: OutputName,
183    data_ty: T,
184}
185
186impl<T> OutputDefinition<T>
187where
188    T: Copy,
189{
190    /// Creates a default output with the given data type.
191    pub const fn default_output(data_ty: T) -> Self {
192        Self {
193            name: OutputName::Default,
194            data_ty,
195        }
196    }
197
198    /// Creates a named output with the given name and data type.
199    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    /// Returns the output name.
210    ///
211    /// If this is a default output, `None` is returned.
212    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    /// Returns the data type.
220    pub fn data_ty(&self) -> T {
221        self.data_ty
222    }
223}
224
225/// A component identifier that specifies the component type.
226#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
227pub struct TypedComponentId {
228    id: ComponentId,
229    ty: ComponentType,
230}
231
232impl TypedComponentId {
233    /// Creates a new `TypedComponentId` from the given component ID and component type.
234    pub fn new(id: ComponentId, ty: ComponentType) -> Self {
235        Self { id, ty }
236    }
237
238    /// Returns a reference to the component ID.
239    pub fn component_id(&self) -> &ComponentId {
240        &self.id
241    }
242
243    /// Returns the component type.
244    pub fn component_type(&self) -> ComponentType {
245        self.ty
246    }
247
248    /// Returns the component context.
249    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    /// Consumes the `TypedComponentId` and returns its component ID, component type, and component context.
262    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/// Unique identifier for a specified output of a component, including the data type of the output.
269#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
270pub struct TypedComponentOutputId {
271    component_output: ComponentOutputId,
272    output_ty: DataType,
273}
274
275impl TypedComponentOutputId {
276    /// Creates a new `TypedComponentOutputId` from the given component output ID and output data type.
277    pub fn new(component_output: ComponentOutputId, output_ty: DataType) -> Self {
278        Self {
279            component_output,
280            output_ty,
281        }
282    }
283
284    /// Gets a reference to the component output ID.
285    pub fn component_output(&self) -> &ComponentOutputId {
286        &self.component_output
287    }
288
289    /// Returns the output data type.
290    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}