1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use std::borrow::Borrow;
3use std::fmt;
4use std::str::FromStr;
5use thiserror::Error;
6
7const MAX_ADAPTER_ID_LEN: usize = 96;
8pub const MAX_ADAPTER_REVISION_LEN: usize = 256;
9
10#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
15pub struct AdapterId(String);
16
17impl AdapterId {
18 pub fn new(value: impl Into<String>) -> Result<Self, AdapterIdError> {
19 let value = value.into();
20 validate_id(&value)?;
21 Ok(Self(value))
22 }
23
24 pub fn as_str(&self) -> &str {
25 &self.0
26 }
27}
28
29fn validate_id(value: &str) -> Result<(), AdapterIdError> {
30 if value.is_empty() {
31 return Err(AdapterIdError::Empty);
32 }
33 if value.len() > MAX_ADAPTER_ID_LEN {
34 return Err(AdapterIdError::TooLong {
35 len: value.len(),
36 max: MAX_ADAPTER_ID_LEN,
37 });
38 }
39 if !value.bytes().all(|byte| {
40 byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'-' | b'_')
41 }) {
42 return Err(AdapterIdError::InvalidCharacters(value.to_owned()));
43 }
44 if matches!(value.as_bytes().first(), Some(b'-' | b'_'))
45 || matches!(value.as_bytes().last(), Some(b'-' | b'_'))
46 {
47 return Err(AdapterIdError::InvalidBoundary(value.to_owned()));
48 }
49 Ok(())
50}
51
52impl AsRef<str> for AdapterId {
53 fn as_ref(&self) -> &str {
54 self.as_str()
55 }
56}
57
58impl Borrow<str> for AdapterId {
59 fn borrow(&self) -> &str {
60 self.as_str()
61 }
62}
63
64impl fmt::Display for AdapterId {
65 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
66 formatter.write_str(self.as_str())
67 }
68}
69
70impl FromStr for AdapterId {
71 type Err = AdapterIdError;
72
73 fn from_str(value: &str) -> Result<Self, Self::Err> {
74 Self::new(value)
75 }
76}
77
78impl Serialize for AdapterId {
79 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
80 where
81 S: Serializer,
82 {
83 serializer.serialize_str(self.as_str())
84 }
85}
86
87impl<'de> Deserialize<'de> for AdapterId {
88 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
89 where
90 D: Deserializer<'de>,
91 {
92 let value = String::deserialize(deserializer)?;
93 Self::new(value).map_err(serde::de::Error::custom)
94 }
95}
96
97#[derive(Debug, Error, Clone, Eq, PartialEq)]
98pub enum AdapterIdError {
99 #[error("adapter ID cannot be empty")]
100 Empty,
101 #[error("adapter ID length {len} exceeds the {max}-byte limit")]
102 TooLong { len: usize, max: usize },
103 #[error("adapter ID must contain only lowercase ASCII letters, digits, '-' or '_': {0}")]
104 InvalidCharacters(String),
105 #[error("adapter ID cannot start or end with '-' or '_': {0}")]
106 InvalidBoundary(String),
107}
108
109#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
110#[serde(rename_all = "kebab-case")]
111pub enum AdapterFamily {
112 PtySemantic,
113 Pipe,
114 OneShot,
115 Acp,
116 Hook,
117 ManagedHook,
118 History,
119 Resume,
120 SessionOptions,
121 CapabilityProbe,
122}
123
124#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
125#[serde(rename_all = "kebab-case")]
126pub enum AdapterVerification {
127 Reference,
128 SyntheticFixture,
129 CapturedFixture,
130 VendorCanary,
131}
132
133#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
134pub struct AdapterBinding {
135 pub id: AdapterId,
136 pub revision: String,
137 pub verification: AdapterVerification,
138}
139
140impl AdapterBinding {
141 pub fn new(
142 id: AdapterId,
143 revision: impl Into<String>,
144 verification: AdapterVerification,
145 ) -> Result<Self, AdapterBindingError> {
146 let revision = revision.into();
147 validate_revision(&revision)?;
148 Ok(Self {
149 id,
150 revision,
151 verification,
152 })
153 }
154
155 pub fn validate(&self) -> Result<(), AdapterBindingError> {
156 validate_revision(&self.revision)
157 }
158}
159
160fn validate_revision(revision: &str) -> Result<(), AdapterBindingError> {
161 if revision.trim().is_empty() {
162 return Err(AdapterBindingError::EmptyRevision);
163 }
164 if revision.len() > MAX_ADAPTER_REVISION_LEN {
165 return Err(AdapterBindingError::RevisionTooLong {
166 len: revision.len(),
167 max: MAX_ADAPTER_REVISION_LEN,
168 });
169 }
170 if revision.chars().any(char::is_control) {
171 return Err(AdapterBindingError::InvalidRevision(revision.to_owned()));
172 }
173 Ok(())
174}
175
176#[derive(Clone, Debug, Error, Eq, PartialEq)]
177pub enum AdapterBindingError {
178 #[error("adapter revision cannot be empty")]
179 EmptyRevision,
180 #[error("adapter revision length {len} exceeds the {max}-byte limit")]
181 RevisionTooLong { len: usize, max: usize },
182 #[error("adapter revision contains control characters: {0:?}")]
183 InvalidRevision(String),
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn adapter_ids_are_extensible_and_wire_safe() {
192 let id = AdapterId::new("grok-hook-v1").unwrap();
193 assert_eq!(id.as_str(), "grok-hook-v1");
194 let json = serde_json::to_string(&id).unwrap();
195 assert_eq!(serde_json::from_str::<AdapterId>(&json).unwrap(), id);
196 }
197
198 #[test]
199 fn invalid_ids_and_revisions_are_rejected() {
200 assert!(matches!(
201 AdapterId::new("Grok"),
202 Err(AdapterIdError::InvalidCharacters(_))
203 ));
204 let id = AdapterId::new("grok").unwrap();
205 assert!(matches!(
206 AdapterBinding::new(id, "", AdapterVerification::Reference),
207 Err(AdapterBindingError::EmptyRevision)
208 ));
209 }
210}