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