lean_ctx_protocol/
common.rs1use serde::{Deserialize, Deserializer, Serialize, de::Error as DeError};
4use std::{error::Error, fmt};
5
6pub const V1_SCHEMA_VERSION: u32 = 1;
8
9pub const MAX_IDENTIFIER_LENGTH: usize = 256;
11
12pub(crate) fn validate_bounded_opaque_identifier(
14 value: &str,
15 type_name: &str,
16) -> Result<(), ValidationError> {
17 if value.trim().is_empty() {
18 return Err(ValidationError::new(format!(
19 "{type_name} must not be empty"
20 )));
21 }
22 if value.len() > MAX_IDENTIFIER_LENGTH {
23 return Err(ValidationError::new(format!(
24 "{type_name} exceeds the {MAX_IDENTIFIER_LENGTH} byte limit"
25 )));
26 }
27 if value.chars().any(char::is_control) {
28 return Err(ValidationError::new(format!(
29 "{type_name} must not contain control characters"
30 )));
31 }
32 Ok(())
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct ValidationError(pub(crate) String);
38
39impl ValidationError {
40 pub(crate) fn new(message: impl Into<String>) -> Self {
41 Self(message.into())
42 }
43}
44
45impl fmt::Display for ValidationError {
46 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
47 formatter.write_str(&self.0)
48 }
49}
50
51impl Error for ValidationError {}
52
53macro_rules! bounded_identifier {
54 ($name:ident) => {
55 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
57 #[serde(transparent)]
58 pub struct $name(String);
59
60 impl $name {
61 pub fn new(value: impl Into<String>) -> Result<Self, ValidationError> {
63 let value = value.into();
64 $crate::common::validate_bounded_opaque_identifier(&value, stringify!($name))?;
65 Ok(Self(value))
66 }
67
68 pub fn as_str(&self) -> &str {
70 &self.0
71 }
72
73 pub fn into_inner(self) -> String {
75 self.0
76 }
77 }
78
79 impl std::str::FromStr for $name {
80 type Err = ValidationError;
81
82 fn from_str(value: &str) -> Result<Self, Self::Err> {
83 Self::new(value)
84 }
85 }
86
87 impl TryFrom<String> for $name {
88 type Error = ValidationError;
89
90 fn try_from(value: String) -> Result<Self, Self::Error> {
91 Self::new(value)
92 }
93 }
94
95 impl TryFrom<&str> for $name {
96 type Error = ValidationError;
97
98 fn try_from(value: &str) -> Result<Self, Self::Error> {
99 Self::new(value)
100 }
101 }
102
103 impl From<$name> for String {
104 fn from(value: $name) -> Self {
105 value.0
106 }
107 }
108
109 impl<'de> Deserialize<'de> for $name {
110 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
111 where
112 D: Deserializer<'de>,
113 {
114 let value = String::deserialize(deserializer)?;
115 Self::new(value).map_err(DeError::custom)
116 }
117 }
118 };
119}
120
121bounded_identifier!(TaskId);
122bounded_identifier!(TraceId);
123bounded_identifier!(ProjectId);
124bounded_identifier!(SessionId);
125bounded_identifier!(AgentId);
126bounded_identifier!(TenantId);
127bounded_identifier!(PlanId);
128bounded_identifier!(ReceiptId);
129bounded_identifier!(OutcomeId);
130bounded_identifier!(CapabilityId);
131bounded_identifier!(DecisionId);
132
133pub fn deserialize_schema_version<'de, D>(deserializer: D) -> Result<u32, D::Error>
135where
136 D: Deserializer<'de>,
137{
138 let version = u32::deserialize(deserializer)?;
139 if version == V1_SCHEMA_VERSION {
140 Ok(version)
141 } else {
142 Err(DeError::custom(format!(
143 "unsupported schema_version {version}; expected {V1_SCHEMA_VERSION}"
144 )))
145 }
146}
147
148pub fn deserialize_milliunit<'de, D>(deserializer: D) -> Result<u16, D::Error>
150where
151 D: Deserializer<'de>,
152{
153 let value = u16::deserialize(deserializer)?;
154 if value <= 1000 {
155 Ok(value)
156 } else {
157 Err(DeError::custom("milliunit must be between 0 and 1000"))
158 }
159}
160
161pub fn deserialize_optional_milliunit<'de, D>(deserializer: D) -> Result<Option<u16>, D::Error>
163where
164 D: Deserializer<'de>,
165{
166 let value = Option::<u16>::deserialize(deserializer)?;
167 value.map_or(Ok(None), |value| {
168 if value <= 1000 {
169 Ok(Some(value))
170 } else {
171 Err(DeError::custom("milliunit must be between 0 and 1000"))
172 }
173 })
174}
175
176pub fn validate_schema_version(version: u32) -> Result<(), ValidationError> {
178 if version == V1_SCHEMA_VERSION {
179 Ok(())
180 } else {
181 Err(ValidationError::new(format!(
182 "unsupported schema_version {version}; expected {V1_SCHEMA_VERSION}"
183 )))
184 }
185}
186
187pub fn validate_milliunit(value: u16, field: &str) -> Result<(), ValidationError> {
189 if value <= 1000 {
190 Ok(())
191 } else {
192 Err(ValidationError::new(format!(
193 "{field} must be between 0 and 1000"
194 )))
195 }
196}