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