Skip to main content

qubit_config/
config_prefix_view.rs

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