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] pub 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}