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}