greentic_secrets_spec/
uri.rs1use crate::error::{Error, Result};
2use crate::types::{Scope, validate_component, validate_version};
3#[cfg(feature = "serde")]
4use serde::{Deserialize, Serialize};
5use std::fmt;
6use std::str::FromStr;
7
8const SCHEME: &str = "secrets://";
9const TEAM_PLACEHOLDER: &str = "_";
10
11#[derive(Debug, Clone, PartialEq, Eq, Hash)]
12pub struct SecretUri {
13 scope: Scope,
14 category: String,
15 name: String,
16 version: Option<String>,
17}
18
19impl SecretUri {
20 pub fn new(scope: Scope, category: impl Into<String>, name: impl Into<String>) -> Result<Self> {
21 let category = category.into();
22 let name = name.into();
23
24 validate_component(&category, "category")?;
25 validate_component(&name, "name")?;
26
27 Ok(Self {
28 scope,
29 category,
30 name,
31 version: None,
32 })
33 }
34
35 pub fn scope(&self) -> &Scope {
36 &self.scope
37 }
38
39 pub fn category(&self) -> &str {
40 &self.category
41 }
42
43 pub fn name(&self) -> &str {
44 &self.name
45 }
46
47 pub fn version(&self) -> Option<&str> {
48 self.version.as_deref()
49 }
50
51 pub fn with_version(mut self, version: Option<&str>) -> Result<Self> {
52 if let Some(value) = version {
53 validate_version(value)?;
54 self.version = Some(value.to_string());
55 } else {
56 self.version = None;
57 }
58 Ok(self)
59 }
60
61 pub fn parse(input: &str) -> Result<Self> {
62 let raw = input.trim();
63 if !raw.starts_with(SCHEME) {
64 return Err(Error::InvalidScheme);
65 }
66
67 let path = &raw[SCHEME.len()..];
68 let mut segments = path.split('/');
69
70 let env = segments.next().ok_or(Error::MissingSegment {
71 field: "environment",
72 })?;
73 let tenant = segments
74 .next()
75 .ok_or(Error::MissingSegment { field: "tenant" })?;
76 let team_segment = segments
77 .next()
78 .ok_or(Error::MissingSegment { field: "team" })?;
79 let category = segments
80 .next()
81 .ok_or(Error::MissingSegment { field: "category" })?;
82 let name_segment = segments
83 .next()
84 .ok_or(Error::MissingSegment { field: "name" })?;
85
86 if segments.next().is_some() {
87 return Err(Error::ExtraSegments);
88 }
89
90 let team = if team_segment == TEAM_PLACEHOLDER {
91 None
92 } else {
93 Some(team_segment.to_string())
94 };
95
96 let (name, version) = split_name_version(name_segment)?;
97
98 let scope = Scope::new(env.to_string(), tenant.to_string(), team)?;
99 let mut uri = SecretUri::new(scope, category, name)?;
100 if let Some(version) = version {
101 uri = uri.with_version(Some(&version))?;
102 }
103
104 Ok(uri)
105 }
106
107 fn format_team(team: Option<&str>) -> &str {
108 team.unwrap_or(TEAM_PLACEHOLDER)
109 }
110}
111
112fn split_name_version(segment: &str) -> Result<(&str, Option<String>)> {
113 let mut parts = segment.split('@');
114 let name = parts.next().unwrap_or_default();
115 let version = parts.next();
116
117 if parts.next().is_some() {
118 return Err(Error::InvalidVersion {
119 value: segment.to_string(),
120 });
121 }
122
123 if let Some(v) = version {
124 validate_version(v)?;
125 Ok((name, Some(v.to_string())))
126 } else {
127 Ok((name, None))
128 }
129}
130
131impl fmt::Display for SecretUri {
132 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133 write!(
134 f,
135 "{SCHEME}{}/{}/{}/{}/{}",
136 self.scope.env(),
137 self.scope.tenant(),
138 Self::format_team(self.scope.team()),
139 self.category,
140 self.name
141 )?;
142
143 if let Some(version) = &self.version {
144 write!(f, "@{version}")?;
145 }
146 Ok(())
147 }
148}
149
150impl FromStr for SecretUri {
151 type Err = Error;
152
153 fn from_str(s: &str) -> Result<Self> {
154 SecretUri::parse(s)
155 }
156}
157
158impl SecretUri {
159 pub fn into_string(self) -> String {
160 self.to_string()
161 }
162}
163
164impl TryFrom<&str> for SecretUri {
165 type Error = Error;
166
167 fn try_from(value: &str) -> Result<Self> {
168 SecretUri::parse(value)
169 }
170}
171
172impl TryFrom<String> for SecretUri {
173 type Error = Error;
174
175 fn try_from(value: String) -> Result<Self> {
176 SecretUri::parse(&value)
177 }
178}
179
180#[cfg(feature = "serde")]
181impl Serialize for SecretUri {
182 fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
183 where
184 S: serde::Serializer,
185 {
186 serializer.serialize_str(&self.to_string())
187 }
188}
189
190#[cfg(feature = "serde")]
191impl<'de> Deserialize<'de> for SecretUri {
192 fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
193 where
194 D: serde::Deserializer<'de>,
195 {
196 let value = String::deserialize(deserializer)?;
197 SecretUri::parse(&value).map_err(serde::de::Error::custom)
198 }
199}
200
201