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> {}