facet_kdl/lib.rs
1//! KDL serialization and deserialization using facet-format.
2//!
3//! This crate provides KDL (KDL Document Language) support using the
4//! `FormatParser` and `FormatSerializer` traits from `facet-format`.
5//!
6//! # KDL Format
7//!
8//! KDL is a document language focused on human readability. Each document
9//! consists of nodes, where each node has:
10//! - A **name** (identifier)
11//! - **Arguments** (positional values after the name)
12//! - **Properties** (key=value pairs)
13//! - **Children** (nested nodes inside braces)
14//!
15//! # Mapping to Rust Types
16//!
17//! KDL nodes map to Rust structs using the `kdl::*` attributes:
18//!
19//! - `#[facet(kdl::argument)]` - Field receives a single positional argument
20//! - `#[facet(kdl::arguments)]` - Field receives all positional arguments as Vec
21//! - `#[facet(kdl::property)]` - Field receives a property value
22//! - `#[facet(kdl::child)]` - Field receives a single child node
23//! - `#[facet(kdl::children)]` - Field receives multiple child nodes as Vec
24//!
25//! # Example
26//!
27//! ```ignore
28//! use facet::Facet;
29//! use facet_kdl::from_str;
30//!
31//! #[derive(Facet, Debug)]
32//! struct Server {
33//! #[facet(kdl::argument)]
34//! host: String,
35//! #[facet(kdl::property)]
36//! port: u16,
37//! }
38//!
39//! let kdl = r#"server "localhost" port=8080"#;
40//! let server: Server = from_str(kdl).unwrap();
41//! ```
42
43#![forbid(unsafe_code)]
44
45extern crate alloc;
46
47mod parser;
48mod serializer;
49
50#[cfg(feature = "axum")]
51mod axum;
52
53pub use parser::{KdlDeserializeError, KdlError, KdlParser, KdlProbe};
54
55#[cfg(feature = "axum")]
56pub use axum::{Kdl, KdlRejection};
57pub use serializer::{KdlSerializeError, KdlSerializer, to_string, to_vec};
58
59// Re-export DeserializeError for convenience
60pub use facet_format::DeserializeError;
61
62/// Deserialize a value from a KDL string into an owned type.
63///
64/// Returns rich error diagnostics with source context for display.
65///
66/// # Example
67///
68/// ```ignore
69/// use facet::Facet;
70/// use facet_kdl::from_str;
71///
72/// #[derive(Facet, Debug)]
73/// struct Config {
74/// #[facet(kdl::property)]
75/// name: String,
76/// }
77///
78/// let kdl = r#"config name="test""#;
79/// let config: Config = from_str(kdl).unwrap();
80/// ```
81#[allow(clippy::result_large_err)] // Rich diagnostics require storing source context
82pub fn from_str<T>(input: &str) -> Result<T, KdlDeserializeError>
83where
84 T: facet_core::Facet<'static>,
85{
86 use facet_format::FormatDeserializer;
87 let parser = KdlParser::new(input);
88 let mut de = FormatDeserializer::new_owned(parser);
89 de.deserialize()
90 .map_err(|inner| KdlDeserializeError::new(inner, input.to_string(), Some(T::SHAPE)))
91}
92
93/// Deserialize a value from a KDL string, allowing zero-copy borrowing.
94///
95/// This variant requires the input to outlive the result (`'input: 'facet`),
96/// enabling zero-copy deserialization of string values.
97pub fn from_str_borrowed<'input, 'facet, T>(
98 input: &'input str,
99) -> Result<T, DeserializeError<KdlError>>
100where
101 T: facet_core::Facet<'facet>,
102 'input: 'facet,
103{
104 use facet_format::FormatDeserializer;
105 let parser = KdlParser::new(input);
106 let mut de = FormatDeserializer::new(parser);
107 de.deserialize()
108}
109
110/// Deserialize a value from KDL bytes into an owned type.
111///
112/// This is the recommended default for most use cases. The input does not need
113/// to outlive the result, making it suitable for deserializing from temporary
114/// buffers (e.g., HTTP request bodies).
115///
116/// # Errors
117///
118/// Returns an error if the input is not valid UTF-8 or if deserialization fails.
119///
120/// # Example
121///
122/// ```ignore
123/// use facet::Facet;
124/// use facet_kdl::from_slice;
125///
126/// #[derive(Facet, Debug)]
127/// struct Config {
128/// #[facet(kdl::property)]
129/// name: String,
130/// }
131///
132/// let kdl = b"config name=\"test\"";
133/// let config: Config = from_slice(kdl).unwrap();
134/// ```
135#[allow(clippy::result_large_err)]
136pub fn from_slice<T>(input: &[u8]) -> Result<T, KdlDeserializeError>
137where
138 T: facet_core::Facet<'static>,
139{
140 let s = core::str::from_utf8(input).map_err(|e| {
141 let inner = DeserializeError::Parser(KdlError::InvalidUtf8(e));
142 KdlDeserializeError::new(inner, String::new(), Some(T::SHAPE))
143 })?;
144 from_str(s)
145}
146
147/// Deserialize a value from KDL bytes, allowing zero-copy borrowing.
148///
149/// This variant requires the input to outlive the result (`'input: 'facet`),
150/// enabling zero-copy deserialization of string values.
151///
152/// # Errors
153///
154/// Returns an error if the input is not valid UTF-8 or if deserialization fails.
155pub fn from_slice_borrowed<'input, 'facet, T>(
156 input: &'input [u8],
157) -> Result<T, DeserializeError<KdlError>>
158where
159 T: facet_core::Facet<'facet>,
160 'input: 'facet,
161{
162 let s = core::str::from_utf8(input)
163 .map_err(|e| DeserializeError::Parser(KdlError::InvalidUtf8(e)))?;
164 from_str_borrowed(s)
165}
166
167// KDL attribute grammar for field and container configuration.
168// This allows users to write #[facet(kdl::property)] etc.
169facet::define_attr_grammar! {
170 ns "kdl";
171 crate_path ::facet_kdl;
172
173 /// KDL attribute types for field and container configuration.
174 pub enum Attr {
175 /// Marks a field as a single KDL child node.
176 ///
177 /// The field name (or `rename`) determines which child node to match.
178 /// Use `#[facet(rename = "custom")]` to match a different node name.
179 Child,
180 /// Marks a field as collecting multiple KDL children into a Vec, HashMap, or Set.
181 ///
182 /// When a struct has a single `#[facet(kdl::children)]` field, all child nodes
183 /// are collected into that field (catch-all behavior).
184 ///
185 /// When a struct has multiple `#[facet(kdl::children)]` fields, nodes are routed
186 /// based on matching the node name to the singular form of the field name.
187 Children,
188 /// Marks a field as a KDL property (key=value)
189 Property,
190 /// Marks a field as a single KDL positional argument
191 Argument,
192 /// Marks a field as collecting all KDL positional arguments
193 Arguments,
194 /// Marks a field as storing the KDL node name during deserialization.
195 NodeName,
196 }
197}