Skip to main content

tanzim_parse/
lib.rs

1#![doc = include_str!("../README.md")]
2#![deny(missing_docs)]
3
4pub use tanzim_source::Source;
5pub use tanzim_value::{Error, LocatedValue, Value};
6
7pub mod closure;
8pub mod span;
9
10#[cfg(feature = "env")]
11pub mod env;
12#[cfg(feature = "json")]
13pub mod json;
14#[cfg(feature = "toml")]
15pub mod toml;
16#[cfg(feature = "yaml")]
17pub mod yaml;
18
19/// Parses raw bytes into a [`LocatedValue`] tree for one format.
20///
21/// Implement this to add a new configuration format. This is the second pipeline stage: it turns
22/// the raw bytes a loader produced into a typed, source-located value tree for merging.
23///
24/// # Contract
25///
26/// - [`parse`](Parse::parse) returns one [`LocatedValue`] tree per payload. `source` carries the
27///   source kind (e.g. `"file"`), the resource path/identifier, and any loader options.
28/// - Every node in the tree — including the root — should carry a [`tanzim_value::Location`] that
29///   points back to the source, resource, and line/column, so downstream error messages can show
30///   users exactly where a bad value came from. Use [`tanzim_value::Location::at`] to build them.
31/// - [`supported_format_list`](Parse::supported_format_list) may return several extensions
32///   for one parser (e.g. `["yml", "yaml"]`). When a payload carries no format hint, selection
33///   instead falls back to probing — see Auto-detection below.
34///
35/// # Auto-detection
36///
37/// When a payload's `format` hint is `None`, the parse stage calls
38/// [`is_format_supported`][Parse::is_format_supported] on each registered
39/// parser in order. Return `Some(true)` if confident, `Some(false)` to skip, or `None`
40/// if unsure (another parser may then claim the bytes).
41///
42/// # Choosing an error
43///
44/// Failures are reported with [`tanzim_value::Error`]; every variant except `Parse` carries a
45/// [`Location`](tanzim_value::Location):
46///
47/// - [`Error::InvalidUtf8`] — the bytes aren't valid UTF-8.
48/// - [`Error::Parse`] — a syntax or structural error; set `location` when you can pinpoint it,
49///   otherwise `None`.
50/// - [`Error::UnsupportedType`] — a value of a type that has no configuration representation
51///   (e.g. a date-time).
52///
53/// # Registering
54///
55/// Pass an instance to `tanzim::Config::with_parser`. The pipeline picks a parser by the payload's
56/// format hint when present, otherwise it probes each parser with
57/// [`is_format_supported`](Parse::is_format_supported). For a one-off parser you don't want
58/// to define a type for, use [`closure::Closure`] instead of implementing this trait.
59///
60/// # Example — custom CSV parser
61///
62/// ```rust
63/// use tanzim_parse::{Parse, Source};
64/// use tanzim_source::SourceBuilder;
65/// use tanzim_value::{Error, LocatedValue, Location, Map, Value};
66///
67/// struct CsvParser;
68///
69/// impl Parse for CsvParser {
70///     fn name(&self) -> &str { "csv" }
71///     fn supported_format_list(&self) -> Vec<String> { vec!["csv".into()] }
72///     fn is_format_supported(&self, bytes: &[u8]) -> Option<bool> {
73///         Some(bytes.contains(&b','))
74///     }
75///     fn parse(&self, source: &Source, bytes: &[u8], _other_source_list: &[Source]) -> Result<LocatedValue, Error> {
76///         let source_name = source.source();
77///         let resource = source.resource();
78///         let text = match std::str::from_utf8(bytes) {
79///             Ok(value) => value,
80///             Err(_) => {
81///                 return Err(Error::InvalidUtf8 {
82///                     location: Box::new(Location::at(source_name, resource, None, None, None)),
83///                 });
84///             }
85///         };
86///         let mut map = Map::new();
87///         for (line_idx, line) in text.lines().enumerate() {
88///             if let Some((key, val)) = line.split_once(',') {
89///                 let loc = Location::at(source_name, resource, Some(line_idx + 1), None, None);
90///                 map.insert(key.trim().to_string(), LocatedValue::new(
91///                     Value::String(val.trim().to_string()),
92///                     loc,
93///                 ));
94///             }
95///         }
96///         let root_loc = Location::at(source_name, resource, None, None, None);
97///         Ok(LocatedValue::new(Value::Map(map), root_loc))
98///     }
99/// }
100///
101/// let source = SourceBuilder::new()
102///     .with_source("file")
103///     .with_resource("config.csv")
104///     .build()
105///     .unwrap();
106/// let value = CsvParser
107///     .parse(&source, b"host,127.0.0.1\nport,8080\n", &[])
108///     .unwrap();
109///
110/// let map = value.value().as_map().unwrap();
111/// assert_eq!(map.get("host").unwrap().value().as_string().unwrap(), "127.0.0.1");
112/// assert_eq!(map.get("port").unwrap().value().as_string().unwrap(), "8080");
113/// // `port` is a string — this parser stores every field verbatim.
114/// ```
115pub trait Parse {
116    /// Human-readable name used in error messages.
117    fn name(&self) -> &str;
118    /// Format extensions this parser handles (e.g. `["json"]`, `["yml", "yaml"]`).
119    fn supported_format_list(&self) -> Vec<String>;
120    /// Probe `bytes` for auto-detection when `Payload::maybe_format` is `None`.
121    ///
122    /// Return `Some(true)` if confident, `Some(false)` if definitely not this format,
123    /// or `None` to abstain (another parser will be tried next).
124    fn is_format_supported(&self, bytes: &[u8]) -> Option<bool>;
125    /// Parse `bytes` into a [`LocatedValue`] tree.
126    ///
127    /// `source` carries the source kind (e.g. `"file"`), the resource path or
128    /// identifier, and any loader options. Use [`Source::source`], [`Source::resource`],
129    /// and [`Source::options`] to access them. `other_source_list` is the pipeline's
130    /// full configured source list; parsers may consult sibling sources when options on
131    /// `source` alone are insufficient. Every node in the returned tree should carry a
132    /// [`tanzim_value::Location`] built from those values.
133    fn parse(
134        &self,
135        source: &Source,
136        bytes: &[u8],
137        other_source_list: &[Source],
138    ) -> Result<LocatedValue, Error>;
139}