facet_styx/
lib.rs

1#![doc = include_str!("../README.md")]
2//! Styx format support for facet.
3//!
4//! This crate provides Styx deserialization and serialization using the facet
5//! reflection system.
6//!
7//! # Deserialization Example
8//!
9//! ```
10//! use facet::Facet;
11//! use facet_styx::from_str;
12//!
13//! #[derive(Facet, Debug, PartialEq)]
14//! struct Config {
15//!     name: String,
16//!     port: u16,
17//! }
18//!
19//! let styx = "name myapp\nport 8080";
20//! let config: Config = from_str(styx).unwrap();
21//! assert_eq!(config.name, "myapp");
22//! assert_eq!(config.port, 8080);
23//! ```
24//!
25//! # Serialization Example
26//!
27//! ```
28//! use facet::Facet;
29//! use facet_styx::to_string;
30//!
31//! #[derive(Facet, Debug)]
32//! struct Config {
33//!     name: String,
34//!     port: u16,
35//! }
36//!
37//! let config = Config { name: "myapp".into(), port: 8080 };
38//! let styx = to_string(&config).unwrap();
39//! assert!(styx.contains("name myapp"));
40//! assert!(styx.contains("port 8080"));
41//! ```
42
43mod error;
44#[cfg(test)]
45mod idempotency_test;
46#[cfg(test)]
47mod other_variant_test;
48mod parser;
49mod schema_error;
50mod schema_gen;
51mod schema_meta;
52mod schema_types;
53mod schema_validate;
54mod serializer;
55#[cfg(test)]
56mod tag_events_test;
57mod tracing_macros;
58#[cfg(test)]
59mod value_expr_test;
60
61pub use error::{RenderError, StyxError, StyxErrorKind};
62pub use facet_format::DeserializeError;
63pub use facet_format::SerializeError;
64pub use parser::StyxParser;
65pub use schema_error::{ValidationError, ValidationErrorKind, ValidationResult, ValidationWarning};
66pub use schema_gen::{GenerateSchema, schema_file_from_type, schema_from_type};
67pub use schema_meta::META_SCHEMA_SOURCE;
68pub use schema_types::*;
69pub use schema_validate::{Validator, validate, validate_as};
70pub use serializer::{
71    SerializeOptions, StyxSerializeError, StyxSerializer, peek_to_string, peek_to_string_expr,
72    peek_to_string_with_options, to_string, to_string_compact, to_string_with_options,
73};
74
75/// Deserialize a value from a Styx string into an owned type.
76///
77/// This is the recommended default for most use cases.
78///
79/// # Example
80///
81/// ```
82/// use facet::Facet;
83/// use facet_styx::from_str;
84///
85/// #[derive(Facet, Debug, PartialEq)]
86/// struct Person {
87///     name: String,
88///     age: u32,
89/// }
90///
91/// let styx = "name Alice\nage 30";
92/// let person: Person = from_str(styx).unwrap();
93/// assert_eq!(person.name, "Alice");
94/// assert_eq!(person.age, 30);
95/// ```
96pub fn from_str<T>(input: &str) -> Result<T, DeserializeError<StyxError>>
97where
98    T: facet_core::Facet<'static>,
99{
100    use facet_format::FormatDeserializer;
101    let parser = StyxParser::new(input);
102    let mut de = FormatDeserializer::new_owned(parser);
103    de.deserialize_root()
104}
105
106/// Deserialize a value from a Styx string, allowing zero-copy borrowing.
107///
108/// This variant requires the input to outlive the result, enabling
109/// zero-copy deserialization of string fields as `&str` or `Cow<str>`.
110///
111/// # Example
112///
113/// ```
114/// use facet::Facet;
115/// use facet_styx::from_str_borrowed;
116///
117/// #[derive(Facet, Debug, PartialEq)]
118/// struct Person<'a> {
119///     name: &'a str,
120///     age: u32,
121/// }
122///
123/// let styx = "name Alice\nage 30";
124/// let person: Person = from_str_borrowed(styx).unwrap();
125/// assert_eq!(person.name, "Alice");
126/// assert_eq!(person.age, 30);
127/// ```
128pub fn from_str_borrowed<'input, 'facet, T>(
129    input: &'input str,
130) -> Result<T, DeserializeError<StyxError>>
131where
132    T: facet_core::Facet<'facet>,
133    'input: 'facet,
134{
135    use facet_format::FormatDeserializer;
136    let parser = StyxParser::new(input);
137    let mut de = FormatDeserializer::new(parser);
138    de.deserialize_root()
139}
140
141/// Deserialize a single value from a Styx expression string.
142///
143/// Unlike `from_str`, this parses a single value rather than an implicit root object.
144/// Use this for parsing embedded values like default values in schemas.
145///
146/// # Example
147///
148/// ```
149/// use facet::Facet;
150/// use facet_styx::from_str_expr;
151///
152/// // Parse an object expression (note the braces)
153/// #[derive(Facet, Debug, PartialEq)]
154/// struct Point { x: i32, y: i32 }
155///
156/// let point: Point = from_str_expr("{x 10, y 20}").unwrap();
157/// assert_eq!(point.x, 10);
158/// assert_eq!(point.y, 20);
159///
160/// // Parse a scalar expression
161/// let num: i32 = from_str_expr("42").unwrap();
162/// assert_eq!(num, 42);
163/// ```
164pub fn from_str_expr<T>(input: &str) -> Result<T, DeserializeError<StyxError>>
165where
166    T: facet_core::Facet<'static>,
167{
168    use facet_format::FormatDeserializer;
169    let parser = StyxParser::new_expr(input);
170    let mut de = FormatDeserializer::new_owned(parser);
171    de.deserialize_root()
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use facet::Facet;
178    use facet_testhelpers::test;
179
180    #[derive(Facet, Debug, PartialEq)]
181    struct Simple {
182        name: String,
183        value: i32,
184    }
185
186    #[derive(Facet, Debug, PartialEq)]
187    struct WithOptional {
188        required: String,
189        optional: Option<i32>,
190    }
191
192    #[derive(Facet, Debug, PartialEq)]
193    struct Nested {
194        inner: Simple,
195    }
196
197    #[test]
198    fn test_simple_struct() {
199        let input = "name hello\nvalue 42";
200        let result: Simple = from_str(input).unwrap();
201        assert_eq!(result.name, "hello");
202        assert_eq!(result.value, 42);
203    }
204
205    #[test]
206    fn test_quoted_string() {
207        let input = r#"name "hello world"
208value 123"#;
209        let result: Simple = from_str(input).unwrap();
210        assert_eq!(result.name, "hello world");
211        assert_eq!(result.value, 123);
212    }
213
214    #[test]
215    fn test_optional_present() {
216        let input = "required hello\noptional 42";
217        let result: WithOptional = from_str(input).unwrap();
218        assert_eq!(result.required, "hello");
219        assert_eq!(result.optional, Some(42));
220    }
221
222    #[test]
223    fn test_optional_absent() {
224        let input = "required hello";
225        let result: WithOptional = from_str(input).unwrap();
226        assert_eq!(result.required, "hello");
227        assert_eq!(result.optional, None);
228    }
229
230    #[test]
231    fn test_bool_values() {
232        #[derive(Facet, Debug, PartialEq)]
233        struct Flags {
234            enabled: bool,
235            debug: bool,
236        }
237
238        let input = "enabled true\ndebug false";
239        let result: Flags = from_str(input).unwrap();
240        assert!(result.enabled);
241        assert!(!result.debug);
242    }
243
244    #[test]
245    fn test_vec() {
246        #[derive(Facet, Debug, PartialEq)]
247        struct WithVec {
248            items: Vec<i32>,
249        }
250
251        let input = "items (1 2 3)";
252        let result: WithVec = from_str(input).unwrap();
253        assert_eq!(result.items, vec![1, 2, 3]);
254    }
255
256    #[test]
257    fn test_schema_directive_skipped() {
258        // @schema directive should be skipped during deserialization
259        // See: https://github.com/bearcove/styx/issues/3
260        #[derive(Facet, Debug, PartialEq)]
261        struct Config {
262            name: String,
263            port: u16,
264        }
265
266        let input = r#"@schema {source crate:test@1, cli test}
267
268name myapp
269port 8080"#;
270        let result: Config = from_str(input).unwrap();
271        assert_eq!(result.name, "myapp");
272        assert_eq!(result.port, 8080);
273    }
274
275    // =========================================================================
276    // Expression mode tests
277    // =========================================================================
278
279    #[test]
280    fn test_from_str_expr_scalar() {
281        let num: i32 = from_str_expr("42").unwrap();
282        assert_eq!(num, 42);
283
284        let s: String = from_str_expr("hello").unwrap();
285        assert_eq!(s, "hello");
286
287        let b: bool = from_str_expr("true").unwrap();
288        assert!(b);
289    }
290
291    #[test]
292    fn test_from_str_expr_object() {
293        #[derive(Facet, Debug, PartialEq)]
294        struct Point {
295            x: i32,
296            y: i32,
297        }
298
299        let point: Point = from_str_expr("{x 10, y 20}").unwrap();
300        assert_eq!(point.x, 10);
301        assert_eq!(point.y, 20);
302    }
303
304    #[test]
305    fn test_from_str_expr_sequence() {
306        let items: Vec<i32> = from_str_expr("(1 2 3)").unwrap();
307        assert_eq!(items, vec![1, 2, 3]);
308    }
309
310    #[test]
311    fn test_expr_roundtrip() {
312        // Serialize with expr mode, deserialize with expr mode
313        #[derive(Facet, Debug, PartialEq)]
314        struct Config {
315            name: String,
316            port: u16,
317        }
318
319        let original = Config {
320            name: "test".into(),
321            port: 8080,
322        };
323
324        // Serialize as expression (with braces)
325        let serialized = to_string_compact(&original).unwrap();
326        assert!(serialized.starts_with('{'));
327
328        // Parse back as expression
329        let parsed: Config = from_str_expr(&serialized).unwrap();
330        assert_eq!(original, parsed);
331    }
332
333    // =========================================================================
334    // Documented<T> tests
335    // =========================================================================
336
337    #[test]
338    fn test_documented_basic() {
339        // Documented<T> should have the metadata_container flag
340        let shape = <Documented<String>>::SHAPE;
341        assert!(shape.is_metadata_container());
342    }
343
344    #[test]
345    fn test_documented_helper_methods() {
346        let doc = Documented::new(42);
347        assert_eq!(*doc.value(), 42);
348        assert!(doc.doc().is_none());
349
350        let doc = Documented::with_doc(42, vec!["The answer".into()]);
351        assert_eq!(*doc.value(), 42);
352        assert_eq!(doc.doc(), Some(&["The answer".to_string()][..]));
353
354        let doc = Documented::with_doc_line(42, "The answer");
355        assert_eq!(doc.doc(), Some(&["The answer".to_string()][..]));
356    }
357
358    #[test]
359    fn test_documented_deref() {
360        let doc = Documented::new("hello".to_string());
361        // Deref should give us access to the inner value
362        assert_eq!(doc.len(), 5);
363        assert!(doc.starts_with("hel"));
364    }
365
366    #[test]
367    fn test_documented_from() {
368        let doc: Documented<i32> = 42.into();
369        assert_eq!(*doc.value(), 42);
370        assert!(doc.doc().is_none());
371    }
372
373    #[test]
374    fn test_documented_map() {
375        let doc = Documented::with_doc_line(42, "The answer");
376        let mapped = doc.map(|x| x.to_string());
377        assert_eq!(*mapped.value(), "42");
378        assert_eq!(mapped.doc(), Some(&["The answer".to_string()][..]));
379    }
380
381    #[test]
382    fn test_unit_field_followed_by_another_field() {
383        // When a field has unit value (no explicit value), followed by
384        // another field on the next line, both should be parsed correctly.
385        use std::collections::HashMap;
386
387        #[derive(Facet, Debug, PartialEq)]
388        struct Fields {
389            #[facet(flatten)]
390            fields: HashMap<String, Option<String>>,
391        }
392
393        let input = "foo\nbar baz";
394        let result: Fields = from_str(input).unwrap();
395
396        assert_eq!(result.fields.len(), 2);
397        assert_eq!(result.fields.get("foo"), Some(&None));
398        assert_eq!(result.fields.get("bar"), Some(&Some("baz".to_string())));
399    }
400
401    #[test]
402    fn test_map_schema_spacing() {
403        // When serializing a map with a unit-payload tag key (like @string)
404        // followed by another type, there should be proper spacing.
405        // i.e., `@map(@string @enum{...})` NOT `@map(@string@enum{...})`
406        use crate::schema_types::{Documented, EnumSchema, MapSchema, Schema};
407        use std::collections::HashMap;
408
409        let mut enum_variants = HashMap::new();
410        enum_variants.insert(Documented::new("a".to_string()), Schema::Unit);
411        enum_variants.insert(Documented::new("b".to_string()), Schema::Unit);
412
413        let map_schema = Schema::Map(MapSchema(vec![
414            Documented::new(Schema::String(None)), // Key type: @string (no payload)
415            Documented::new(Schema::Enum(EnumSchema(enum_variants))), // Value type: @enum{...}
416        ]));
417
418        let output = to_string(&map_schema).unwrap();
419
420        // Check that there's a space between @string and @enum
421        assert!(
422            output.contains("@string @enum"),
423            "Expected space between @string and @enum, got: {}",
424            output
425        );
426    }
427}