Skip to main content

canic_core/config/component_group/
label.rs

1//! Module: config::component_group::label
2//!
3//! Responsibility: define bounded canonical deployment-label primitives.
4//! Does not own: inheritance, typed purpose, placement, authorization, or runtime state.
5//! Boundary: source and decoded label text is validated before group compilation.
6
7use std::{borrow::Borrow, fmt};
8
9use candid::CandidType;
10use serde::{Deserialize, Deserializer, Serialize, de};
11use thiserror::Error as ThisError;
12
13/// Maximum bytes in one canonical deployment-label key.
14pub const MAX_COMPONENT_DEPLOYMENT_LABEL_KEY_BYTES: usize = 40;
15/// Maximum bytes in one canonical deployment-label value.
16pub const MAX_COMPONENT_DEPLOYMENT_LABEL_VALUE_BYTES: usize = 128;
17/// Maximum effective deployment labels on one flattened Component occurrence.
18pub 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/// One bounded inert metadata label on a flattened Component occurrence.
100#[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/// Typed rejection for malformed Component deployment label text.
108#[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}