Skip to main content

zerodds_config/
read.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Typed reads out of a merged tree, for a program's own schema.
4//!
5//! Every refusal is a [`ConfigError::Invalid`] that names the dotted setting,
6//! the origin of the value (file and line, environment variable, flag) and
7//! what the setting accepts. A key a schema does not name is refused the same
8//! way: `tls.cert_fiel` is valid text and a value nobody reads.
9
10extern crate alloc;
11
12use alloc::collections::BTreeMap;
13use alloc::string::{String, ToString};
14use alloc::vec::Vec;
15
16use crate::{ConfigError, Node, Origin};
17
18/// One map of the tree, read under a dotted prefix.
19#[derive(Debug, Clone)]
20pub struct Section<'a> {
21    map: Option<&'a BTreeMap<String, Node>>,
22    prefix: String,
23}
24
25static EMPTY: BTreeMap<String, Node> = BTreeMap::new();
26
27/// The origin of the first scalar under `node`, for refusals about a
28/// sequence or a map, which carry none of their own.
29///
30/// zerodds-lint: recursion-depth 32
31#[must_use]
32pub fn first_origin(node: &Node) -> Origin {
33    /// zerodds-lint: recursion-depth 32
34    fn walk(node: &Node, depth: usize) -> Option<&Origin> {
35        if depth > crate::MAX_NESTING_DEPTH {
36            return None;
37        }
38        match node {
39            Node::Scalar(_, origin) => Some(origin),
40            Node::Seq(items) => items.iter().find_map(|n| walk(n, depth + 1)),
41            Node::Map(map) => map.values().find_map(|n| walk(n, depth + 1)),
42        }
43    }
44    walk(node, 0).cloned().unwrap_or(Origin::Default)
45}
46
47fn invalid(setting: String, origin: Origin, message: impl Into<String>) -> ConfigError {
48    ConfigError::Invalid {
49        setting,
50        origin,
51        message: message.into(),
52    }
53}
54
55impl<'a> Section<'a> {
56    /// The root of a tree (`None` = nothing was loaded: every read is absent).
57    ///
58    /// # Errors
59    /// [`ConfigError::Invalid`] when the root is not a map.
60    pub fn root(node: Option<&'a Node>) -> Result<Self, ConfigError> {
61        Self::of(node, "")
62    }
63
64    /// `node` as a section named `prefix`.
65    ///
66    /// # Errors
67    /// [`ConfigError::Invalid`] when `node` is not a map.
68    pub fn of(node: Option<&'a Node>, prefix: impl Into<String>) -> Result<Self, ConfigError> {
69        let prefix = prefix.into();
70        match node {
71            None => Ok(Self { map: None, prefix }),
72            Some(Node::Map(map)) => Ok(Self {
73                map: Some(map),
74                prefix,
75            }),
76            Some(other) => Err(invalid(
77                prefix,
78                first_origin(other),
79                "expected a map of settings",
80            )),
81        }
82    }
83
84    fn path(&self, key: &str) -> String {
85        if self.prefix.is_empty() {
86            key.to_string()
87        } else {
88            alloc::format!("{}.{key}", self.prefix)
89        }
90    }
91
92    /// The dotted path of `key` in this section, for a caller's own refusal.
93    #[must_use]
94    pub fn path_of(&self, key: &str) -> String {
95        self.path(key)
96    }
97
98    /// The raw node at `key`.
99    #[must_use]
100    pub fn node(&self, key: &str) -> Option<&'a Node> {
101        self.map.unwrap_or(&EMPTY).get(key)
102    }
103
104    /// Refuses every key that `allowed` does not name.
105    ///
106    /// # Errors
107    /// [`ConfigError::Invalid`] for the first unknown key, with its origin.
108    pub fn only(&self, allowed: &[&str]) -> Result<(), ConfigError> {
109        for (key, node) in self.map.unwrap_or(&EMPTY) {
110            if !allowed.contains(&key.as_str()) {
111                return Err(invalid(
112                    self.path(key),
113                    first_origin(node),
114                    alloc::format!("unknown setting; known here: {}", allowed.join(", ")),
115                ));
116            }
117        }
118        Ok(())
119    }
120
121    /// The nested section at `key`.
122    ///
123    /// # Errors
124    /// [`ConfigError::Invalid`] when it is not a map.
125    pub fn section(&self, key: &str) -> Result<Section<'a>, ConfigError> {
126        Section::of(self.node(key), self.path(key))
127    }
128
129    /// The scalar at `key` and where it came from.
130    ///
131    /// # Errors
132    /// [`ConfigError::Invalid`] when `key` holds a list or a map.
133    pub fn scalar(&self, key: &str) -> Result<Option<(&'a str, &'a Origin)>, ConfigError> {
134        match self.node(key) {
135            None => Ok(None),
136            Some(Node::Scalar(value, origin)) => Ok(Some((value.as_str(), origin))),
137            Some(other) => Err(invalid(
138                self.path(key),
139                first_origin(other),
140                "expected a single value",
141            )),
142        }
143    }
144
145    /// The text at `key`.
146    ///
147    /// # Errors
148    /// As [`Self::scalar`].
149    pub fn string(&self, key: &str) -> Result<Option<String>, ConfigError> {
150        Ok(self.scalar(key)?.map(|(v, _)| v.to_string()))
151    }
152
153    /// The value at `key`, parsed; `expect` says what the setting accepts.
154    ///
155    /// # Errors
156    /// As [`Self::scalar`], and [`ConfigError::Invalid`] when it does not
157    /// parse.
158    pub fn parse<T: core::str::FromStr>(
159        &self,
160        key: &str,
161        expect: &str,
162    ) -> Result<Option<T>, ConfigError> {
163        match self.scalar(key)? {
164            None => Ok(None),
165            Some((value, origin)) => value.trim().parse::<T>().map(Some).map_err(|_| {
166                invalid(
167                    self.path(key),
168                    origin.clone(),
169                    alloc::format!("got `{value}`, expected {expect}"),
170                )
171            }),
172        }
173    }
174
175    /// `true`/`false` (or `1`/`0`) at `key`.
176    ///
177    /// # Errors
178    /// [`ConfigError::Invalid`] for any other spelling.
179    pub fn boolean(&self, key: &str) -> Result<Option<bool>, ConfigError> {
180        match self.scalar(key)? {
181            None => Ok(None),
182            Some((value, origin)) => match value.trim() {
183                "true" | "1" => Ok(Some(true)),
184                "false" | "0" => Ok(Some(false)),
185                other => Err(invalid(
186                    self.path(key),
187                    origin.clone(),
188                    alloc::format!("got `{other}`, expected true/false"),
189                )),
190            },
191        }
192    }
193
194    /// One of `choices` at `key`.
195    ///
196    /// # Errors
197    /// [`ConfigError::Invalid`] for anything else.
198    pub fn choice(&self, key: &str, choices: &[&str]) -> Result<Option<String>, ConfigError> {
199        match self.scalar(key)? {
200            None => Ok(None),
201            Some((value, origin)) => {
202                let value = value.trim();
203                if choices.contains(&value) {
204                    Ok(Some(value.to_string()))
205                } else {
206                    Err(invalid(
207                        self.path(key),
208                        origin.clone(),
209                        alloc::format!("got `{value}`, expected one of {}", choices.join(", ")),
210                    ))
211                }
212            }
213        }
214    }
215
216    /// A list of texts at `key`: a sequence of scalars, or one scalar as a
217    /// list of one.
218    ///
219    /// # Errors
220    /// [`ConfigError::Invalid`] for a map or a sequence holding one.
221    pub fn strings(&self, key: &str) -> Result<Option<Vec<String>>, ConfigError> {
222        match self.node(key) {
223            None => Ok(None),
224            Some(Node::Scalar(value, _)) => Ok(Some(alloc::vec![value.clone()])),
225            Some(Node::Seq(items)) => items
226                .iter()
227                .map(|item| match item {
228                    Node::Scalar(value, _) => Ok(value.clone()),
229                    other => Err(invalid(
230                        self.path(key),
231                        first_origin(other),
232                        "expected a list of values",
233                    )),
234                })
235                .collect::<Result<Vec<_>, _>>()
236                .map(Some),
237            Some(other) => Err(invalid(
238                self.path(key),
239                first_origin(other),
240                "expected a list of values",
241            )),
242        }
243    }
244
245    /// The items of the sequence at `key`, each with its dotted path.
246    ///
247    /// # Errors
248    /// [`ConfigError::Invalid`] when `key` is not a sequence.
249    pub fn items(&self, key: &str) -> Result<Vec<(String, &'a Node)>, ConfigError> {
250        match self.node(key) {
251            None => Ok(Vec::new()),
252            Some(Node::Seq(items)) => Ok(items
253                .iter()
254                .enumerate()
255                .map(|(i, item)| (alloc::format!("{}[{i}]", self.path(key)), item))
256                .collect()),
257            Some(other) => Err(invalid(
258                self.path(key),
259                first_origin(other),
260                "expected a list",
261            )),
262        }
263    }
264}
265
266/// Splits a bridge's `--topic DDS:OTHER` shorthand at the single `:` that is
267/// not part of an IDL `::`: `Chat::Message:chat/msg` is the DDS topic
268/// `Chat::Message` with `chat/msg` on the other side; without such a `:` the
269/// whole text is the DDS topic.
270#[must_use]
271pub fn split_topic_shorthand(text: &str) -> (String, Option<String>) {
272    let bytes = text.as_bytes();
273    for (i, b) in bytes.iter().enumerate() {
274        let lone =
275            *b == b':' && bytes.get(i + 1) != Some(&b':') && (i == 0 || bytes[i - 1] != b':');
276        if lone {
277            return (
278                text[..i].trim().to_string(),
279                Some(text[i + 1..].trim().to_string()),
280            );
281        }
282    }
283    (text.trim().to_string(), None)
284}