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