Skip to main content

cloudillo_core/settings/
service.rs

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