1pub mod features;
4
5pub use features::{FeatureChecker, FeatureContext, features};
6
7use std::collections::HashMap;
8use std::path::Path;
9use std::sync::{Arc, OnceLock, RwLock};
10
11use sentry_options_validation::{
12 SchemaRegistry, ValidationError, ValuesWatcher, resolve_options_dir,
13};
14use serde_json::Value;
15use thiserror::Error;
16
17pub mod testing;
18
19static GLOBAL_OPTIONS: OnceLock<Options> = OnceLock::new();
20
21#[derive(Debug, Error)]
22pub enum OptionsError {
23 #[error("Options not initialized - call init() first")]
24 NotInitialized,
25
26 #[error("Unknown namespace: {0}")]
27 UnknownNamespace(String),
28
29 #[error("Unknown option '{key}' in namespace '{namespace}'")]
30 UnknownOption { namespace: String, key: String },
31
32 #[error("Schema error: {0}")]
33 Schema(#[from] ValidationError),
34}
35
36pub type Result<T> = std::result::Result<T, OptionsError>;
37
38pub struct Options {
40 registry: Arc<SchemaRegistry>,
41 values: Arc<RwLock<HashMap<String, HashMap<String, Value>>>>,
42 _watcher: ValuesWatcher,
43}
44
45impl Options {
46 pub fn new() -> Result<Self> {
50 Self::from_directory(&resolve_options_dir())
51 }
52
53 pub fn from_directory(base_dir: &Path) -> Result<Self> {
56 let schemas_dir = base_dir.join("schemas");
57 let values_dir = base_dir.join("values");
58
59 let registry = Arc::new(SchemaRegistry::from_directory(&schemas_dir)?);
60 let (loaded_values, _) = registry.load_values_json(&values_dir)?;
61 let values = Arc::new(RwLock::new(loaded_values));
62
63 let watcher_registry = Arc::clone(®istry);
64 let watcher_values = Arc::clone(&values);
65 let watcher = ValuesWatcher::new(values_dir.as_path(), watcher_registry, watcher_values)?;
67
68 Ok(Self {
69 registry,
70 values,
71 _watcher: watcher,
72 })
73 }
74
75 pub fn get(&self, namespace: &str, key: &str) -> Result<Value> {
77 if let Some(value) = testing::get_override(namespace, key) {
78 return Ok(value);
79 }
80
81 let schema = self
82 .registry
83 .get(namespace)
84 .ok_or_else(|| OptionsError::UnknownNamespace(namespace.to_string()))?;
85
86 let values_guard = self
87 .values
88 .read()
89 .unwrap_or_else(|poisoned| poisoned.into_inner());
90 if let Some(ns_values) = values_guard.get(namespace)
91 && let Some(value) = ns_values.get(key)
92 {
93 return Ok(value.clone());
94 }
95
96 let default = schema
97 .get_default(key)
98 .ok_or_else(|| OptionsError::UnknownOption {
99 namespace: namespace.to_string(),
100 key: key.to_string(),
101 })?;
102
103 Ok(default.clone())
104 }
105
106 pub fn validate_override(&self, namespace: &str, key: &str, value: &Value) -> Result<()> {
108 let schema = self
109 .registry
110 .get(namespace)
111 .ok_or_else(|| OptionsError::UnknownNamespace(namespace.to_string()))?;
112
113 schema.validate_option(key, value)?;
114
115 Ok(())
116 }
117 pub fn isset(&self, namespace: &str, key: &str) -> Result<bool> {
124 let schema = self
125 .registry
126 .get(namespace)
127 .ok_or_else(|| OptionsError::UnknownNamespace(namespace.to_string()))?;
128
129 if !schema.options.contains_key(key) {
130 return Err(OptionsError::UnknownOption {
131 namespace: namespace.into(),
132 key: key.into(),
133 });
134 }
135
136 let values_guard = self
137 .values
138 .read()
139 .unwrap_or_else(|poisoned| poisoned.into_inner());
140
141 if let Some(ns_values) = values_guard.get(namespace) {
142 Ok(ns_values.contains_key(key))
143 } else {
144 Ok(false)
145 }
146 }
147}
148
149pub fn init() -> Result<()> {
152 if GLOBAL_OPTIONS.get().is_some() {
153 return Ok(());
154 }
155 let opts = Options::new()?;
156 let _ = GLOBAL_OPTIONS.set(opts);
157 Ok(())
158}
159
160pub fn options(namespace: &str) -> Result<NamespaceOptions> {
164 let opts = GLOBAL_OPTIONS.get().ok_or(OptionsError::NotInitialized)?;
165 Ok(NamespaceOptions {
166 namespace: namespace.to_string(),
167 options: opts,
168 })
169}
170
171pub struct NamespaceOptions {
173 namespace: String,
174 options: &'static Options,
175}
176
177impl NamespaceOptions {
178 pub fn get(&self, key: &str) -> Result<Value> {
180 self.options.get(&self.namespace, key)
181 }
182
183 pub fn isset(&self, key: &str) -> Result<bool> {
185 self.options.isset(&self.namespace, key)
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use serde_json::json;
193 use std::fs;
194 use tempfile::TempDir;
195
196 fn create_schema(dir: &Path, namespace: &str, schema: &str) {
197 let schema_dir = dir.join(namespace);
198 fs::create_dir_all(&schema_dir).unwrap();
199 fs::write(schema_dir.join("schema.json"), schema).unwrap();
200 }
201
202 fn create_values(dir: &Path, namespace: &str, values: &str) {
203 let ns_dir = dir.join(namespace);
204 fs::create_dir_all(&ns_dir).unwrap();
205 fs::write(ns_dir.join("values.json"), values).unwrap();
206 }
207
208 #[test]
209 fn test_get_value() {
210 let temp = TempDir::new().unwrap();
211 let schemas = temp.path().join("schemas");
212 let values = temp.path().join("values");
213 fs::create_dir_all(&schemas).unwrap();
214
215 create_schema(
216 &schemas,
217 "test",
218 r#"{
219 "version": "1.0",
220 "type": "object",
221 "properties": {
222 "enabled": {
223 "type": "boolean",
224 "default": false,
225 "description": "Enable feature"
226 }
227 }
228 }"#,
229 );
230 create_values(&values, "test", r#"{"options": {"enabled": true}}"#);
231
232 let options = Options::from_directory(temp.path()).unwrap();
233 assert_eq!(options.get("test", "enabled").unwrap(), json!(true));
234 }
235
236 #[test]
237 fn test_get_default() {
238 let temp = TempDir::new().unwrap();
239 let schemas = temp.path().join("schemas");
240 let values = temp.path().join("values");
241 fs::create_dir_all(&schemas).unwrap();
242 fs::create_dir_all(&values).unwrap();
243
244 create_schema(
245 &schemas,
246 "test",
247 r#"{
248 "version": "1.0",
249 "type": "object",
250 "properties": {
251 "timeout": {
252 "type": "integer",
253 "default": 30,
254 "description": "Timeout"
255 }
256 }
257 }"#,
258 );
259
260 let options = Options::from_directory(temp.path()).unwrap();
261 assert_eq!(options.get("test", "timeout").unwrap(), json!(30));
262 }
263
264 #[test]
265 fn test_unknown_namespace() {
266 let temp = TempDir::new().unwrap();
267 let schemas = temp.path().join("schemas");
268 let values = temp.path().join("values");
269 fs::create_dir_all(&schemas).unwrap();
270 fs::create_dir_all(&values).unwrap();
271
272 create_schema(
273 &schemas,
274 "test",
275 r#"{"version": "1.0", "type": "object", "properties": {}}"#,
276 );
277
278 let options = Options::from_directory(temp.path()).unwrap();
279 assert!(matches!(
280 options.get("unknown", "key"),
281 Err(OptionsError::UnknownNamespace(_))
282 ));
283 }
284
285 #[test]
286 fn test_unknown_option() {
287 let temp = TempDir::new().unwrap();
288 let schemas = temp.path().join("schemas");
289 let values = temp.path().join("values");
290 fs::create_dir_all(&schemas).unwrap();
291 fs::create_dir_all(&values).unwrap();
292
293 create_schema(
294 &schemas,
295 "test",
296 r#"{
297 "version": "1.0",
298 "type": "object",
299 "properties": {
300 "known": {"type": "string", "default": "x", "description": "Known"}
301 }
302 }"#,
303 );
304
305 let options = Options::from_directory(temp.path()).unwrap();
306 assert!(matches!(
307 options.get("test", "unknown"),
308 Err(OptionsError::UnknownOption { .. })
309 ));
310 }
311
312 #[test]
313 fn test_missing_values_dir() {
314 let temp = TempDir::new().unwrap();
315 let schemas = temp.path().join("schemas");
316 fs::create_dir_all(&schemas).unwrap();
317
318 create_schema(
319 &schemas,
320 "test",
321 r#"{
322 "version": "1.0",
323 "type": "object",
324 "properties": {
325 "opt": {"type": "string", "default": "default_val", "description": "Opt"}
326 }
327 }"#,
328 );
329
330 let options = Options::from_directory(temp.path()).unwrap();
331 assert_eq!(options.get("test", "opt").unwrap(), json!("default_val"));
332 }
333
334 #[test]
335 fn isset_with_defined_and_undefined_keys() {
336 let temp = TempDir::new().unwrap();
337 let schemas = temp.path().join("schemas");
338 fs::create_dir_all(&schemas).unwrap();
339
340 let values = temp.path().join("values");
341 create_values(&values, "test", r#"{"options": {"has-value": "yes"}}"#);
342
343 create_schema(
344 &schemas,
345 "test",
346 r#"{
347 "version": "1.0",
348 "type": "object",
349 "properties": {
350 "has-value": {"type": "string", "default": "", "description": ""},
351 "defined-with-default": {"type": "string", "default": "default_val", "description": "Opt"}
352 }
353 }"#,
354 );
355
356 let options = Options::from_directory(temp.path()).unwrap();
357 assert!(options.isset("test", "not-defined").is_err());
358 assert!(!options.isset("test", "defined-with-default").unwrap());
359 assert!(options.isset("test", "has-value").unwrap());
360 }
361}