gloves_core/
secret_ref.rs1use std::{fmt, str::FromStr};
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4
5use crate::{types::SecretId, ValidationError};
6
7const SECRET_REF_SCHEME: &str = "gloves://";
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11pub struct SecretRef {
12 secret_id: SecretId,
13}
14
15impl SecretRef {
16 pub fn new(secret_id: SecretId) -> Self {
18 Self { secret_id }
19 }
20
21 pub fn secret_id(&self) -> &SecretId {
23 &self.secret_id
24 }
25
26 pub fn as_str(&self) -> String {
28 format!("{SECRET_REF_SCHEME}{}", self.secret_id.as_str())
29 }
30}
31
32impl fmt::Display for SecretRef {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 f.write_str(&self.as_str())
35 }
36}
37
38impl From<SecretId> for SecretRef {
39 fn from(value: SecretId) -> Self {
40 Self::new(value)
41 }
42}
43
44impl From<SecretRef> for String {
45 fn from(value: SecretRef) -> Self {
46 value.to_string()
47 }
48}
49
50impl FromStr for SecretRef {
51 type Err = SecretRefParseError;
52
53 fn from_str(value: &str) -> Result<Self, Self::Err> {
54 let raw_path = value
55 .strip_prefix(SECRET_REF_SCHEME)
56 .ok_or(SecretRefParseError::InvalidScheme)?;
57 if raw_path.is_empty() {
58 return Err(SecretRefParseError::MissingPath);
59 }
60
61 let mut segments = Vec::new();
62 for segment in raw_path.split('/') {
63 if segment.is_empty() {
64 return Err(SecretRefParseError::EmptyPathSegment);
65 }
66 segments.push(segment);
67 }
68
69 let secret_path = segments.join("/");
70 let secret_id =
71 SecretId::new(&secret_path).map_err(SecretRefParseError::InvalidSecretId)?;
72 Ok(Self::new(secret_id))
73 }
74}
75
76impl Serialize for SecretRef {
77 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
78 where
79 S: Serializer,
80 {
81 serializer.serialize_str(&self.to_string())
82 }
83}
84
85impl<'de> Deserialize<'de> for SecretRef {
86 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
87 where
88 D: Deserializer<'de>,
89 {
90 let value = String::deserialize(deserializer)?;
91 value.parse().map_err(serde::de::Error::custom)
92 }
93}
94
95#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
97pub enum SecretRefParseError {
98 #[error("invalid secret ref scheme: expected `gloves://`")]
100 InvalidScheme,
101 #[error("invalid secret ref: missing secret path")]
103 MissingPath,
104 #[error("invalid secret ref: empty path segment")]
106 EmptyPathSegment,
107 #[error(transparent)]
109 InvalidSecretId(#[from] ValidationError),
110}
111
112#[cfg(test)]
113mod tests {
114 use super::{SecretRef, SecretRefParseError};
115 use crate::types::SecretId;
116
117 #[test]
118 fn secret_ref_roundtrips_agent_paths() {
119 let secret_ref: SecretRef = "gloves://agents/devy/api-keys/openai".parse().unwrap();
120
121 assert_eq!(
122 secret_ref.secret_id().as_str(),
123 "agents/devy/api-keys/openai"
124 );
125 assert_eq!(
126 secret_ref.to_string(),
127 "gloves://agents/devy/api-keys/openai"
128 );
129 }
130
131 #[test]
132 fn secret_ref_from_secret_id_uses_canonical_format() {
133 let secret_ref = SecretRef::from(SecretId::new("shared/database-url").unwrap());
134
135 assert_eq!(secret_ref.to_string(), "gloves://shared/database-url");
136 }
137
138 #[test]
139 fn secret_ref_rejects_invalid_shapes() {
140 assert!(matches!(
141 "https://agents/devy/api-key".parse::<SecretRef>(),
142 Err(SecretRefParseError::InvalidScheme)
143 ));
144 assert!(matches!(
145 "gloves://".parse::<SecretRef>(),
146 Err(SecretRefParseError::MissingPath)
147 ));
148 assert!(matches!(
149 "gloves://agents//api-key".parse::<SecretRef>(),
150 Err(SecretRefParseError::EmptyPathSegment)
151 ));
152 assert!("gloves://agents/devy/../api-key"
153 .parse::<SecretRef>()
154 .is_err());
155 }
156}