Skip to main content

tjson/
value.rs

1use std::fmt;
2
3use crate::error::{Error, Result};
4use crate::number::Number;
5use crate::options::RenderOptions;
6use crate::parse::{MultilineLocalEol, ParseOptions};
7use crate::render::Renderer;
8use crate::util::{
9    is_allowed_bare_string, /*is_comma_like,*/ is_forbidden_literal_tjson_char, is_pipe_like,
10    is_reserved_word, parse_bare_key_prefix,
11};
12
13/// Single-pass string classifier. Carries the original `&str` plus all renderer-relevant
14/// boolean flags so callers can classify once and branch without re-scanning.
15#[derive(Clone, Copy)]
16pub(crate) struct StrMeta<'a> {
17    pub(crate) s: &'a str,
18    /// `true` when the string contains at least one `\n` (covers both LF and CRLF lines).
19    pub(crate) has_eol: bool,
20    /// `Some(Lf)` / `Some(CrLf)` when newlines are uniform; `None` when mixed or absent.
21    pub(crate) eol_type: Option<MultilineLocalEol>,
22    /// `true` when the string contains any char that is forbidden in literal TJSON strings.
23    pub(crate) has_forbidden_literal: bool,
24    /// `true` when `is_allowed_bare_string` would return `true`.
25    pub(crate) is_bare_eligible: bool,
26    /// `true` when the string matches a reserved word ("true", "false", "null", "[]", "{}", "\"\"").
27    pub(crate) is_reserved_word: bool,
28    /// `true` when the string contains at least one pipe-like character.
29    pub(crate) has_pipe_like: bool,
30    // /// `true` when the string contains at least one comma-like character.
31    //pub(crate) has_comma_like: bool,
32}
33
34impl<'a> StrMeta<'a> {
35    pub(crate) fn new(s: &'a str) -> Self {
36        let has_eol = s.as_bytes().contains(&b'\n');
37        let eol_type = if has_eol { detect_multiline_local_eol(s) } else { Some(MultilineLocalEol::default()) };
38        let has_forbidden_literal = s.chars().any(is_forbidden_literal_tjson_char);
39        let is_bare_eligible = is_allowed_bare_string(s);
40        let is_reserved_word = is_reserved_word(s);
41        let has_pipe_like = s.chars().any(is_pipe_like);
42        // let has_comma_like = s.chars().any(is_comma_like);
43        StrMeta { s, has_eol, eol_type, has_forbidden_literal, is_bare_eligible, is_reserved_word, has_pipe_like/*, has_comma_like*/ }
44    }
45}
46
47/// A `&str` guaranteed to satisfy the TJSON bare-string rules (rendereable without quoting).
48#[allow(dead_code)]
49pub(crate) struct BareString<'a>(StrMeta<'a>);
50
51#[allow(dead_code)]
52impl<'a> BareString<'a> {
53    pub(crate) fn new(s: &'a str) -> Option<Self> {
54        let meta = StrMeta::new(s);
55        if meta.is_bare_eligible { Some(BareString(meta)) } else { None }
56    }
57
58    pub(crate) fn meta(&self) -> &StrMeta<'a> { &self.0 }
59}
60
61impl<'a> std::ops::Deref for BareString<'a> {
62    type Target = str;
63    fn deref(&self) -> &str { self.0.s }
64}
65
66/// A `BareString` that is also safe in table cells: not a reserved word, no pipe-like chars.
67///
68/// `TableBareString` is a strict subtype of `BareString` — it can always be used anywhere
69/// a `BareString` is accepted via `Deref`.
70#[allow(dead_code)]
71pub(crate) struct TableBareString<'a>(BareString<'a>);
72
73#[allow(dead_code)]
74impl<'a> TableBareString<'a> {
75    pub(crate) fn new(s: &'a str) -> Option<Self> {
76        let meta = StrMeta::new(s);
77        if meta.is_bare_eligible && !meta.is_reserved_word && !meta.has_pipe_like {
78            Some(TableBareString(BareString(meta)))
79        } else {
80            None
81        }
82    }
83}
84
85impl<'a> std::ops::Deref for TableBareString<'a> {
86    type Target = BareString<'a>;
87    fn deref(&self) -> &BareString<'a> { &self.0 }
88}
89
90/// A `&str` that can be rendered as a TJSON multiline string: contains newlines (uniform
91/// LF or uniform CRLF) and no forbidden literal characters.
92#[allow(dead_code)]
93pub(crate) struct MultilineString<'a>(StrMeta<'a>);
94
95#[allow(dead_code)]
96impl<'a> MultilineString<'a> {
97    pub(crate) fn new(s: &'a str) -> Option<Self> {
98        let meta = StrMeta::new(s);
99        if meta.has_eol && meta.eol_type.is_some() && !meta.has_forbidden_literal {
100            Some(MultilineString(meta))
101        } else {
102            None
103        }
104    }
105
106    pub(crate) fn eol(&self) -> MultilineLocalEol { self.0.eol_type.unwrap() }
107}
108
109impl<'a> std::ops::Deref for MultilineString<'a> {
110    type Target = str;
111    fn deref(&self) -> &str { self.0.s }
112}
113
114/// A `&str` guaranteed to satisfy the TJSON bare-key rules.
115#[allow(dead_code)]
116pub(crate) struct BareKey<'a>(&'a str);
117
118#[allow(dead_code)]
119impl<'a> BareKey<'a> {
120    pub(crate) fn new(s: &'a str) -> Option<Self> {
121        if parse_bare_key_prefix(s).is_some_and(|end| end == s.len()) {
122            Some(BareKey(s))
123        } else {
124            None
125        }
126    }
127}
128
129impl<'a> std::ops::Deref for BareKey<'a> {
130    type Target = str;
131    fn deref(&self) -> &str { self.0 }
132}
133
134/// A single key-value entry in a TJSON object.
135///
136/// Used instead of a tuple so that code handling object entries can use named fields
137/// rather than `.0` / `.1`. Objects are represented as `Vec<Entry>` to preserve
138/// insertion order and allow duplicate keys.
139#[derive(Clone, Debug, PartialEq, Eq)]
140pub struct Entry {
141    pub key: String,
142    pub value: Value,
143}
144
145/// A parsed TJSON value. Mirrors the JSON type system with the same six variants.
146///
147/// Numbers are stored as [`Number`] values, which preserve the exact string representation.
148/// Objects are stored as an ordered `Vec` of key-value pairs, which allows duplicate keys
149/// at the data structure level (though JSON and TJSON parsers typically deduplicate them).
150#[derive(Clone, Debug, PartialEq, Eq)]
151pub enum Value {
152    /// JSON `null`.
153    Null,
154    /// JSON boolean.
155    Bool(bool),
156    /// JSON number.
157    Number(Number),
158    /// JSON string.
159    String(String),
160    /// JSON array.
161    Array(Vec<Value>),
162    /// JSON object, as an ordered list of key-value pairs.
163    Object(Vec<Entry>),
164}
165
166#[cfg(feature = "serde_json")]
167impl From<serde_json::Value> for Value {
168    fn from(value: serde_json::Value) -> Self {
169        Self::from_serde_json(value)
170    }
171}
172
173#[cfg(feature = "serde_json")]
174impl From<Value> for serde_json::Value {
175    fn from(value: Value) -> Self {
176        value.to_serde_json()
177    }
178}
179
180impl Value {
181    /// Convert from a `serde_json::Value`. Used internally regardless of the `serde_json`
182    /// feature, since serde_json is a hard dependency.
183    pub(crate) fn from_serde_json(value: serde_json::Value) -> Self {
184        match value {
185            serde_json::Value::Null => Self::Null,
186            serde_json::Value::Bool(v) => Self::Bool(v),
187            serde_json::Value::Number(n) => Self::Number(Number(n.to_string())),
188            serde_json::Value::String(s) => Self::String(s),
189            serde_json::Value::Array(values) => {
190                Self::Array(values.into_iter().map(Self::from_serde_json).collect())
191            }
192            serde_json::Value::Object(map) => Self::Object(
193                map.into_iter()
194                    .map(|(key, value)| Entry { key, value: Self::from_serde_json(value) })
195                    .collect(),
196            ),
197        }
198    }
199
200    pub(crate) fn to_serde_json(&self) -> serde_json::Value {
201        match self {
202            Self::Null => serde_json::Value::Null,
203            Self::Bool(v) => serde_json::Value::Bool(*v),
204            Self::Number(n) => serde_json::Value::Number(n.to_serde_json_number()),
205            Self::String(s) => serde_json::Value::String(s.clone()),
206            Self::Array(values) => {
207                serde_json::Value::Array(values.iter().map(Value::to_serde_json).collect())
208            }
209            Self::Object(entries) => {
210                let mut map = serde_json::Map::new();
211                for Entry { key, value } in entries {
212                    map.insert(key.clone(), value.to_serde_json());
213                }
214                serde_json::Value::Object(map)
215            }
216        }
217    }
218
219    pub(crate) fn parse_with(input: &str, options: ParseOptions) -> Result<Self> {
220        crate::parse::Parser::parse_document(input, options.start_indent).map_err(Error::Parse)
221    }
222
223    /// Render this value as a TJSON string using the given options.
224    ///
225    /// ```
226    /// use tjson::{Value, RenderOptions};
227    ///
228    /// let v: Value = "  name: Alice  age:30".parse().unwrap();
229    /// let s = v.to_tjson_with(RenderOptions::canonical());
230    /// assert_eq!(s, "  name: Alice\n  age:30");
231    /// ```
232    pub fn to_tjson_with(&self, options: RenderOptions) -> String {
233        Renderer::render(self, &options)
234    }
235
236    /// Serialize this value to a JSON string.
237    ///
238    /// ```
239    /// use tjson::Value;
240    ///
241    /// let v: Value = "  name: Alice".parse().unwrap();
242    /// assert_eq!(v.to_json(), r#"{"name":"Alice"}"#);
243    /// ```
244    pub fn to_json(&self) -> String {
245        let mut out = String::new();
246        write_json(self, &mut out);
247        out
248    }
249}
250
251fn write_json(value: &Value, out: &mut String) {
252    match value {
253        Value::Null => out.push_str("null"),
254        Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
255        Value::Number(n) => out.push_str(&n.to_string()),
256        Value::String(s) => {
257            // serde_json handles all JSON string escaping correctly.
258            out.push_str(&serde_json::to_string(s).expect("string serialization is infallible"))
259        }
260        Value::Array(values) => {
261            out.push('[');
262            for (i, v) in values.iter().enumerate() {
263                if i > 0 { out.push(','); }
264                write_json(v, out);
265            }
266            out.push(']');
267        }
268        Value::Object(entries) => {
269            out.push('{');
270            for (i, Entry { key, value }) in entries.iter().enumerate() {
271                if i > 0 { out.push(','); }
272                out.push_str(&serde_json::to_string(key).expect("string serialization is infallible"));
273                out.push(':');
274                write_json(value, out);
275            }
276            out.push('}');
277        }
278    }
279}
280
281impl serde::Serialize for Value {
282    fn serialize<S: serde::Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
283        use serde::ser::{SerializeMap, SerializeSeq};
284        match self {
285            Self::Null => serializer.serialize_unit(),
286            Self::Bool(b) => serializer.serialize_bool(*b),
287            Self::Number(n) => serde::Serialize::serialize(n, serializer),
288            Self::String(s) => serializer.serialize_str(s),
289            Self::Array(values) => {
290                let mut seq = serializer.serialize_seq(Some(values.len()))?;
291                for v in values {
292                    seq.serialize_element(v)?;
293                }
294                seq.end()
295            }
296            Self::Object(entries) => {
297                let mut map = serializer.serialize_map(Some(entries.len()))?;
298                for Entry { key, value } in entries {
299                    map.serialize_entry(key, value)?;
300                }
301                map.end()
302            }
303        }
304    }
305}
306
307impl fmt::Display for Value {
308    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309        f.write_str(&self.to_tjson_with(RenderOptions::default()))
310    }
311}
312
313/// ```
314/// let v: tjson::Value = "  name: Alice".parse().unwrap();
315/// assert!(matches!(v, tjson::Value::Object(_)));
316/// ```
317impl std::str::FromStr for Value {
318    type Err = Error;
319
320    fn from_str(s: &str) -> Result<Self> {
321        Self::parse_with(s, ParseOptions::default())
322    }
323}
324
325pub(crate) fn detect_multiline_local_eol(value: &str) -> Option<MultilineLocalEol> {
326    let bytes = value.as_bytes();
327    let mut index = 0usize;
328    let mut saw_lf = false;
329    let mut saw_crlf = false;
330
331    while index < bytes.len() {
332        match bytes[index] {
333            b'\r' => {
334                if bytes.get(index + 1) == Some(&b'\n') {
335                    saw_crlf = true;
336                    index += 2;
337                } else {
338                    return None;
339                }
340            }
341            b'\n' => {
342                saw_lf = true;
343                index += 1;
344            }
345            _ => index += 1,
346        }
347    }
348
349    match (saw_lf, saw_crlf) {
350        (false, false) => None,
351        (true, false) => Some(MultilineLocalEol::Lf),
352        (false, true) => Some(MultilineLocalEol::CrLf),
353        (true, true) => None,
354    }
355}