Skip to main content

agp_config/component/
id.rs

1// Copyright AGNTCY Contributors (https://github.com/agntcy)
2// SPDX-License-Identifier: Apache-2.0
3
4use regex::Regex;
5use std::fmt;
6use thiserror::Error;
7
8#[derive(Error, Debug)]
9pub enum IdError {
10    #[error("id cannot be emapty")]
11    Empty,
12    #[error("kind contains invalid character(s): {0}")]
13    InvalidCharacter(String),
14    #[error("name part is too long: {0}")]
15    NameTooLong(String),
16    #[error("unknown error")]
17    Unknown,
18}
19
20// Constant for the separator used in composite keys
21const KIND_AND_NAME_SEPARATOR: &str = "/";
22
23// Regex patterns for validating kind and names
24lazy_static::lazy_static! {
25    static ref KIND_REGEX: Regex = Regex::new(r"^[a-zA-Z][0-9a-zA-Z_]{0,62}$").unwrap();
26    static ref NAME_REGEX: Regex = Regex::new(r"^[^\p{Z}\p{C}\p{S}]+$").unwrap();
27}
28
29/// Kind represents the type of a component.
30#[derive(Debug, Clone, PartialEq, Eq, Hash)]
31pub struct Kind {
32    name: String,
33}
34
35impl Kind {
36    /// Create a new Kind
37    pub fn new(ty: &str) -> Result<Self, IdError> {
38        if ty.is_empty() {
39            return Err(IdError::Empty);
40        }
41        if !KIND_REGEX.is_match(ty) {
42            return Err(IdError::InvalidCharacter(ty.to_string()));
43        }
44        Ok(Kind {
45            name: ty.to_string(),
46        })
47    }
48}
49
50impl fmt::Display for Kind {
51    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
52        write!(f, "{}", self.name)
53    }
54}
55
56/// ID represents a unique identifier for a component.
57#[derive(Debug, Hash, PartialEq, Eq, Clone)]
58pub struct ID {
59    kind_val: Kind,
60    name_val: String,
61}
62
63impl ID {
64    /// Create a new ID
65    pub fn new(kind_val: Kind) -> Self {
66        ID {
67            kind_val,
68            name_val: String::new(),
69        }
70    }
71
72    /// Create a new ID with a name
73    pub fn new_with_name(kind_val: Kind, name_val: &str) -> Result<Self, IdError> {
74        validate_name(name_val)?;
75        Ok(ID {
76            kind_val,
77            name_val: name_val.to_string(),
78        })
79    }
80
81    pub fn new_with_str(kind_and_name: &str) -> Result<Self, IdError> {
82        let (kind, name) = kind_and_name
83            .split_once(KIND_AND_NAME_SEPARATOR)
84            .unwrap_or(("", ""));
85
86        if kind.is_empty() {
87            return Err(IdError::Empty);
88        }
89
90        let kind_val = Kind::new(kind)?;
91
92        ID::new_with_name(kind_val, name)
93    }
94
95    /// Get the kind of the ID
96    pub fn kind(&self) -> &Kind {
97        &self.kind_val
98    }
99
100    /// Get the name of the ID
101    pub fn name(&self) -> &str {
102        &self.name_val
103    }
104
105    /// Marshal the ID to a string
106    pub fn marshal_text(&self) -> String {
107        self.to_string()
108    }
109}
110
111/// Implement Display for ID
112impl fmt::Display for ID {
113    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
114        if self.name_val.is_empty() {
115            write!(f, "{}", self.kind_val)
116        } else {
117            write!(
118                f,
119                "{}{}{}",
120                self.kind_val, KIND_AND_NAME_SEPARATOR, self.name_val
121            )
122        }
123    }
124}
125
126/// Validate the name part of an ID
127fn validate_name(name_str: &str) -> Result<(), IdError> {
128    // it is ok if the name is empty
129    if name_str.is_empty() {
130        return Ok(());
131    }
132
133    if name_str.len() > 1024 {
134        return Err(IdError::NameTooLong(name_str.to_string()));
135    }
136    if !NAME_REGEX.is_match(name_str) {
137        return Err(IdError::InvalidCharacter(name_str.to_string()));
138    }
139    Ok(())
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn test_kind_creation() {
148        let valid_kind = Kind::new("validKind").unwrap();
149        assert_eq!(valid_kind.to_string(), "validKind");
150
151        assert!(Kind::new("").is_err());
152        assert!(Kind::new("123Invalid").is_err());
153    }
154
155    #[test]
156    fn test_id_creation() {
157        let kind_val = Kind::new("validKind").unwrap();
158
159        let id = ID::new(kind_val.clone());
160        assert_eq!(id.kind().to_string(), "validKind");
161        assert!(id.name().is_empty());
162
163        let id_with_name = ID::new_with_name(kind_val.clone(), "validName").unwrap();
164        assert_eq!(id_with_name.name(), "validName");
165
166        assert!(ID::new_with_name(kind_val.clone(), "").is_ok());
167        assert!(ID::new_with_name(kind_val.clone(), "Invalid Name!").is_err());
168    }
169}