dynamic_config/reader.rs
1//! Reading a document: the seam between text and this crate's tree.
2//!
3//! A reader answers one question — *what does this text say, in this
4//! format?* — and answers it with a [`Value`]. Everything above it works
5//! on that tree and never on a parser, which is what lets the parser be a
6//! choice.
7//!
8//! ```text
9//! "[db]\nport = 5432\n" + Format::Toml ──▶ {db: {port: 5432}}
10//! ```
11//!
12//! Three ship, and the reason to choose between them is not taste:
13//!
14//! | reader | feature | parses | notes |
15//! |---|---|---|---|
16//! | [`native()`] — **the default** | — | JSON, TOML, YAML, INI, `.properties` | the only `.properties` parser anywhere |
17//! | [`config_rs()`] | — | JSON, TOML, YAML, INI, RON, JSON5 | YAML through the maintained `yaml-rust2` |
18//! | [`figment()`] | `figment` | JSON, TOML, YAML | |
19//!
20//! The column is what each one **parses**, not what a load that chose it
21//! can read: a format the chosen reader has no parser for is handed to one
22//! that has — so choosing `config_rs()` for its YAML does not cost you the
23//! `.properties` file beside it.
24//!
25//! **Unlike the [engines](crate::engine), readers are not interchangeable
26//! down to the corner.** A fold is one rule with an implementation on
27//! each side; a parser is a *dialect*, and two YAML libraries disagree about
28//! things no specification settles. What the tests hold is the part a
29//! deployment depends on — the shapes documents actually take — and the
30//! places they diverge are named in the book rather than papered over.
31//!
32//! Which one runs is the same choice the engine is: `Builder::reader`,
33//! [`LoadSpec::with_reader`](crate::LoadSpec::with_reader), or
34//! [`set_reader`] once for the process.
35
36use std::fmt;
37
38use crate::error::{Error, ErrorKind};
39use crate::source::Format;
40use crate::value::Value;
41
42/// Text in, this crate's tree out.
43///
44/// Implement it to read a format this crate does not ship, or to read one
45/// it does with a parser of your own.
46///
47/// # Errors
48///
49/// A reader's error must **never carry document content**. The line that
50/// failed to parse is, on a bad day, the line holding the password — so a
51/// message says where it stopped and why, and never what it found there.
52pub trait Reader: fmt::Debug + Send + Sync {
53 /// What to call this reader in a diagnostic.
54 fn name(&self) -> &str;
55
56 /// Whether this reader can read `format` in this build.
57 ///
58 /// A reader whose backend was compiled without a format answers
59 /// `false` for it, and the load says which reader could have.
60 fn reads(&self, format: Format) -> bool;
61
62 /// `text`, as this crate's tree.
63 ///
64 /// The result is always a [`Value::Table`]: a document is keys.
65 ///
66 /// # Errors
67 ///
68 /// If the text is not valid in its format.
69 fn parse(&self, text: &str, format: Format) -> Result<Value, Error>;
70}
71
72/// This crate's own parsers.
73///
74/// The only reader that reads `.properties`, and the one whose INI dialect
75/// the book documents.
76#[must_use]
77pub fn native() -> &'static dyn Reader {
78 &Native
79}
80
81/// The [`config`](https://docs.rs/config) crate's parsers.
82///
83/// **Not the default** — [`native()`] is, and the module's own
84/// documentation says why. This is the engine's opposite: there the
85/// backend's fold is what runs unless a load says otherwise, because a
86/// fold can be proved interchangeable and a parser is a dialect.
87///
88/// Reads two formats nothing else here does — RON and JSON5 — and reads
89/// YAML through `yaml-rust2`, which is maintained where this crate's own
90/// `serde_yaml` is archived.
91#[must_use]
92pub fn config_rs() -> &'static dyn Reader {
93 &ConfigRs
94}
95
96/// The [`figment`](https://docs.rs/figment) crate's parsers.
97#[cfg(feature = "figment")]
98#[cfg_attr(docsrs, doc(cfg(feature = "figment")))]
99#[must_use]
100pub fn figment() -> &'static dyn Reader {
101 &Figment
102}
103
104/// Every reader this build ships, this crate's own first.
105///
106/// **The one list.** The agreement tests walk it, so a reader added here
107/// is compared against the others on every corpus without a test being
108/// edited.
109#[must_use]
110pub fn all() -> Vec<&'static dyn Reader> {
111 vec![
112 native(),
113 config_rs(),
114 #[cfg(feature = "figment")]
115 figment(),
116 ]
117}
118
119/// The reader a load uses when nothing chose one: this crate's own.
120///
121/// **Not the backend, unlike the [engine](crate::engine).** The asymmetry
122/// is deliberate and it is about what can be proved. A fold is one rule
123/// with an implementation on each side, and the tests hold both to it leaf
124/// by leaf, so which one runs is not a question about meaning. A parser is
125/// a *dialect*: this crate's INI is the one the book specifies, and
126/// `.properties` has no parser anywhere else — every reader reads one, and
127/// it is this one. Handing those to a different
128/// library by default would change what a document means for everyone who
129/// upgraded, quietly.
130///
131/// The backend's parsers are one call away, and worth having — see
132/// [`config_rs`].
133pub(crate) fn default() -> &'static dyn Reader {
134 native()
135}
136
137static INSTALLED: std::sync::OnceLock<&'static dyn Reader> = std::sync::OnceLock::new();
138
139/// The installed reader, or the default.
140pub(crate) fn installed() -> &'static dyn Reader {
141 INSTALLED.get().copied().unwrap_or_else(default)
142}
143
144/// Installs `reader` for every load in this process that does not name one.
145///
146/// Call it before the first `init()`.
147///
148/// # Errors
149///
150/// If one is already installed. The rejected reader is returned, so a
151/// caller can tell "already set" from "failed".
152pub fn set_reader(reader: &'static dyn Reader) -> Result<(), &'static dyn Reader> {
153 INSTALLED.set(reader)
154}
155
156/// Whether a reader has been installed.
157#[must_use]
158pub fn has_reader() -> bool {
159 INSTALLED.get().is_some()
160}
161
162/// The reader that will parse `format`: the chosen one, or the first that
163/// can.
164///
165/// A reader that does not read a format hands it on rather than refusing
166/// it, which is what makes a format like RON — parsed by the backend and
167/// by nothing here — work without a caller having to install a reader by
168/// hand. Deterministic, in [`all`]'s order, and **additive**: the fallback
169/// can only fire where the chosen reader would have failed outright, so a
170/// load that worked keeps working through exactly the same parser.
171pub(crate) fn for_format(
172 chosen: &'static dyn Reader,
173 format: Format,
174) -> Option<&'static dyn Reader> {
175 if chosen.reads(format) {
176 return Some(chosen);
177 }
178
179 all().into_iter().find(|reader| reader.reads(format))
180}
181
182/// Nobody in this build reads `format`.
183///
184/// Names the readers that *could*, because the answer is almost always a
185/// feature: this crate's own for `.properties`, the backend's for RON.
186pub(crate) fn unread(format: Format) -> Error {
187 let readers: Vec<&str> = all()
188 .into_iter()
189 .filter(|reader| reader.reads(format))
190 .map(Reader::name)
191 .collect();
192
193 let advice = if readers.is_empty() {
194 format!(
195 "add features = [\"{}\"] to your dynamic-config dependency",
196 format.feature()
197 )
198 } else {
199 // Reachable by calling a reader's `parse` directly, and not by
200 // loading: a load hands a format its chosen reader cannot read to
201 // one that can. So the advice is about *this call*, not about a
202 // build that is missing something.
203 format!(
204 "the {} reader{} in this build read{} it, and a load hands the \
205 format to whichever does — this call named one that does not",
206 readers.join(" and the "),
207 if readers.len() > 1 { "s" } else { "" },
208 if readers.len() > 1 { "" } else { "s" },
209 )
210 };
211
212 Error::new(
213 ErrorKind::Backend,
214 format!("nothing here reads {format:?}: {advice}"),
215 )
216}
217
218// ---------------------------------------------------------------------------
219// This crate's own
220// ---------------------------------------------------------------------------
221
222#[derive(Debug)]
223struct Native;
224
225impl Reader for Native {
226 fn name(&self) -> &str {
227 "native"
228 }
229
230 fn reads(&self, format: Format) -> bool {
231 match format {
232 Format::Json => cfg!(feature = "json"),
233 Format::Toml => cfg!(feature = "toml"),
234 Format::Yaml => cfg!(feature = "yaml"),
235 Format::Ini => cfg!(feature = "ini"),
236 Format::Properties => cfg!(feature = "properties"),
237 // Neither has a writer here, and this crate's readers are the
238 // half that has to round-trip with one.
239 Format::Ron | Format::Json5 => false,
240 }
241 }
242
243 fn parse(&self, text: &str, format: Format) -> Result<Value, Error> {
244 crate::document::parse_natively(text, format)
245 }
246}
247
248// ---------------------------------------------------------------------------
249// config-rs
250// ---------------------------------------------------------------------------
251
252use crate::backend::config_rs::Reader as ConfigRs;
253
254// ---------------------------------------------------------------------------
255// figment
256// ---------------------------------------------------------------------------
257
258#[cfg(feature = "figment")]
259use crate::backend::figment::Reader as Figment;