Skip to main content

obeli_sk_concepts/
component_id.rs

1use crate::naming::StrVariant;
2use ::serde::{Deserialize, Serialize};
3use std::{
4    fmt::{Debug, Display, Write as _},
5    hash::Hash,
6    marker::PhantomData,
7    str::FromStr,
8};
9
10#[derive(
11    Debug,
12    Clone,
13    Copy,
14    strum::Display,
15    PartialEq,
16    Eq,
17    strum::EnumString,
18    Hash,
19    serde_with::SerializeDisplay,
20    serde_with::DeserializeFromStr,
21    schemars::JsonSchema,
22)]
23#[schemars(with = "String")]
24#[strum(serialize_all = "snake_case")]
25pub enum ComponentType {
26    Activity,
27    ActivityStub,
28    Workflow,
29    WebhookEndpoint,
30    Cron,
31}
32impl ComponentType {
33    #[must_use]
34    pub fn is_activity(&self) -> bool {
35        matches!(self, ComponentType::Activity | ComponentType::ActivityStub)
36    }
37}
38
39#[derive(
40    derive_more::Debug,
41    Clone,
42    PartialEq,
43    Eq,
44    Hash,
45    derive_more::Display,
46    Serialize,
47    Deserialize,
48    schemars::JsonSchema,
49)]
50#[display("{component_type}:{name}:{component_digest}")]
51#[debug("{}", self)]
52#[non_exhaustive] // force using the constructor as much as possible due to validation
53pub struct ComponentId {
54    pub component_type: ComponentType,
55    pub name: StrVariant,
56    pub component_digest: ComponentDigest,
57}
58impl ComponentId {
59    pub fn new(
60        component_type: ComponentType,
61        name: StrVariant,
62        component_digest: ComponentDigest,
63    ) -> Result<Self, InvalidNameError<Self>> {
64        Ok(Self {
65            component_type,
66            name: check_name(name, "_.-")?,
67            component_digest,
68        })
69    }
70
71    #[must_use]
72    pub const fn dummy_activity() -> Self {
73        Self {
74            component_type: ComponentType::Activity,
75            name: StrVariant::empty(),
76            component_digest: COMPONENT_DIGEST_DUMMY,
77        }
78    }
79
80    #[cfg(any(test, feature = "test"))]
81    #[must_use]
82    pub const fn dummy_workflow() -> ComponentId {
83        ComponentId {
84            component_type: ComponentType::Workflow,
85            name: StrVariant::empty(),
86            component_digest: COMPONENT_DIGEST_DUMMY,
87        }
88    }
89}
90
91pub fn check_name<T>(
92    name: StrVariant,
93    special: &'static str,
94) -> Result<StrVariant, InvalidNameError<T>> {
95    if let Some(invalid) = name
96        .as_ref()
97        .chars()
98        .find(|c| !c.is_ascii_alphanumeric() && !special.contains(*c))
99    {
100        Err(InvalidNameError::<T> {
101            invalid,
102            name: name.as_ref().to_string(),
103            special,
104            phantom_data: PhantomData,
105        })
106    } else {
107        Ok(name)
108    }
109}
110#[derive(Debug, thiserror::Error)]
111#[error(
112    "name of {} `{name}` contains invalid character `{invalid}`, must only contain alphanumeric characters and following characters {special}",
113    std::any::type_name::<T>().rsplit("::").next().unwrap()
114)]
115pub struct InvalidNameError<T> {
116    invalid: char,
117    name: String,
118    special: &'static str,
119    phantom_data: PhantomData<T>,
120}
121
122#[derive(
123    Debug,
124    Clone,
125    derive_more::Display,
126    derive_more::FromStr,
127    derive_more::Deref,
128    PartialEq,
129    Eq,
130    Hash,
131    serde_with::SerializeDisplay,
132    serde_with::DeserializeFromStr,
133    schemars::JsonSchema,
134)]
135#[schemars(with = "String")]
136pub struct ComponentDigest(pub Digest);
137
138#[derive(
139    Debug,
140    Clone,
141    derive_more::Display,
142    derive_more::FromStr,
143    derive_more::Deref,
144    PartialEq,
145    Eq,
146    Hash,
147    serde_with::SerializeDisplay,
148    serde_with::DeserializeFromStr,
149    schemars::JsonSchema,
150)]
151#[schemars(with = "String")]
152pub struct ContentDigest(pub Digest);
153
154pub const CONTENT_DIGEST_DUMMY: ContentDigest = ContentDigest(DIGEST_DUMMY);
155pub const COMPONENT_DIGEST_DUMMY: ComponentDigest = ComponentDigest(DIGEST_DUMMY);
156pub const DIGEST_DUMMY: Digest = Digest([0; 32]);
157
158#[derive(
159    Clone,
160    PartialEq,
161    Eq,
162    Hash,
163    serde_with::SerializeDisplay,
164    serde_with::DeserializeFromStr,
165    derive_more::Deref,
166    schemars::JsonSchema,
167)]
168#[schemars(with = "String")]
169pub struct Digest(pub [u8; 32]);
170impl Digest {
171    #[must_use]
172    fn digest_base16_without_prefix(&self) -> String {
173        let mut out = String::with_capacity(self.0.len() * 2);
174        for &b in &self.0 {
175            write!(&mut out, "{b:02x}").expect("writing to string");
176        }
177        out
178    }
179
180    #[must_use]
181    pub fn with_infix(&self, infix: &str) -> String {
182        format!("{HASH_TYPE}{infix}{}", self.digest_base16_without_prefix())
183    }
184
185    fn parse_without_prefix(hash_base16: &str) -> Result<Digest, DigestParseErrror> {
186        if hash_base16.len() != 64 {
187            return Err(DigestParseErrror::SuffixHexLength(hash_base16.len()));
188        }
189        let mut digest = [0u8; 32];
190        for i in 0..32 {
191            let chunk = &hash_base16[i * 2..i * 2 + 2];
192            digest[i] = u8::from_str_radix(chunk, 16).map_err(|_| DigestParseErrror::InvalidHex)?;
193        }
194        Ok(Digest(digest))
195    }
196}
197
198impl TryFrom<&[u8]> for Digest {
199    type Error = DigestParseErrror;
200
201    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
202        if let Ok(value) = value.try_into() {
203            Ok(Digest(value))
204        } else {
205            Err(DigestParseErrror::BinLength(value.len()))
206        }
207    }
208}
209
210const HASH_TYPE: &str = "sha256";
211const HASH_TYPE_WITH_DELIMITER: &str = const_format::formatcp!("{}:", HASH_TYPE);
212impl Display for Digest {
213    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
214        write!(f, "{HASH_TYPE_WITH_DELIMITER}")?;
215        for b in self.0 {
216            write!(f, "{b:02x}")?;
217        }
218        Ok(())
219    }
220}
221impl Debug for Digest {
222    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223        Display::fmt(&self, f)
224    }
225}
226
227#[derive(Debug, thiserror::Error)]
228pub enum DigestParseErrror {
229    #[error("cannot parse Digest - invalid prefix")]
230    InvalidPrefix,
231    #[error("cannot parse Digest - invalid suffix length, expected 64 hex digits, got {0}")]
232    SuffixHexLength(usize),
233    #[error("cannot parse Digest - suffix must be hex encoded")]
234    InvalidHex,
235    #[error("cannot parse Digest - expected 32 bytes, got {0}")]
236    BinLength(usize),
237}
238
239impl FromStr for Digest {
240    type Err = DigestParseErrror;
241
242    fn from_str(input: &str) -> Result<Self, Self::Err> {
243        let Some(hash_base16) = input.strip_prefix(HASH_TYPE_WITH_DELIMITER) else {
244            return Err(DigestParseErrror::InvalidPrefix);
245        };
246        Digest::parse_without_prefix(hash_base16)
247    }
248}