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 registry = SchemaRegistry::from_directory(&base_dir.join("schemas"))?;
57 Self::with_registry_and_values(registry, &base_dir.join("values"))
58 }
59
60 pub fn from_schemas(schemas: &[(&str, &str)]) -> Result<Self> {
63 let registry = SchemaRegistry::from_schemas(schemas)?;
64 Self::with_registry_and_values(registry, &resolve_options_dir().join("values"))
65 }
66
67 fn with_registry_and_values(registry: SchemaRegistry, values_dir: &Path) -> Result<Self> {
68 let registry = Arc::new(registry);
69 let (loaded_values, _) = registry.load_values_json(values_dir)?;
70 let values = Arc::new(RwLock::new(loaded_values));
71 let watcher = ValuesWatcher::new(values_dir, Arc::clone(®istry), Arc::clone(&values))?;
72 Ok(Self {
73 registry,
74 values,
75 _watcher: watcher,
76 })
77 }
78
79 pub fn get(&self, namespace: &str, key: &str) -> Result<Value> {
81 if let Some(value) = testing::get_override(namespace, key) {
82 return Ok(value);
83 }
84
85 let schema = self
86 .registry
87 .get(namespace)
88 .ok_or_else(|| OptionsError::UnknownNamespace(namespace.to_string()))?;
89
90 let values_guard = self
91 .values
92 .read()
93 .unwrap_or_else(|poisoned| poisoned.into_inner());
94 if let Some(ns_values) = values_guard.get(namespace)
95 && let Some(value) = ns_values.get(key)
96 {
97 return Ok(value.clone());
98 }
99
100 let default = schema
101 .get_default(key)
102 .ok_or_else(|| OptionsError::UnknownOption {
103 namespace: namespace.to_string(),
104 key: key.to_string(),
105 })?;
106
107 Ok(default.clone())
108 }
109
110 pub fn validate_override(&self, namespace: &str, key: &str, value: &Value) -> Result<()> {
112 let schema = self
113 .registry
114 .get(namespace)
115 .ok_or_else(|| OptionsError::UnknownNamespace(namespace.to_string()))?;
116
117 schema.validate_option(key, value)?;
118
119 Ok(())
120 }
121 pub fn isset(&self, namespace: &str, key: &str) -> Result<bool> {
128 let schema = self
129 .registry
130 .get(namespace)
131 .ok_or_else(|| OptionsError::UnknownNamespace(namespace.to_string()))?;
132
133 if !schema.options.contains_key(key) {
134 return Err(OptionsError::UnknownOption {
135 namespace: namespace.into(),
136 key: key.into(),
137 });
138 }
139
140 let values_guard = self
141 .values
142 .read()
143 .unwrap_or_else(|poisoned| poisoned.into_inner());
144
145 if let Some(ns_values) = values_guard.get(namespace) {
146 Ok(ns_values.contains_key(key))
147 } else {
148 Ok(false)
149 }
150 }
151}
152
153pub fn init() -> Result<()> {
158 if GLOBAL_OPTIONS.get().is_some() {
159 return Ok(());
160 }
161 let opts = Options::new()?;
162 let _ = GLOBAL_OPTIONS.set(opts);
163 Ok(())
164}
165
166pub fn init_with_schemas(schemas: &[(&str, &str)]) -> Result<()> {
179 if GLOBAL_OPTIONS.get().is_some() {
180 return Ok(());
181 }
182 let opts = Options::from_schemas(schemas)?;
183 let _ = GLOBAL_OPTIONS.set(opts);
184 Ok(())
185}
186
187pub fn options(namespace: &str) -> Result<NamespaceOptions> {
191 let opts = GLOBAL_OPTIONS.get().ok_or(OptionsError::NotInitialized)?;
192 Ok(NamespaceOptions {
193 namespace: namespace.to_string(),
194 options: opts,
195 })
196}
197
198pub struct NamespaceOptions {
200 namespace: String,
201 options: &'static Options,
202}
203
204impl NamespaceOptions {
205 pub fn get(&self, key: &str) -> Result<Value> {
207 self.options.get(&self.namespace, key)
208 }
209
210 pub fn isset(&self, key: &str) -> Result<bool> {
212 self.options.isset(&self.namespace, key)
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use serde_json::json;
220 use std::fs;
221 use tempfile::TempDir;
222
223 fn create_schema(dir: &Path, namespace: &str, schema: &str) {
224 let schema_dir = dir.join(namespace);
225 fs::create_dir_all(&schema_dir).unwrap();
226 fs::write(schema_dir.join("schema.json"), schema).unwrap();
227 }
228
229 fn create_values(dir: &Path, namespace: &str, values: &str) {
230 let ns_dir = dir.join(namespace);
231 fs::create_dir_all(&ns_dir).unwrap();
232 fs::write(ns_dir.join("values.json"), values).unwrap();
233 }
234
235 #[test]
236 fn test_get_value() {
237 let temp = TempDir::new().unwrap();
238 let schemas = temp.path().join("schemas");
239 let values = temp.path().join("values");
240 fs::create_dir_all(&schemas).unwrap();
241
242 create_schema(
243 &schemas,
244 "test",
245 r#"{
246 "version": "1.0",
247 "type": "object",
248 "properties": {
249 "enabled": {
250 "type": "boolean",
251 "default": false,
252 "description": "Enable feature"
253 }
254 }
255 }"#,
256 );
257 create_values(&values, "test", r#"{"options": {"enabled": true}}"#);
258
259 let options = Options::from_directory(temp.path()).unwrap();
260 assert_eq!(options.get("test", "enabled").unwrap(), json!(true));
261 }
262
263 #[test]
264 fn test_get_default() {
265 let temp = TempDir::new().unwrap();
266 let schemas = temp.path().join("schemas");
267 let values = temp.path().join("values");
268 fs::create_dir_all(&schemas).unwrap();
269 fs::create_dir_all(&values).unwrap();
270
271 create_schema(
272 &schemas,
273 "test",
274 r#"{
275 "version": "1.0",
276 "type": "object",
277 "properties": {
278 "timeout": {
279 "type": "integer",
280 "default": 30,
281 "description": "Timeout"
282 }
283 }
284 }"#,
285 );
286
287 let options = Options::from_directory(temp.path()).unwrap();
288 assert_eq!(options.get("test", "timeout").unwrap(), json!(30));
289 }
290
291 #[test]
292 fn test_unknown_namespace() {
293 let temp = TempDir::new().unwrap();
294 let schemas = temp.path().join("schemas");
295 let values = temp.path().join("values");
296 fs::create_dir_all(&schemas).unwrap();
297 fs::create_dir_all(&values).unwrap();
298
299 create_schema(
300 &schemas,
301 "test",
302 r#"{"version": "1.0", "type": "object", "properties": {}}"#,
303 );
304
305 let options = Options::from_directory(temp.path()).unwrap();
306 assert!(matches!(
307 options.get("unknown", "key"),
308 Err(OptionsError::UnknownNamespace(_))
309 ));
310 }
311
312 #[test]
313 fn test_unknown_option() {
314 let temp = TempDir::new().unwrap();
315 let schemas = temp.path().join("schemas");
316 let values = temp.path().join("values");
317 fs::create_dir_all(&schemas).unwrap();
318 fs::create_dir_all(&values).unwrap();
319
320 create_schema(
321 &schemas,
322 "test",
323 r#"{
324 "version": "1.0",
325 "type": "object",
326 "properties": {
327 "known": {"type": "string", "default": "x", "description": "Known"}
328 }
329 }"#,
330 );
331
332 let options = Options::from_directory(temp.path()).unwrap();
333 assert!(matches!(
334 options.get("test", "unknown"),
335 Err(OptionsError::UnknownOption { .. })
336 ));
337 }
338
339 #[test]
340 fn test_missing_values_dir() {
341 let temp = TempDir::new().unwrap();
342 let schemas = temp.path().join("schemas");
343 fs::create_dir_all(&schemas).unwrap();
344
345 create_schema(
346 &schemas,
347 "test",
348 r#"{
349 "version": "1.0",
350 "type": "object",
351 "properties": {
352 "opt": {"type": "string", "default": "default_val", "description": "Opt"}
353 }
354 }"#,
355 );
356
357 let options = Options::from_directory(temp.path()).unwrap();
358 assert_eq!(options.get("test", "opt").unwrap(), json!("default_val"));
359 }
360
361 #[test]
362 fn isset_with_defined_and_undefined_keys() {
363 let temp = TempDir::new().unwrap();
364 let schemas = temp.path().join("schemas");
365 fs::create_dir_all(&schemas).unwrap();
366
367 let values = temp.path().join("values");
368 create_values(&values, "test", r#"{"options": {"has-value": "yes"}}"#);
369
370 create_schema(
371 &schemas,
372 "test",
373 r#"{
374 "version": "1.0",
375 "type": "object",
376 "properties": {
377 "has-value": {"type": "string", "default": "", "description": ""},
378 "defined-with-default": {"type": "string", "default": "default_val", "description": "Opt"}
379 }
380 }"#,
381 );
382
383 let options = Options::from_directory(temp.path()).unwrap();
384 assert!(options.isset("test", "not-defined").is_err());
385 assert!(!options.isset("test", "defined-with-default").unwrap());
386 assert!(options.isset("test", "has-value").unwrap());
387 }
388
389 #[test]
390 fn test_from_schemas_get_default() {
391 let schema = r#"{
392 "version": "1.0",
393 "type": "object",
394 "properties": {
395 "enabled": {
396 "type": "boolean",
397 "default": false,
398 "description": "Enable feature"
399 }
400 }
401 }"#;
402
403 let registry = SchemaRegistry::from_schemas(&[("test", schema)]).unwrap();
404 let default = registry
405 .get("test")
406 .unwrap()
407 .get_default("enabled")
408 .unwrap();
409 assert_eq!(*default, json!(false));
410 }
411
412 #[test]
413 fn test_from_schemas_with_values() {
414 let temp = TempDir::new().unwrap();
415 let values_dir = temp.path().join("values");
416 create_values(&values_dir, "test", r#"{"options": {"enabled": true}}"#);
417
418 let schema = r#"{
419 "version": "1.0",
420 "type": "object",
421 "properties": {
422 "enabled": {
423 "type": "boolean",
424 "default": false,
425 "description": "Enable feature"
426 }
427 }
428 }"#;
429
430 let registry = Arc::new(SchemaRegistry::from_schemas(&[("test", schema)]).unwrap());
431 let (loaded_values, _) = registry.load_values_json(&values_dir).unwrap();
432 assert_eq!(loaded_values["test"]["enabled"], json!(true));
433 }
434
435 #[test]
436 fn test_from_schemas_invalid_json() {
437 let result = SchemaRegistry::from_schemas(&[("test", "not valid json")]);
438 assert!(result.is_err());
439 }
440}