evault_core/model/
profile.rs1use serde::{Deserialize, Serialize};
4
5use crate::error::ManifestError;
6
7#[non_exhaustive]
25#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
26#[serde(transparent)]
27pub struct Profile(String);
28
29impl Profile {
30 pub const DEFAULT_NAME: &'static str = "default";
32
33 #[must_use]
35 pub fn default_profile() -> Self {
36 Self(Self::DEFAULT_NAME.to_owned())
37 }
38
39 pub fn named(name: impl Into<String>) -> Self {
45 let name = name.into();
46 if name == Self::DEFAULT_NAME {
47 return Self::default_profile();
48 }
49 Self(name)
50 }
51
52 pub fn try_named(name: impl Into<String>) -> Result<Self, ManifestError> {
65 let name = name.into();
66 if name.is_empty() {
67 return Err(ManifestError::Invalid("profile name is empty".into()));
68 }
69 if name.len() > 32 {
70 return Err(ManifestError::Invalid(
71 "profile name longer than 32 characters".into(),
72 ));
73 }
74 for (offset, &b) in name.as_bytes().iter().enumerate() {
75 if !(b.is_ascii_alphanumeric() || b == b'_' || b == b'-') {
76 return Err(ManifestError::Invalid(format!(
77 "profile name contains an invalid character at byte offset {offset}"
78 )));
79 }
80 }
81 if name.bytes().all(|b| b.is_ascii_digit()) {
82 return Err(ManifestError::Invalid(
83 "profile name must contain at least one non-digit character".into(),
84 ));
85 }
86 Ok(Self::named(name))
87 }
88
89 #[must_use]
91 pub fn as_str(&self) -> &str {
92 &self.0
93 }
94
95 #[must_use]
97 pub fn is_default(&self) -> bool {
98 self.0 == Self::DEFAULT_NAME
99 }
100}
101
102impl Default for Profile {
103 fn default() -> Self {
104 Self::default_profile()
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn default_profile_is_named_default() {
114 let p = Profile::default_profile();
115 assert!(p.is_default());
116 assert_eq!(p.as_str(), "default");
117 }
118
119 #[test]
120 fn named_profile_is_not_default() {
121 let p = Profile::named("dev");
122 assert!(!p.is_default());
123 assert_eq!(p.as_str(), "dev");
124 }
125
126 #[test]
127 fn named_default_normalizes_to_canonical() {
128 let p = Profile::named("default");
129 assert!(p.is_default());
130 assert_eq!(p, Profile::default_profile());
131 }
132
133 #[test]
134 fn try_named_accepts_valid_names() {
135 for name in ["dev", "staging", "prod-2", "feat_branch_123"] {
136 assert!(Profile::try_named(name).is_ok(), "expected {name} ok");
137 }
138 }
139
140 #[test]
141 fn try_named_rejects_invalid_names() {
142 let bad = [
143 "",
144 " ",
145 "with space",
146 "with\nnewline",
147 "[evil]",
148 "123",
149 &"a".repeat(33),
150 ];
151 for name in bad {
152 assert!(
153 Profile::try_named(name).is_err(),
154 "expected {name:?} rejected"
155 );
156 }
157 }
158
159 #[test]
160 fn try_named_default_normalizes() {
161 let p = Profile::try_named("default").expect("valid");
162 assert!(p.is_default());
163 }
164
165 #[test]
166 fn profile_serializes_transparently() {
167 let p = Profile::named("staging");
168 let s = serde_json::to_string(&p).expect("serde");
169 assert_eq!(s, "\"staging\"");
170 }
171}