ktav/lib.rs
1//! # Ktav — a plain configuration format
2//!
3//! JSON5-shaped, but with no quotes, no commas, and dotted keys for nesting.
4//! A document is an implicit top-level object. Native `serde` integration:
5//! any type implementing `Serialize` / `Deserialize` (including
6//! `#[derive]`-generated ones) round-trips through Ktav out of the box.
7//!
8//! ## Syntax
9//!
10//! ```text
11//! # comment — any line starting with '#'
12//! key: value — scalar; `key` may be a dotted path (a.b.c: 10)
13//! key:: value — scalar; value is ALWAYS a literal string
14//! key: { ... } — multi-line object; `}` closes on its own line
15//! key: [ ... ] — multi-line array; `]` closes on its own line
16//! key: {} / key: [] — empty compound, inline
17//! :: value — (inside an array) literal-string item
18//! ```
19//!
20//! ## Structured errors
21//!
22//! The parser returns [`Error::Structured`] for every parse failure since
23//! `0.1.5`. Each [`ErrorKind`] variant carries a 1-based `line` and a
24//! byte-offset [`Span`] you can slice the original input with.
25//!
26//! ```text
27//! use ktav::{parse, Error, ErrorKind};
28//!
29//! // The first line anchors the document as an Object; the malformed
30//! // `port:8080` on line 2 then errors with a `MissingSeparatorSpace`
31//! // (a first-line `port:8080` would now be a top-level Array
32//! // bare-scalar item — spec § 5.0.1).
33//! let src = "anchor: ok\nport:8080\n";
34//! match parse(src) {
35//! Ok(_) => unreachable!(),
36//! Err(Error::Structured(ErrorKind::MissingSeparatorSpace { line, span, .. })) => {
37//! assert_eq!(line, 2);
38//! // The span covers the offending body chunk glued to the marker.
39//! assert_eq!(span.slice(src), Some("8080"));
40//! }
41//! Err(other) => panic!("unexpected error: {other:?}"),
42//! }
43//! ```
44//! (See `tests/error_accessors.rs` for the executed test.)
45//!
46//! ## Example
47//!
48//! See [`tests/doc_example.rs`](../tests/doc_example.rs) for the executed
49//! version of this snippet — it exercises the full parse → struct → render
50//! → parse round-trip:
51//!
52//! ```text
53//! use serde::{Deserialize, Serialize};
54//!
55//! #[derive(Debug, Serialize, Deserialize, PartialEq)]
56//! struct Upstream {
57//! host: String,
58//! port: u16,
59//! }
60//!
61//! #[derive(Debug, Serialize, Deserialize, PartialEq)]
62//! struct Config {
63//! port: u16,
64//! upstreams: Vec<Upstream>,
65//! }
66//!
67//! let text = "\
68//! port: 8080
69//!
70//! upstreams: [
71//! {
72//! host: a.example
73//! port: 1080
74//! }
75//! {
76//! host: b.example
77//! port: 1080
78//! }
79//! ]
80//! ";
81//! let cfg: Config = ktav::from_str(text).unwrap();
82//! assert_eq!(cfg.port, 8080);
83//! assert_eq!(cfg.upstreams.len(), 2);
84//!
85//! let back = ktav::to_string(&cfg).unwrap();
86//! let round: Config = ktav::from_str(&back).unwrap();
87//! assert_eq!(cfg, round);
88//! ```
89#![allow(clippy::module_inception)]
90#![warn(missing_docs)]
91
92pub mod de;
93pub mod error;
94pub mod parser;
95pub mod render;
96pub mod ser;
97pub mod thin;
98pub mod value;
99
100pub use error::{CompoundKind, ConflictKind, Error, ErrorKind, Result, Span};
101pub use thin::{parse_events, ParseEvent};
102pub use value::{ObjectMap, Value};
103
104use std::fs;
105use std::path::Path;
106
107use serde::de::DeserializeOwned;
108use serde::Serialize;
109
110/// Parse a Ktav document from a string into a raw [`Value`]. Useful when
111/// you want to inspect or manipulate the document generically. For
112/// deserializing into a user type, prefer [`from_str`].
113pub fn parse(text: &str) -> Result<Value> {
114 parser::parse_str(text)
115}
116
117/// Parse a Ktav document from a string and deserialize it into `T`. Uses
118/// the zero-copy event path: the parser tokenizes the document into a
119/// flat `Vec<Event>` (object keys and single-line scalars are borrowed
120/// directly from `s`), and serde walks that vec linearly without ever
121/// materialising a tree. Compound nesting is bracketed by
122/// `BeginObject`/`EndObject` events instead of nested allocations.
123pub fn from_str<T: DeserializeOwned>(s: &str) -> Result<T> {
124 // Pre-size the bump arena to avoid re-allocations during event parsing.
125 // Each Event is 24 bytes; the pre-allocated count is ~text.len()/4.
126 // Adding overhead for the bump metadata and per-object-level BumpVecs.
127 let arena_bytes = (s.len() / 4).saturating_mul(24) + 4096;
128 let bump = bumpalo::Bump::with_capacity(arena_bytes);
129 let events = thin::parse_events_raw(s, &bump)?;
130 let mut cursor = thin::EventCursor::new(&events);
131 T::deserialize(thin::EventDeserializer::new(&mut cursor))
132}
133
134/// Parse a Ktav document from a file path and deserialize it into `T`.
135pub fn from_file<T: DeserializeOwned, P: AsRef<Path>>(path: P) -> Result<T> {
136 let text = fs::read_to_string(path)?;
137 from_str(&text)
138}
139
140/// Serialize `value` as a Ktav document string. Uses the direct text
141/// serializer — no `Value` intermediate.
142pub fn to_string<T: ?Sized + Serialize>(value: &T) -> Result<String> {
143 ser::to_string(value)
144}
145
146/// Serialize `value` as a Ktav document and write it to `path`.
147pub fn to_file<T: ?Sized + Serialize, P: AsRef<Path>>(value: &T, path: P) -> Result<()> {
148 let text = to_string(value)?;
149 fs::write(path, text)?;
150 Ok(())
151}
152
153/// Render a [`Value`] with **every scalar coerced to a String** —
154/// typed integers (`:i`), typed floats (`:f`), booleans, and null
155/// are flattened to their textual form and emitted via the raw-
156/// marker `::`. Compounds (Object / Array) preserve their structure;
157/// only leaf scalars are coerced. The output round-trips back
158/// through the parser as the same set of String scalars.
159///
160/// Useful for "everything is a string" dumps — e.g. for downstream
161/// consumers that don't understand typed markers, or for diff-
162/// friendly canonical text.
163pub fn to_string_force_strings(value: &Value) -> Result<String> {
164 render::to_string_force_strings(value)
165}
166
167/// Emit a canonical Ktav serialisation of `value` (spec § 5.9).
168///
169/// The top-level value must be an Object or an Array (§ 5.0.1).
170/// Returns an error for any other variant, or if a String contains a
171/// `CR` byte (not representable in canonical form, § 5.9.7).
172///
173/// Two writer-conforming implementations fed the same [`Value`] MUST produce
174/// identical output (§ 8.2).
175pub fn emit_canonical(value: &Value) -> Result<String> {
176 render::emit_canonical(value)
177}