facet_styx/
schema_types.rs

1//! Schema type definitions derived from the meta-schema.
2//!
3//! These types are deserialized from STYX schema files using facet-styx.
4
5use std::collections::HashMap;
6
7use facet::Facet;
8
9/// A complete schema file.
10#[derive(Facet, Debug, Clone)]
11pub struct SchemaFile {
12    /// Schema metadata (required).
13    pub meta: Meta,
14    /// External schema imports (optional).
15    /// Maps namespace prefixes to external schema locations.
16    #[facet(skip_serializing_if = Option::is_none)]
17    pub imports: Option<HashMap<String, String>>,
18    /// Type definitions.
19    /// Keys are type names, or `None` for the document root (serialized as `@`).
20    pub schema: HashMap<Option<String>, Schema>,
21}
22
23/// Schema metadata.
24#[derive(Facet, Debug, Clone)]
25pub struct Meta {
26    /// Unique identifier for the schema (e.g., `crate:myapp-config@1`).
27    pub id: String,
28    /// Schema version (semver).
29    #[facet(skip_serializing_if = Option::is_none)]
30    pub version: Option<String>,
31    /// CLI binary name for schema discovery.
32    #[facet(skip_serializing_if = Option::is_none)]
33    pub cli: Option<String>,
34    /// Human-readable description.
35    #[facet(skip_serializing_if = Option::is_none)]
36    pub description: Option<String>,
37    /// LSP extension configuration.
38    #[facet(skip_serializing_if = Option::is_none)]
39    pub lsp: Option<LspExtensionConfig>,
40}
41
42/// Configuration for LSP extensions.
43#[derive(Facet, Debug, Clone)]
44pub struct LspExtensionConfig {
45    /// Command to launch the extension: (command arg1 arg2 ...)
46    /// e.g., (dibs lsp-extension --stdio)
47    pub launch: Vec<String>,
48    /// Capabilities the extension supports (optional, discovered at runtime if omitted).
49    #[facet(skip_serializing_if = Option::is_none)]
50    pub capabilities: Option<Vec<String>>,
51}
52
53/// A type constraint (corresponds to Schema @enum{...} in the meta-schema).
54///
55/// This is a tagged enum - each variant corresponds to a STYX tag like
56/// `@string`, `@int`, `@object`, `@seq`, etc.
57#[derive(Facet, Debug, Clone)]
58#[facet(rename_all = "lowercase")]
59#[repr(u8)]
60pub enum Schema {
61    // =========================================================================
62    // Built-in scalar types with optional constraints
63    // =========================================================================
64    /// String type: @string or @string{minLen, maxLen, pattern}
65    String(Option<StringConstraints>),
66
67    /// Integer type: @int or @int{min, max}
68    Int(Option<IntConstraints>),
69
70    /// Float type: @float or @float{min, max}
71    Float(Option<FloatConstraints>),
72
73    /// Boolean type: @bool (no constraints)
74    Bool,
75
76    /// Unit type: @unit (the value must be unit `@`)
77    Unit,
78
79    /// Any type: @any (accepts any value)
80    Any,
81
82    // =========================================================================
83    // Structural types
84    // =========================================================================
85    /// Object schema: @object{field @type, @ @type}
86    Object(ObjectSchema),
87
88    /// Sequence schema: @seq(@type)
89    Seq(SeqSchema),
90
91    /// Tuple schema: @tuple(@A @B @C ...)
92    /// Each position has a distinct type.
93    Tuple(TupleSchema),
94
95    /// Map schema: @map(@V) or @map(@K @V)
96    Map(MapSchema),
97
98    // =========================================================================
99    // Combinators
100    // =========================================================================
101    /// Union: @union(@A @B ...)
102    Union(UnionSchema),
103
104    /// Optional: @optional(@T)
105    Optional(OptionalSchema),
106
107    /// Enum: @enum{variant @type, ...}
108    Enum(EnumSchema),
109
110    /// Value constraint: @one-of(@type value1 value2 ...)
111    /// Constrains values to a finite set. First element is base type, rest are allowed values.
112    #[facet(rename = "one-of")]
113    OneOf(OneOfSchema),
114
115    /// Flatten: @flatten(@Type) - inline fields from another type
116    Flatten(FlattenSchema),
117
118    // =========================================================================
119    // Wrappers / modifiers
120    // =========================================================================
121    /// Default value: @default(value @type)
122    Default(DefaultSchema),
123
124    /// Deprecated field: @deprecated("reason" @type)
125    Deprecated(DeprecatedSchema),
126
127    // =========================================================================
128    // Other
129    // =========================================================================
130    /// Literal value constraint (must match exactly)
131    Literal(String),
132
133    /// User-defined type reference (fallback for unknown tags)
134    /// e.g., @MyCustomType becomes Type { name: "MyCustomType" }
135    #[facet(other)]
136    Type {
137        #[facet(tag)]
138        name: Option<String>,
139    },
140}
141
142// =============================================================================
143// Constraint types
144// =============================================================================
145
146/// Constraints for @string type.
147#[derive(Facet, Debug, Clone, Default)]
148#[facet(rename_all = "camelCase")]
149pub struct StringConstraints {
150    /// Minimum length (inclusive).
151    pub min_len: Option<usize>,
152    /// Maximum length (inclusive).
153    pub max_len: Option<usize>,
154    /// Regex pattern the string must match.
155    pub pattern: Option<String>,
156}
157
158/// Constraints for @int type.
159#[derive(Facet, Debug, Clone, Default)]
160pub struct IntConstraints {
161    /// Minimum value (inclusive).
162    pub min: Option<i128>,
163    /// Maximum value (inclusive).
164    pub max: Option<i128>,
165}
166
167/// Constraints for @float type.
168#[derive(Facet, Debug, Clone, Default)]
169pub struct FloatConstraints {
170    /// Minimum value (inclusive).
171    pub min: Option<f64>,
172    /// Maximum value (inclusive).
173    pub max: Option<f64>,
174}
175
176// =============================================================================
177// Structural schema types
178// =============================================================================
179
180/// A key in an object schema.
181///
182/// Object keys can be:
183/// - Named fields: `name` → value = Some("name"), tag = None
184/// - Type patterns: `@string` → value = None, tag = Some("string")
185/// - Unit catch-all: `@` → value = None, tag = Some("")
186///
187/// This is a metadata container - `tag` is captured from the parser's FieldKey
188/// via `#[facet(metadata = "tag")]`.
189#[derive(Facet, Debug, Clone)]
190#[facet(metadata_container)]
191pub struct ObjectKey {
192    /// The field name for named keys, or None for tag-based keys.
193    pub value: Option<String>,
194    /// The tag name for type patterns (`@string` → "string", `@` → "").
195    #[facet(metadata = "tag")]
196    pub tag: Option<String>,
197}
198
199impl ObjectKey {
200    /// Create a named field key.
201    pub fn named(name: impl Into<String>) -> Self {
202        Self {
203            value: Some(name.into()),
204            tag: None,
205        }
206    }
207
208    /// Create a type pattern key (e.g., `@string`).
209    pub fn typed(tag: impl Into<String>) -> Self {
210        Self {
211            value: None,
212            tag: Some(tag.into()),
213        }
214    }
215
216    /// Create a unit catch-all key (`@`).
217    pub fn unit() -> Self {
218        Self {
219            value: None,
220            tag: Some(String::new()),
221        }
222    }
223
224    /// Returns true if this is a named field (not a tag pattern).
225    pub fn is_named(&self) -> bool {
226        self.value.is_some()
227    }
228
229    /// Returns true if this is a type pattern (e.g., `@string`).
230    pub fn is_typed(&self) -> bool {
231        self.tag.is_some() && !self.tag.as_ref().unwrap().is_empty()
232    }
233
234    /// Returns true if this is the unit catch-all (`@`).
235    pub fn is_unit(&self) -> bool {
236        self.tag.as_ref().is_some_and(|t| t.is_empty())
237    }
238
239    /// Get the field name if this is a named key.
240    pub fn name(&self) -> Option<&str> {
241        self.value.as_deref()
242    }
243
244    /// Get the tag name if this is a type pattern.
245    pub fn tag_name(&self) -> Option<&str> {
246        self.tag.as_deref().filter(|t| !t.is_empty())
247    }
248}
249
250// Hash and Eq based on both value and tag
251impl std::hash::Hash for ObjectKey {
252    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
253        self.value.hash(state);
254        self.tag.hash(state);
255    }
256}
257
258impl PartialEq for ObjectKey {
259    fn eq(&self, other: &Self) -> bool {
260        self.value == other.value && self.tag == other.tag
261    }
262}
263
264impl Eq for ObjectKey {}
265
266/// Object schema: @object{field @Schema, @string @Schema}.
267/// Maps field keys to their type constraints.
268/// Keys can be named fields or type patterns (like `@string` for catch-all).
269/// Keys are wrapped in `Documented<ObjectKey>` to carry field documentation.
270#[derive(Facet, Debug, Clone)]
271#[repr(transparent)]
272pub struct ObjectSchema(pub HashMap<Documented<ObjectKey>, Schema>);
273
274/// Sequence schema: @seq(@Schema).
275/// All elements must match the inner schema.
276#[derive(Facet, Debug, Clone)]
277#[repr(transparent)]
278pub struct SeqSchema(pub (Documented<Box<Schema>>,));
279
280/// Tuple schema: @tuple(@A @B @C ...).
281/// Each position has a distinct type, unlike @seq which is homogeneous.
282#[derive(Facet, Debug, Clone)]
283#[repr(transparent)]
284pub struct TupleSchema(pub Vec<Documented<Schema>>);
285
286/// Map schema: @map(@V) or @map(@K @V).
287/// Vec contains 1 element (value type, key defaults to @string) or 2 elements (key, value).
288#[derive(Facet, Debug, Clone)]
289#[repr(transparent)]
290pub struct MapSchema(pub Vec<Documented<Schema>>);
291
292// =============================================================================
293// Combinator schema types
294// =============================================================================
295
296/// Union schema: @union(@A @B ...).
297/// Value must match one of the listed types.
298#[derive(Facet, Debug, Clone)]
299#[repr(transparent)]
300pub struct UnionSchema(pub Vec<Documented<Schema>>);
301
302/// Optional schema: @optional(@T).
303/// Field can be absent or match the inner type.
304#[derive(Facet, Debug, Clone)]
305#[repr(transparent)]
306pub struct OptionalSchema(pub (Documented<Box<Schema>>,));
307
308/// Enum schema: @enum{variant @Type, variant @object{...}}.
309/// Maps variant names to their payload schemas.
310#[derive(Facet, Debug, Clone)]
311#[repr(transparent)]
312pub struct EnumSchema(pub HashMap<Documented<String>, Schema>);
313
314/// One-of schema: @one-of(@type value1 value2 ...).
315/// Constrains values to a finite set. Tuple is (base_type, allowed_values).
316#[derive(Facet, Debug, Clone)]
317#[repr(transparent)]
318pub struct OneOfSchema(pub (Documented<Box<Schema>>, Vec<RawStyx>));
319
320/// Flatten schema: @flatten(@Type).
321/// Inlines fields from another type into the containing object.
322#[derive(Facet, Debug, Clone)]
323#[repr(transparent)]
324pub struct FlattenSchema(pub (Documented<Box<Schema>>,));
325
326// =============================================================================
327// Wrapper schema types
328// =============================================================================
329
330/// A raw Styx value that serializes without quotes and deserializes any value as a string.
331///
332/// Used for embedding Styx expressions in schemas (e.g., default values).
333#[derive(Debug, Clone, PartialEq, Eq, Facet)]
334pub struct RawStyx(pub String);
335
336impl RawStyx {
337    pub fn new(s: impl Into<String>) -> Self {
338        RawStyx(s.into())
339    }
340
341    pub fn as_str(&self) -> &str {
342        &self.0
343    }
344}
345
346impl std::fmt::Display for RawStyx {
347    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
348        f.write_str(&self.0)
349    }
350}
351
352/// Default value wrapper: @default(value @type).
353/// If the field is missing, use the default value.
354/// Tuple is (default_value, inner_schema).
355#[derive(Facet, Debug, Clone)]
356#[repr(transparent)]
357pub struct DefaultSchema(pub (RawStyx, Documented<Box<Schema>>));
358
359/// Deprecated wrapper: @deprecated("reason" @type).
360/// Marks a field as deprecated; validation warns but doesn't fail.
361/// Tuple is (reason, inner_schema).
362#[derive(Facet, Debug, Clone)]
363#[repr(transparent)]
364pub struct DeprecatedSchema(pub (String, Documented<Box<Schema>>));
365
366// =============================================================================
367// Metadata container types
368// =============================================================================
369
370/// A value with documentation metadata.
371///
372/// This is a metadata container - it serializes transparently as just the value,
373/// but formats that support metadata (like Styx) can emit the doc comments.
374///
375/// # Example
376///
377/// ```ignore
378/// let config = Config {
379///     port: Documented {
380///         value: 8080,
381///         doc: Some(vec!["The port to listen on".into()]),
382///     },
383/// };
384///
385/// // JSON (no metadata support): {"port": 8080}
386/// // Styx (with metadata support):
387/// // /// The port to listen on
388/// // port 8080
389/// ```
390#[derive(Facet, Debug, Clone)]
391#[facet(metadata_container)]
392pub struct Documented<T> {
393    /// The actual value.
394    pub value: T,
395    /// Documentation lines (each line is a separate string).
396    #[facet(metadata = "doc")]
397    pub doc: Option<Vec<String>>,
398}
399
400impl<T> Documented<T> {
401    /// Create a new documented value without any documentation.
402    pub fn new(value: T) -> Self {
403        Self { value, doc: None }
404    }
405
406    /// Create a new documented value with documentation.
407    pub fn with_doc(value: T, doc: Vec<String>) -> Self {
408        Self {
409            value,
410            doc: Some(doc),
411        }
412    }
413
414    /// Create a new documented value with a single line of documentation.
415    pub fn with_doc_line(value: T, line: impl Into<String>) -> Self {
416        Self {
417            value,
418            doc: Some(vec![line.into()]),
419        }
420    }
421
422    /// Get a reference to the inner value.
423    pub fn value(&self) -> &T {
424        &self.value
425    }
426
427    /// Get a mutable reference to the inner value.
428    pub fn value_mut(&mut self) -> &mut T {
429        &mut self.value
430    }
431
432    /// Unwrap into the inner value, discarding documentation.
433    pub fn into_inner(self) -> T {
434        self.value
435    }
436
437    /// Get the documentation lines, if any.
438    pub fn doc(&self) -> Option<&[String]> {
439        self.doc.as_deref()
440    }
441
442    /// Map the inner value to a new type.
443    pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Documented<U> {
444        Documented {
445            value: f(self.value),
446            doc: self.doc,
447        }
448    }
449}
450
451impl<T: Default> Default for Documented<T> {
452    fn default() -> Self {
453        Self {
454            value: T::default(),
455            doc: None,
456        }
457    }
458}
459
460impl<T> std::ops::Deref for Documented<T> {
461    type Target = T;
462
463    fn deref(&self) -> &Self::Target {
464        &self.value
465    }
466}
467
468impl<T> std::ops::DerefMut for Documented<T> {
469    fn deref_mut(&mut self) -> &mut Self::Target {
470        &mut self.value
471    }
472}
473
474impl<T> From<T> for Documented<T> {
475    fn from(value: T) -> Self {
476        Self::new(value)
477    }
478}
479
480// Hash and Eq only consider the value, not the documentation.
481// Documentation is metadata and doesn't affect identity.
482
483impl<T: std::hash::Hash> std::hash::Hash for Documented<T> {
484    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
485        self.value.hash(state);
486    }
487}
488
489impl<T: PartialEq> PartialEq for Documented<T> {
490    fn eq(&self, other: &Self) -> bool {
491        self.value == other.value
492    }
493}
494
495impl<T: Eq> Eq for Documented<T> {}