Skip to main content

qubit_config/
config_prefix_view.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2025 - 2026.
4 *    Haixing Hu, Qubit Co. Ltd.
5 *
6 *    All rights reserved.
7 *
8 ******************************************************************************/
9#![allow(private_bounds)]
10
11use std::borrow::Cow;
12
13use qubit_value::multi_values::{MultiValuesFirstGetter, MultiValuesGetter};
14use qubit_value::MultiValues;
15
16use crate::config::Config;
17use crate::config_reader::ConfigReader;
18use crate::{ConfigResult, Property};
19
20/// Read-only **prefix** view over a [`Config`]: key lookups use a logical key
21/// prefix.
22///
23/// This type is named explicitly so other kinds of configuration views can be
24/// added later without overloading a generic `ConfigView`.
25///
26/// Lookups rewrite keys by prepending `prefix`, while exposing keys relative to
27/// that prefix.
28#[derive(Debug, Clone)]
29pub struct ConfigPrefixView<'a> {
30    config: &'a Config,
31    prefix: String,
32    full_prefix: Option<String>,
33}
34
35impl<'a> ConfigPrefixView<'a> {
36    /// Builds a prefix view for `config` with the given `prefix` (leading and
37    /// trailing `.` are trimmed; empty means the root).
38    ///
39    /// # Parameters
40    ///
41    /// * `config` - Underlying configuration.
42    /// * `prefix` - Logical prefix for relative keys.
43    ///
44    /// # Returns
45    ///
46    /// A new [`ConfigPrefixView`].
47    #[inline]
48    pub(crate) fn new(config: &'a Config, prefix: &str) -> Self {
49        let normalized_prefix = prefix.trim_matches('.').to_string();
50        let full_prefix = if normalized_prefix.is_empty() {
51            None
52        } else {
53            Some(format!("{normalized_prefix}."))
54        };
55        Self {
56            config,
57            prefix: normalized_prefix,
58            full_prefix,
59        }
60    }
61
62    /// Gets the logical prefix of this view.
63    ///
64    /// # Returns
65    ///
66    /// The normalized prefix string (no leading or trailing dot separators).
67    #[inline]
68    pub fn prefix(&self) -> &str {
69        &self.prefix
70    }
71
72    /// Creates a nested prefix view by appending `prefix`.
73    ///
74    /// # Parameters
75    ///
76    /// * `prefix` - Segment to append (`.` is trimmed); empty keeps the current
77    ///   prefix.
78    ///
79    /// # Returns
80    ///
81    /// A new view with the combined prefix.
82    pub fn prefix_view(&self, prefix: &str) -> ConfigPrefixView<'a> {
83        let child = prefix.trim_matches('.');
84        if self.prefix.is_empty() {
85            ConfigPrefixView::new(self.config, child)
86        } else if child.is_empty() {
87            ConfigPrefixView::new(self.config, self.prefix.as_str())
88        } else {
89            ConfigPrefixView::new(self.config, &format!("{}.{}", self.prefix, child))
90        }
91    }
92
93    /// Maps a caller-supplied key to the storage key used on the underlying
94    /// [`Config`].
95    ///
96    /// # Parameters
97    ///
98    /// * `name` - Relative or already-qualified property key.
99    ///
100    /// # Returns
101    ///
102    /// [`Cow::Borrowed`] when `name` needs no rewrite (empty
103    /// [`Self::prefix`], empty `name`, `name` equal to the view prefix, or
104    /// `name` already starts with `{prefix}.`); otherwise [`Cow::Owned`] with
105    /// `{prefix}.{name}`.
106    fn resolve_key_cow<'b>(&'b self, name: &'b str) -> Cow<'b, str> {
107        if self.prefix.is_empty() || name.is_empty() {
108            return Cow::Borrowed(name);
109        }
110        if name == self.prefix {
111            return Cow::Borrowed(name);
112        }
113        let full_prefix = self
114            .full_prefix
115            .as_deref()
116            .expect("full_prefix must exist for non-empty prefix");
117        if name.starts_with(full_prefix) {
118            return Cow::Borrowed(name);
119        }
120        Cow::Owned(format!("{}.{}", self.prefix, name))
121    }
122
123    fn visible_entries<'b>(&'b self) -> Box<dyn Iterator<Item = (&'b str, &'b Property)> + 'b> {
124        let prefix = self.prefix.as_str();
125        if prefix.is_empty() {
126            return Box::new(self.config.properties.iter().map(|(k, v)| (k.as_str(), v)));
127        }
128        let full_prefix = self
129            .full_prefix
130            .as_deref()
131            .expect("full_prefix must exist for non-empty prefix");
132        Box::new(self.config.properties.iter().filter_map(move |(k, v)| {
133            if k == prefix {
134                Some((prefix, v))
135            } else {
136                k.strip_prefix(full_prefix).map(|stripped| (stripped, v))
137            }
138        }))
139    }
140
141    /// Combines this view's prefix with a relative `sub_prefix` for delegation
142    /// to [`Config::subconfig`] / [`Config::deserialize`].
143    fn effective_root_prefix(&self, sub_prefix: &str) -> String {
144        let child = sub_prefix.trim_matches('.');
145        if self.prefix.is_empty() {
146            child.to_string()
147        } else if child.is_empty() {
148            self.prefix.clone()
149        } else {
150            format!("{}.{}", self.prefix, child)
151        }
152    }
153}
154
155impl<'a> ConfigReader for ConfigPrefixView<'a> {
156    #[inline]
157    fn is_enable_variable_substitution(&self) -> bool {
158        self.config.is_enable_variable_substitution()
159    }
160
161    #[inline]
162    fn max_substitution_depth(&self) -> usize {
163        self.config.max_substitution_depth()
164    }
165
166    #[inline]
167    fn description(&self) -> Option<&str> {
168        self.config.description()
169    }
170
171    fn get_property(&self, name: &str) -> Option<&Property> {
172        let key = self.resolve_key_cow(name);
173        self.config.get_property(key.as_ref())
174    }
175
176    fn len(&self) -> usize {
177        self.visible_entries().count()
178    }
179
180    fn is_empty(&self) -> bool {
181        self.visible_entries().next().is_none()
182    }
183
184    fn keys(&self) -> Vec<String> {
185        self.visible_entries().map(|(k, _)| k.to_string()).collect()
186    }
187
188    fn contains(&self, name: &str) -> bool {
189        let key = self.resolve_key_cow(name);
190        self.config.contains(key.as_ref())
191    }
192
193    fn get<T>(&self, name: &str) -> ConfigResult<T>
194    where
195        MultiValues: MultiValuesFirstGetter<T>,
196    {
197        let key = self.resolve_key_cow(name);
198        self.config.get(key.as_ref())
199    }
200
201    fn get_list<T>(&self, name: &str) -> ConfigResult<Vec<T>>
202    where
203        MultiValues: MultiValuesGetter<T>,
204    {
205        let key = self.resolve_key_cow(name);
206        self.config.get_list(key.as_ref())
207    }
208
209    fn get_optional<T>(&self, name: &str) -> ConfigResult<Option<T>>
210    where
211        MultiValues: MultiValuesFirstGetter<T>,
212    {
213        let key = self.resolve_key_cow(name);
214        self.config.get_optional(key.as_ref())
215    }
216
217    fn get_optional_list<T>(&self, name: &str) -> ConfigResult<Option<Vec<T>>>
218    where
219        MultiValues: MultiValuesGetter<T>,
220    {
221        let key = self.resolve_key_cow(name);
222        self.config.get_optional_list(key.as_ref())
223    }
224
225    fn contains_prefix(&self, prefix: &str) -> bool {
226        self.visible_entries().any(|(k, _)| k.starts_with(prefix))
227    }
228
229    fn iter_prefix<'b>(
230        &'b self,
231        prefix: &'b str,
232    ) -> Box<dyn Iterator<Item = (&'b str, &'b Property)> + 'b> {
233        Box::new(
234            self.visible_entries()
235                .filter(move |(k, _)| k.starts_with(prefix)),
236        )
237    }
238
239    fn iter<'b>(&'b self) -> Box<dyn Iterator<Item = (&'b str, &'b Property)> + 'b> {
240        self.visible_entries()
241    }
242
243    fn is_null(&self, name: &str) -> bool {
244        let key = self.resolve_key_cow(name);
245        self.config.is_null(key.as_ref())
246    }
247
248    fn subconfig(&self, prefix: &str, strip_prefix: bool) -> ConfigResult<Config> {
249        let full = self.effective_root_prefix(prefix);
250        self.config.subconfig(&full, strip_prefix)
251    }
252
253    fn deserialize<T>(&self, prefix: &str) -> ConfigResult<T>
254    where
255        T: serde::de::DeserializeOwned,
256    {
257        let full = self.effective_root_prefix(prefix);
258        self.config.deserialize(&full)
259    }
260
261    #[inline]
262    fn prefix_view(&self, prefix: &str) -> ConfigPrefixView<'a> {
263        ConfigPrefixView::prefix_view(self, prefix)
264    }
265
266    fn resolve_key(&self, name: &str) -> String {
267        if name.is_empty() {
268            return self.prefix.clone();
269        }
270        self.resolve_key_cow(name).into_owned()
271    }
272}