Skip to main content

cloudillo_core/settings/
service.rs

1//! Settings service with caching, validation, and permission checks
2
3use lru::LruCache;
4use std::num::NonZeroUsize;
5use std::sync::Arc;
6
7use crate::prelude::*;
8use cloudillo_types::meta_adapter::MetaAdapter;
9
10use super::types::{
11	FrozenSettingsRegistry, Setting, SettingDefinition, SettingScope, SettingValue,
12};
13
14// Compile-time constant for default cache capacity
15const DEFAULT_CACHE_CAPACITY: NonZeroUsize = match NonZeroUsize::new(100) {
16	Some(n) => n,
17	None => unreachable!(),
18};
19
20/// LRU cache for settings values
21pub struct SettingsCache {
22	cache: Arc<parking_lot::RwLock<LruCache<(TnId, String), SettingValue>>>,
23}
24
25impl SettingsCache {
26	pub fn new(capacity: usize) -> Self {
27		let non_zero = NonZeroUsize::new(capacity).unwrap_or(DEFAULT_CACHE_CAPACITY);
28		Self { cache: Arc::new(parking_lot::RwLock::new(LruCache::new(non_zero))) }
29	}
30
31	pub fn get(&self, tn_id: TnId, key: &str) -> Option<SettingValue> {
32		let mut cache = self.cache.write();
33		cache.get(&(tn_id, key.to_string())).cloned()
34	}
35
36	pub fn put(&self, tn_id: TnId, key: String, value: SettingValue) {
37		let mut cache = self.cache.write();
38		cache.put((tn_id, key), value);
39	}
40
41	/// Invalidate all cached settings
42	pub fn clear(&self) {
43		let mut cache = self.cache.write();
44		cache.clear();
45	}
46
47	/// Invalidate specific key across all tenants (when global setting changes)
48	pub fn invalidate_key(&self, _key: &str) {
49		// For simplicity, clear entire cache
50		// TODO: Could optimize to only remove entries with matching key
51		self.clear();
52	}
53}
54
55/// Settings service - main interface for accessing and managing settings
56pub struct SettingsService {
57	registry: Arc<FrozenSettingsRegistry>,
58	cache: SettingsCache,
59	meta: Arc<dyn MetaAdapter>,
60}
61
62impl SettingsService {
63	pub fn new(
64		registry: Arc<FrozenSettingsRegistry>,
65		meta: Arc<dyn MetaAdapter>,
66		cache_size: usize,
67	) -> Self {
68		Self { registry, cache: SettingsCache::new(cache_size), meta }
69	}
70
71	/// Get setting value with full resolution (tenant -> global -> default)
72	pub async fn get(&self, tn_id: TnId, key: &str) -> ClResult<SettingValue> {
73		// Check cache
74		if let Some(value) = self.cache.get(tn_id, key) {
75			debug!("Setting cache hit: {}.{}", tn_id.0, key);
76			return Ok(value);
77		}
78
79		// Get definition (supports wildcard patterns like "ui.*")
80		let def = self
81			.registry
82			.get(key)
83			.ok_or_else(|| Error::ValidationError(format!("Unknown setting: {}", key)))?;
84
85		// Try tenant-specific setting
86		if tn_id.0 != 0 {
87			if let Some(json_value) = self.meta.read_setting(tn_id, key).await? {
88				let value = serde_json::from_value::<SettingValue>(json_value)
89					.map_err(|e| Error::ValidationError(format!("Invalid setting value: {}", e)))?;
90				self.cache.put(tn_id, key.to_string(), value.clone());
91				return Ok(value);
92			}
93		}
94
95		// Try global setting
96		if let Some(json_value) = self.meta.read_setting(TnId(0), key).await? {
97			let value = serde_json::from_value::<SettingValue>(json_value)
98				.map_err(|e| Error::ValidationError(format!("Invalid setting value: {}", e)))?;
99			self.cache.put(tn_id, key.to_string(), value.clone());
100			return Ok(value);
101		}
102
103		// Use default (or error if no default)
104		match &def.default {
105			Some(default) => {
106				let value = default.clone();
107				self.cache.put(tn_id, key.to_string(), value.clone());
108				Ok(value)
109			}
110			None => Err(Error::ValidationError(format!(
111				"Setting '{}' has no default and must be configured",
112				key
113			))),
114		}
115	}
116
117	/// Set setting value with validation and permission checks
118	/// The `roles` parameter should be the authenticated user's roles
119	pub async fn set<S: AsRef<str>>(
120		&self,
121		tn_id: TnId,
122		key: &str,
123		value: SettingValue,
124		roles: &[S],
125	) -> ClResult<Setting> {
126		// Get definition (supports wildcard patterns like "ui.*")
127		let def = self
128			.registry
129			.get(key)
130			.ok_or_else(|| Error::ValidationError(format!("Unknown setting: {}", key)))?;
131
132		// Check permission level
133		if !def.permission.check(roles) {
134			warn!("Permission denied for setting '{}': requires {:?}", key, def.permission);
135			return Err(Error::PermissionDenied);
136		}
137
138		// Check scope validity
139		// Determine the actual tn_id to use for storage
140		let storage_tn_id = match (def.scope, tn_id.0) {
141			(SettingScope::System, _) => {
142				return Err(Error::PermissionDenied);
143			}
144			(SettingScope::Global, 0) => {
145				// OK: Setting global value
146				TnId(0)
147			}
148			(SettingScope::Global, _) => {
149				// Admin users can update global settings from their tenant context
150				// The setting is stored with tn_id=0 to be global
151				if !roles.iter().any(|r| r.as_ref() == "SADM") {
152					return Err(Error::PermissionDenied);
153				}
154				TnId(0)
155			}
156			(SettingScope::Tenant, 0) => {
157				// Setting global default for tenant-scoped setting
158				// This is OK - acts as default for all tenants
159				TnId(0)
160			}
161			(SettingScope::Tenant, _) => {
162				// OK: Setting tenant-specific value
163				tn_id
164			}
165		};
166
167		// Validate type matches definition (if default exists)
168		if let Some(default) = &def.default {
169			if !value.matches_type(default) {
170				return Err(Error::ValidationError(format!(
171					"Type mismatch for setting '{}': expected {}, got {}",
172					key,
173					default.type_name(),
174					value.type_name()
175				)));
176			}
177		}
178
179		// Run custom validator if present
180		if let Some(validator) = &def.validator {
181			validator(&value)?;
182		}
183
184		// Convert to JSON and save to database
185		let json_value = serde_json::to_value(&value)
186			.map_err(|e| Error::ValidationError(format!("Failed to serialize setting: {}", e)))?;
187		self.meta.update_setting(storage_tn_id, key, Some(json_value)).await?;
188
189		// Invalidate cache
190		if storage_tn_id.0 == 0 {
191			// Global setting changed, invalidate all tenants for this key
192			self.cache.invalidate_key(key);
193		} else {
194			// Just clear cache (simple approach)
195			self.cache.clear();
196		}
197
198		info!("Setting '{}' updated for tn_id={}", key, storage_tn_id.0);
199
200		// Return the setting (note: the current adapter doesn't track updated_at, so we use now)
201		Ok(Setting {
202			key: key.to_string(),
203			value,
204			tn_id: storage_tn_id,
205			updated_at: cloudillo_types::types::Timestamp::now(),
206		})
207	}
208
209	/// Delete a setting (falls back to next level)
210	pub async fn delete(&self, tn_id: TnId, key: &str) -> ClResult<bool> {
211		self.meta.update_setting(tn_id, key, None).await?;
212		self.cache.clear();
213
214		info!("Setting '{}' deleted for tn_id={}", key, tn_id.0);
215		Ok(true)
216	}
217
218	/// Validate that all required settings (no default and not optional) are configured
219	pub async fn validate_required_settings(&self) -> ClResult<()> {
220		for def in self.registry.list() {
221			// Skip optional settings and settings with defaults
222			if def.optional || def.default.is_some() {
223				continue;
224			}
225
226			// This setting is required - check if it's configured globally
227			if self.meta.read_setting(TnId(0), &def.key).await?.is_none() {
228				return Err(Error::ValidationError(format!(
229					"Required setting '{}' is not configured",
230					def.key
231				)));
232			}
233		}
234		Ok(())
235	}
236
237	/// Type-safe getters (required - returns error if not found)
238	pub async fn get_string(&self, tn_id: TnId, key: &str) -> ClResult<String> {
239		match self.get(tn_id, key).await? {
240			SettingValue::String(s) => Ok(s),
241			v => Err(Error::ValidationError(format!(
242				"Setting '{}' is not a string, got {}",
243				key,
244				v.type_name()
245			))),
246		}
247	}
248
249	pub async fn get_int(&self, tn_id: TnId, key: &str) -> ClResult<i64> {
250		match self.get(tn_id, key).await? {
251			SettingValue::Int(i) => Ok(i),
252			v => Err(Error::ValidationError(format!(
253				"Setting '{}' is not an integer, got {}",
254				key,
255				v.type_name()
256			))),
257		}
258	}
259
260	pub async fn get_bool(&self, tn_id: TnId, key: &str) -> ClResult<bool> {
261		match self.get(tn_id, key).await? {
262			SettingValue::Bool(b) => Ok(b),
263			v => Err(Error::ValidationError(format!(
264				"Setting '{}' is not a boolean, got {}",
265				key,
266				v.type_name()
267			))),
268		}
269	}
270
271	pub async fn get_json(&self, tn_id: TnId, key: &str) -> ClResult<serde_json::Value> {
272		match self.get(tn_id, key).await? {
273			SettingValue::Json(j) => Ok(j),
274			v => Err(Error::ValidationError(format!(
275				"Setting '{}' is not JSON, got {}",
276				key,
277				v.type_name()
278			))),
279		}
280	}
281
282	/// Type-safe optional getters (returns None if not found or has no default)
283	/// Still returns error if setting exists but has wrong type
284	pub async fn get_string_opt(&self, tn_id: TnId, key: &str) -> ClResult<Option<String>> {
285		match self.get(tn_id, key).await {
286			Ok(SettingValue::String(s)) => Ok(Some(s)),
287			Ok(v) => Err(Error::ValidationError(format!(
288				"Setting '{}' is not a string, got {}",
289				key,
290				v.type_name()
291			))),
292			Err(Error::ValidationError(msg)) if msg.contains("has no default") => Ok(None),
293			Err(Error::ValidationError(msg)) if msg.contains("Unknown setting") => Ok(None),
294			Err(e) => Err(e),
295		}
296	}
297
298	pub async fn get_int_opt(&self, tn_id: TnId, key: &str) -> ClResult<Option<i64>> {
299		match self.get(tn_id, key).await {
300			Ok(SettingValue::Int(i)) => Ok(Some(i)),
301			Ok(v) => Err(Error::ValidationError(format!(
302				"Setting '{}' is not an integer, got {}",
303				key,
304				v.type_name()
305			))),
306			Err(Error::ValidationError(msg)) if msg.contains("has no default") => Ok(None),
307			Err(Error::ValidationError(msg)) if msg.contains("Unknown setting") => Ok(None),
308			Err(e) => Err(e),
309		}
310	}
311
312	pub async fn get_bool_opt(&self, tn_id: TnId, key: &str) -> ClResult<Option<bool>> {
313		match self.get(tn_id, key).await {
314			Ok(SettingValue::Bool(b)) => Ok(Some(b)),
315			Ok(v) => Err(Error::ValidationError(format!(
316				"Setting '{}' is not a boolean, got {}",
317				key,
318				v.type_name()
319			))),
320			Err(Error::ValidationError(msg)) if msg.contains("has no default") => Ok(None),
321			Err(Error::ValidationError(msg)) if msg.contains("Unknown setting") => Ok(None),
322			Err(e) => Err(e),
323		}
324	}
325
326	pub async fn get_json_opt(
327		&self,
328		tn_id: TnId,
329		key: &str,
330	) -> ClResult<Option<serde_json::Value>> {
331		match self.get(tn_id, key).await {
332			Ok(SettingValue::Json(j)) => Ok(Some(j)),
333			Ok(v) => Err(Error::ValidationError(format!(
334				"Setting '{}' is not JSON, got {}",
335				key,
336				v.type_name()
337			))),
338			Err(Error::ValidationError(msg)) if msg.contains("has no default") => Ok(None),
339			Err(Error::ValidationError(msg)) if msg.contains("Unknown setting") => Ok(None),
340			Err(e) => Err(e),
341		}
342	}
343
344	/// Get reference to registry (for listing all settings)
345	pub fn registry(&self) -> &Arc<FrozenSettingsRegistry> {
346		&self.registry
347	}
348
349	/// List stored settings by prefix with definition metadata
350	///
351	/// This queries the database for actual stored settings matching the prefix,
352	/// then resolves each against the registry (supporting wildcard patterns like "ui.*").
353	/// Global settings are merged with tenant-specific settings (tenant overrides global).
354	pub async fn list_by_prefix(
355		&self,
356		tn_id: TnId,
357		prefix: &str,
358	) -> ClResult<Vec<(String, SettingValue, &SettingDefinition)>> {
359		let prefixes = vec![format!("{}.", prefix)]; // "ui" -> "ui."
360
361		// Get global settings first (tn_id=0)
362		let global_settings = self.meta.list_settings(TnId(0), Some(&prefixes)).await?;
363
364		// Get tenant-specific settings (override global)
365		let tenant_settings = if tn_id.0 != 0 {
366			self.meta.list_settings(tn_id, Some(&prefixes)).await?
367		} else {
368			std::collections::HashMap::new()
369		};
370
371		// Merge: tenant overrides global
372		let mut merged = global_settings;
373		merged.extend(tenant_settings);
374
375		let mut result = Vec::new();
376		for (key, json_value) in merged {
377			if let Some(definition) = self.registry.get(&key) {
378				let value = serde_json::from_value::<SettingValue>(json_value)
379					.map_err(|e| Error::ValidationError(format!("Invalid setting value: {}", e)))?;
380				result.push((key, value, definition));
381			}
382		}
383
384		Ok(result)
385	}
386}
387
388// vim: ts=4