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