1use std::{collections::HashMap, ops::Deref, path::Path};
2
3use anyhow::Result;
4use serde_json::Value;
5
6use crate::{Key, SecretsFile, decrypt};
7
8const SEPARATOR: &str = ".";
9
10pub struct SecretsMap {
39 inner: HashMap<String, String>,
40}
41
42impl SecretsMap {
43 pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
45 let secrets = SecretsFile::load(path)?;
46 Ok(secrets.value.into())
47 }
48
49 pub fn load_and_decrypt<P: AsRef<Path>>(path: P, private_key: Key) -> Result<Self> {
51 let mut secrets = SecretsFile::load(path)?;
52 secrets.transform(decrypt(&secrets, private_key)?)?;
53 Ok(secrets.value.into())
54 }
55
56 pub fn fetch<K: AsRef<str>>(&self, key: K) -> String {
58 self.inner.get(key.as_ref()).unwrap().to_owned()
59 }
60
61 pub fn fetch_or<K: AsRef<str>, V: Into<String>>(&self, key: K, default: V) -> String {
64 self.inner.get(key.as_ref()).unwrap_or(&default.into()).to_owned()
65 }
66}
67
68impl Deref for SecretsMap {
69 type Target = HashMap<String, String>;
70
71 fn deref(&self) -> &Self::Target {
73 &self.inner
74 }
75}
76
77impl From<Value> for SecretsMap {
78 fn from(value: Value) -> Self {
79 let mut map = HashMap::new();
80
81 if let Some(value) = value.as_object() {
82 value.iter().for_each(|(k, v)| {
83 extract_keys(&mut map, &safe_key(k), v);
84 });
85 }
86
87 Self { inner: map }
88 }
89}
90
91fn extract_keys(map: &mut HashMap<String, String>, key: &str, value: &Value) {
92 match value {
93 Value::Object(obj) => obj.iter().for_each(|(k, v)| {
94 extract_keys(map, &format!("{}.{}", key, safe_key(k)), v);
95 }),
96 Value::String(s) => {
97 map.insert(key.into(), s.to_string());
98 }
99 Value::Number(n) => {
100 map.insert(key.into(), n.to_string());
101 }
102 Value::Bool(b) => {
103 map.insert(key.into(), b.to_string());
104 }
105 _ => {}
106 }
107}
108
109fn safe_key<K: Into<String>>(k: K) -> String {
110 let key = k.into();
111
112 if key.contains(SEPARATOR) {
113 format!("[{}]", key)
114 } else {
115 key
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use serde_json::json;
122
123 use super::*;
124
125 #[test]
126 fn load() {
127 let secrets_path = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
128 .join("examples")
129 .join("data")
130 .join("secrets.ejson");
131
132 assert!(SecretsMap::load(secrets_path).is_ok());
133 }
134
135 #[test]
136 fn load_and_decrypt() -> Result<()> {
137 let secrets_path = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
138 .join("examples")
139 .join("data")
140 .join("secrets.ejson");
141
142 let key_path = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
143 .join("examples")
144 .join("data")
145 .join("2549b26efec29cf60e473797f5dda5f41d99460cf1c32f34f1c0247d9bd7ff5b");
146
147 let map = SecretsMap::load_and_decrypt(secrets_path, Key::from_file(key_path)?)?;
148 assert_eq!("key", map.fetch("some"));
149
150 Ok(())
151 }
152
153 #[test]
154 fn fetch() {
155 let data = json!({
156 "_public_key": "anything",
157 "some": "key"
158 });
159
160 let map: SecretsMap = data.into();
161 assert_eq!("key", map.fetch("some"));
162 assert_eq!("default", map.fetch_or("wat", "default"));
163 }
164
165 #[test]
166 fn non_string_scalars() {
167 let data = json!({
168 "_public_key": "anything",
169 "int": 10,
170 "float": 10.542,
171 "bool": true,
172 });
173
174 let map: SecretsMap = data.into();
175 assert_eq!("10", map.fetch("int"));
176 assert_eq!("10.542", map.fetch("float"));
177 assert_eq!("true", map.fetch("bool"));
178 }
179
180 #[test]
181 fn key_wrapping() {
182 let data = json!({
183 "_public_key": "anything",
184 "environment.test": "top-level value", "environment": {
186 "test":"value",
187 "_a": {
188 "b": "n",
189 "_c": "c",
190 "key.json": "contents"
191 }
192 },
193 "other": "key"
194 });
195
196 let map: SecretsMap = data.into();
197 assert_eq!("anything", map.fetch("_public_key"));
198 assert_eq!("key", map.fetch("other"));
199 assert_eq!("top-level value", map.fetch("[environment.test]"));
200 assert_eq!("value", map.fetch("environment.test"));
201 assert_eq!("n", map.fetch("environment._a.b"));
202 assert_eq!("contents", map.fetch("environment._a.[key.json]"));
203 }
204}