canic_core/config/component_group/
label.rs1use std::{borrow::Borrow, fmt};
8
9use candid::CandidType;
10use serde::{Deserialize, Deserializer, Serialize, de};
11use thiserror::Error as ThisError;
12
13pub const MAX_COMPONENT_DEPLOYMENT_LABEL_KEY_BYTES: usize = 40;
15pub const MAX_COMPONENT_DEPLOYMENT_LABEL_VALUE_BYTES: usize = 128;
17pub const MAX_COMPONENT_DEPLOYMENT_LABELS: usize = 32;
19
20macro_rules! bounded_component_deployment_label_text {
21 ($name:ident, $kind:literal, $maximum:expr, $validate:ident) => {
22 #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
23 #[serde(transparent)]
24 pub struct $name(String);
25
26 impl $name {
27 #[must_use]
28 pub const fn as_str(&self) -> &str {
29 self.0.as_str()
30 }
31 }
32
33 impl fmt::Display for $name {
34 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
35 formatter.write_str(self.as_str())
36 }
37 }
38
39 impl AsRef<str> for $name {
40 fn as_ref(&self) -> &str {
41 self.as_str()
42 }
43 }
44
45 impl Borrow<str> for $name {
46 fn borrow(&self) -> &str {
47 self.as_str()
48 }
49 }
50
51 impl CandidType for $name {
52 fn _ty() -> candid::types::Type {
53 candid::types::TypeInner::Text.into()
54 }
55
56 fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
57 where
58 S: candid::types::Serializer,
59 {
60 serializer.serialize_text(self.as_str())
61 }
62 }
63
64 impl TryFrom<String> for $name {
65 type Error = ComponentDeploymentLabelParseError;
66
67 fn try_from(value: String) -> Result<Self, Self::Error> {
68 $validate(&value, $kind, $maximum)?;
69 Ok(Self(value))
70 }
71 }
72
73 impl<'de> Deserialize<'de> for $name {
74 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
75 where
76 D: Deserializer<'de>,
77 {
78 let value = String::deserialize(deserializer)?;
79 Self::try_from(value).map_err(de::Error::custom)
80 }
81 }
82 };
83}
84
85bounded_component_deployment_label_text!(
86 ComponentDeploymentLabelKey,
87 "Component deployment label key",
88 MAX_COMPONENT_DEPLOYMENT_LABEL_KEY_BYTES,
89 validate_label_key
90);
91
92bounded_component_deployment_label_text!(
93 ComponentDeploymentLabelValue,
94 "Component deployment label value",
95 MAX_COMPONENT_DEPLOYMENT_LABEL_VALUE_BYTES,
96 validate_label_value
97);
98
99#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
101#[serde(deny_unknown_fields)]
102pub struct ComponentDeploymentLabel {
103 pub key: ComponentDeploymentLabelKey,
104 pub value: ComponentDeploymentLabelValue,
105}
106
107#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
109pub enum ComponentDeploymentLabelParseError {
110 #[error("{kind} must not be empty")]
111 Empty { kind: &'static str },
112
113 #[error("{kind} must not exceed {maximum} bytes, got {actual}")]
114 TooLong {
115 kind: &'static str,
116 actual: usize,
117 maximum: usize,
118 },
119
120 #[error("Component deployment label key must use only ASCII letters, numbers, '-' or '_'")]
121 InvalidKeyCharacters,
122
123 #[error("Component deployment label value must not contain control characters")]
124 InvalidValueCharacters,
125}
126
127fn validate_label_key(
128 value: &str,
129 kind: &'static str,
130 maximum: usize,
131) -> Result<(), ComponentDeploymentLabelParseError> {
132 validate_label_text_length(value, kind, maximum)?;
133 if !value
134 .bytes()
135 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
136 {
137 return Err(ComponentDeploymentLabelParseError::InvalidKeyCharacters);
138 }
139 Ok(())
140}
141
142fn validate_label_value(
143 value: &str,
144 kind: &'static str,
145 maximum: usize,
146) -> Result<(), ComponentDeploymentLabelParseError> {
147 validate_label_text_length(value, kind, maximum)?;
148 if value.chars().any(char::is_control) {
149 return Err(ComponentDeploymentLabelParseError::InvalidValueCharacters);
150 }
151 Ok(())
152}
153
154const fn validate_label_text_length(
155 value: &str,
156 kind: &'static str,
157 maximum: usize,
158) -> Result<(), ComponentDeploymentLabelParseError> {
159 if value.is_empty() {
160 return Err(ComponentDeploymentLabelParseError::Empty { kind });
161 }
162 if value.len() > maximum {
163 return Err(ComponentDeploymentLabelParseError::TooLong {
164 kind,
165 actual: value.len(),
166 maximum,
167 });
168 }
169 Ok(())
170}