Skip to main content

rdx_schema/
lib.rs

1use std::collections::HashMap;
2
3use rdx_ast::AttributeValue;
4use serde::{Deserialize, Serialize};
5
6mod validate;
7pub use validate::{Diagnostic, Severity, validate};
8
9/// A schema registry mapping component names to their definitions.
10///
11/// # Example
12///
13/// ```rust
14/// use rdx_schema::{Schema, ComponentSchema, PropSchema, PropType};
15///
16/// let schema = Schema::new()
17///     .component("Notice", ComponentSchema::new()
18///         .prop("type", PropSchema::required(PropType::String))
19///         .prop("title", PropSchema::optional(PropType::String))
20///     )
21///     .component("Badge", ComponentSchema::new()
22///         .prop("label", PropSchema::required(PropType::String))
23///         .prop("color", PropSchema::optional(PropType::String))
24///     );
25/// ```
26#[derive(Debug, Clone, Default, Serialize, Deserialize)]
27pub struct Schema {
28    /// Component definitions keyed by tag name.
29    pub components: HashMap<String, ComponentSchema>,
30    /// When true, components not in the schema produce an error.
31    /// When false (default), unknown components are silently accepted.
32    #[serde(default)]
33    pub strict: bool,
34}
35
36impl Schema {
37    pub fn new() -> Self {
38        Self::default()
39    }
40
41    /// Register a component definition. Builder pattern.
42    pub fn component(mut self, name: &str, schema: ComponentSchema) -> Self {
43        self.components.insert(name.to_string(), schema);
44        self
45    }
46
47    /// Enable strict mode: unknown components produce errors.
48    pub fn strict(mut self, strict: bool) -> Self {
49        self.strict = strict;
50        self
51    }
52}
53
54/// Schema definition for a single component.
55#[derive(Debug, Clone, Default, Serialize, Deserialize)]
56pub struct ComponentSchema {
57    /// Allowed props keyed by attribute name.
58    pub props: HashMap<String, PropSchema>,
59    /// Whether the component must be self-closing (no children).
60    #[serde(default)]
61    pub self_closing: bool,
62    /// Optional list of allowed child component names.
63    /// If `None`, any children are allowed. If `Some`, only listed component names
64    /// (and standard Markdown nodes) are permitted as direct children.
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub allowed_children: Option<Vec<String>>,
67    /// Human-readable description for tooling and error messages.
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub description: Option<String>,
70}
71
72impl ComponentSchema {
73    pub fn new() -> Self {
74        Self::default()
75    }
76
77    /// Add a prop definition. Builder pattern.
78    pub fn prop(mut self, name: &str, schema: PropSchema) -> Self {
79        self.props.insert(name.to_string(), schema);
80        self
81    }
82
83    /// Mark as self-closing (must not have children).
84    pub fn self_closing(mut self, val: bool) -> Self {
85        self.self_closing = val;
86        self
87    }
88
89    /// Restrict allowed child component names.
90    pub fn allowed_children(mut self, names: Vec<&str>) -> Self {
91        self.allowed_children = Some(names.into_iter().map(|s| s.to_string()).collect());
92        self
93    }
94
95    /// Set a description.
96    pub fn description(mut self, desc: &str) -> Self {
97        self.description = Some(desc.to_string());
98        self
99    }
100}
101
102/// Schema definition for a single prop.
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct PropSchema {
105    /// The expected type.
106    #[serde(rename = "type")]
107    pub prop_type: PropType,
108    /// Whether the prop is required.
109    #[serde(default)]
110    pub required: bool,
111    /// Optional default value (informational; not applied by the validator).
112    #[serde(default, skip_serializing_if = "Option::is_none")]
113    pub default: Option<serde_json::Value>,
114    /// For `PropType::Enum`, the allowed string values.
115    #[serde(default, skip_serializing_if = "Option::is_none")]
116    pub values: Option<Vec<String>>,
117    /// Human-readable description.
118    #[serde(default, skip_serializing_if = "Option::is_none")]
119    pub description: Option<String>,
120}
121
122impl PropSchema {
123    /// A required prop of the given type.
124    pub fn required(prop_type: PropType) -> Self {
125        PropSchema {
126            prop_type,
127            required: true,
128            default: None,
129            values: None,
130            description: None,
131        }
132    }
133
134    /// An optional prop of the given type.
135    pub fn optional(prop_type: PropType) -> Self {
136        PropSchema {
137            prop_type,
138            required: false,
139            default: None,
140            values: None,
141            description: None,
142        }
143    }
144
145    /// A required enum prop restricted to specific string values.
146    pub fn enum_required(values: Vec<&str>) -> Self {
147        PropSchema {
148            prop_type: PropType::Enum,
149            required: true,
150            default: None,
151            values: Some(values.into_iter().map(|s| s.to_string()).collect()),
152            description: None,
153        }
154    }
155
156    /// An optional enum prop.
157    pub fn enum_optional(values: Vec<&str>) -> Self {
158        PropSchema {
159            prop_type: PropType::Enum,
160            required: false,
161            default: None,
162            values: Some(values.into_iter().map(|s| s.to_string()).collect()),
163            description: None,
164        }
165    }
166
167    /// Set a default value (informational).
168    pub fn with_default(mut self, val: serde_json::Value) -> Self {
169        self.default = Some(val);
170        self
171    }
172
173    /// Set a description.
174    pub fn with_description(mut self, desc: &str) -> Self {
175        self.description = Some(desc.to_string());
176        self
177    }
178}
179
180/// The expected type of a prop value.
181#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
182#[serde(rename_all = "lowercase")]
183pub enum PropType {
184    /// Any string value.
185    String,
186    /// A numeric value (integer or float).
187    Number,
188    /// A boolean value.
189    Boolean,
190    /// A restricted set of string values (see `PropSchema::values`).
191    Enum,
192    /// A JSON object (`{{ }}`).
193    Object,
194    /// A JSON array (`{{ }}`).
195    Array,
196    /// A context variable (`{$path}`).
197    Variable,
198    /// Accepts any attribute value type.
199    Any,
200}
201
202/// Check whether an `AttributeValue` matches the expected `PropType`.
203pub(crate) fn type_matches(value: &AttributeValue, expected: &PropType) -> bool {
204    match expected {
205        PropType::Any => true,
206        PropType::String => matches!(value, AttributeValue::String(_)),
207        PropType::Number => matches!(value, AttributeValue::Number(_)),
208        PropType::Boolean => matches!(value, AttributeValue::Bool(_)),
209        PropType::Object => matches!(value, AttributeValue::Object(_)),
210        PropType::Array => matches!(value, AttributeValue::Array(_)),
211        PropType::Variable => matches!(value, AttributeValue::Variable(_)),
212        PropType::Enum => matches!(value, AttributeValue::String(_)),
213    }
214}
215
216/// Get a human-readable name for an attribute value's type.
217pub(crate) fn value_type_name(value: &AttributeValue) -> &'static str {
218    match value {
219        AttributeValue::Null => "null",
220        AttributeValue::Bool(_) => "boolean",
221        AttributeValue::Number(_) => "number",
222        AttributeValue::String(_) => "string",
223        AttributeValue::Array(_) => "array",
224        AttributeValue::Object(_) => "object",
225        AttributeValue::Variable(_) => "variable",
226    }
227}