Skip to main content

simple_agents_healing/
schema.rs

1//! Schema definition system for type validation and coercion.
2//!
3//! Provides a simple yet extensible schema definition system for describing expected types.
4//! Future work will include derive macros for automatic schema generation from Rust types.
5
6use serde::{Deserialize, Serialize};
7
8/// Describes the expected structure and types for parsed JSON.
9///
10/// # Examples
11///
12/// ```
13/// use simple_agents_healing::schema::Schema;
14///
15/// // Simple string schema
16/// let name_schema = Schema::String;
17///
18/// // Integer with range
19/// let age_schema = Schema::Int;
20///
21/// // Object with fields
22/// let person_schema = Schema::object(vec![
23///     ("name".into(), Schema::String, true),
24///     ("age".into(), Schema::Int, true),
25///     ("email".into(), Schema::String, false),  // optional
26/// ]);
27/// ```
28#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
29pub enum Schema {
30    /// String type
31    String,
32    /// Signed integer (i64)
33    Int,
34    /// Unsigned integer (u64)
35    UInt,
36    /// Floating point number (f64)
37    Float,
38    /// Boolean
39    Bool,
40    /// Null value
41    Null,
42    /// Array of elements (homogeneous)
43    Array(Box<Schema>),
44    /// Object with named fields
45    Object(ObjectSchema),
46    /// Union of multiple possible types (tagged or untagged)
47    Union(Vec<Schema>),
48    /// Any valid JSON value (no validation)
49    Any,
50}
51
52/// Schema for an object type with named fields.
53#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
54pub struct ObjectSchema {
55    /// Field definitions
56    pub fields: Vec<Field>,
57    /// Whether to allow additional fields not in schema
58    pub allow_additional_fields: bool,
59}
60
61/// Field definition in an object schema.
62#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
63pub struct Field {
64    /// Field name as it appears in the schema
65    pub name: String,
66    /// Expected type for this field
67    pub schema: Schema,
68    /// Whether this field is required (true) or optional (false)
69    pub required: bool,
70    /// Alternative names this field might have (aliases)
71    pub aliases: Vec<String>,
72    /// Default value if field is missing (JSON string representation)
73    pub default: Option<serde_json::Value>,
74    /// Description of the field (for documentation)
75    pub description: Option<String>,
76    /// Streaming annotation (controls emission timing)
77    #[serde(default)]
78    pub stream_annotation: StreamAnnotation,
79}
80
81/// Streaming annotation for field-level emission control.
82///
83/// Controls when a field value should be emitted during streaming parsing.
84///
85/// # Examples
86///
87/// ```
88/// use simple_agents_healing::schema::{StreamAnnotation, Field, Schema};
89///
90/// // Emit as soon as available (default)
91/// let normal_field = Field {
92///     name: "name".to_string(),
93///     schema: Schema::String,
94///     required: true,
95///     aliases: vec![],
96///     default: None,
97///     description: None,
98///     stream_annotation: StreamAnnotation::Normal,
99/// };
100///
101/// // Don't emit until non-null
102/// let id_field = Field {
103///     name: "id".to_string(),
104///     schema: Schema::Int,
105///     required: true,
106///     aliases: vec![],
107///     default: None,
108///     description: None,
109///     stream_annotation: StreamAnnotation::NotNull,
110/// };
111///
112/// // Only emit when complete
113/// let status_field = Field {
114///     name: "status".to_string(),
115///     schema: Schema::String,
116///     required: true,
117///     aliases: vec![],
118///     default: None,
119///     description: None,
120///     stream_annotation: StreamAnnotation::Done,
121/// };
122/// ```
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
124pub enum StreamAnnotation {
125    /// Emit field as soon as it's available (default)
126    #[default]
127    Normal,
128    /// Don't emit until value is non-null (@@stream.not_null)
129    NotNull,
130    /// Only emit when the entire structure is complete (@@stream.done)
131    Done,
132}
133
134impl Schema {
135    /// Create a simple object schema with fields.
136    ///
137    /// # Arguments
138    ///
139    /// * `fields` - List of (name, schema, required) tuples
140    ///
141    /// # Examples
142    ///
143    /// ```
144    /// use simple_agents_healing::schema::Schema;
145    ///
146    /// let schema = Schema::object(vec![
147    ///     ("name".into(), Schema::String, true),
148    ///     ("age".into(), Schema::Int, true),
149    /// ]);
150    /// ```
151    pub fn object(fields: Vec<(String, Schema, bool)>) -> Self {
152        Schema::Object(ObjectSchema {
153            fields: fields
154                .into_iter()
155                .map(|(name, schema, required)| Field {
156                    name,
157                    schema,
158                    required,
159                    aliases: Vec::new(),
160                    default: None,
161                    description: None,
162                    stream_annotation: StreamAnnotation::Normal,
163                })
164                .collect(),
165            allow_additional_fields: false,
166        })
167    }
168
169    /// Create an array schema with element type.
170    ///
171    /// # Examples
172    ///
173    /// ```
174    /// use simple_agents_healing::schema::Schema;
175    ///
176    /// let string_array = Schema::array(Schema::String);
177    /// let int_array = Schema::array(Schema::Int);
178    /// ```
179    pub fn array(element_schema: Schema) -> Self {
180        Schema::Array(Box::new(element_schema))
181    }
182
183    /// Create a union schema (sum type).
184    ///
185    /// # Examples
186    ///
187    /// ```
188    /// use simple_agents_healing::schema::Schema;
189    ///
190    /// // String or Int
191    /// let schema = Schema::union(vec![Schema::String, Schema::Int]);
192    /// ```
193    pub fn union(variants: Vec<Schema>) -> Self {
194        Schema::Union(variants)
195    }
196
197    /// Check if this schema represents a primitive type.
198    pub fn is_primitive(&self) -> bool {
199        matches!(
200            self,
201            Schema::String
202                | Schema::Int
203                | Schema::UInt
204                | Schema::Float
205                | Schema::Bool
206                | Schema::Null
207        )
208    }
209
210    /// Check if this schema is nullable (includes Null in a union).
211    pub fn is_nullable(&self) -> bool {
212        match self {
213            Schema::Null => true,
214            Schema::Union(variants) => variants.iter().any(|v| v.is_nullable()),
215            _ => false,
216        }
217    }
218
219    /// Get a human-readable type name for error messages.
220    pub fn type_name(&self) -> &'static str {
221        match self {
222            Schema::String => "string",
223            Schema::Int => "int",
224            Schema::UInt => "uint",
225            Schema::Float => "float",
226            Schema::Bool => "bool",
227            Schema::Null => "null",
228            Schema::Array(_) => "array",
229            Schema::Object(_) => "object",
230            Schema::Union(_) => "union",
231            Schema::Any => "any",
232        }
233    }
234}
235
236impl Field {
237    /// Create a new required field.
238    pub fn required(name: impl Into<String>, schema: Schema) -> Self {
239        Field {
240            name: name.into(),
241            schema,
242            required: true,
243            aliases: Vec::new(),
244            default: None,
245            description: None,
246            stream_annotation: StreamAnnotation::Normal,
247        }
248    }
249
250    /// Create a new optional field.
251    pub fn optional(name: impl Into<String>, schema: Schema) -> Self {
252        Field {
253            name: name.into(),
254            schema,
255            required: false,
256            aliases: Vec::new(),
257            default: None,
258            description: None,
259            stream_annotation: StreamAnnotation::Normal,
260        }
261    }
262
263    /// Add an alias to this field.
264    pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
265        self.aliases.push(alias.into());
266        self
267    }
268
269    /// Add a default value for this field.
270    pub fn with_default(mut self, default: serde_json::Value) -> Self {
271        self.default = Some(default);
272        self
273    }
274
275    /// Add a description to this field.
276    pub fn with_description(mut self, description: impl Into<String>) -> Self {
277        self.description = Some(description.into());
278        self
279    }
280
281    /// Set the streaming annotation for this field.
282    ///
283    /// # Examples
284    ///
285    /// ```
286    /// use simple_agents_healing::schema::{Field, Schema, StreamAnnotation};
287    ///
288    /// // Don't emit until non-null
289    /// let id_field = Field::required("id", Schema::Int)
290    ///     .with_stream_annotation(StreamAnnotation::NotNull);
291    ///
292    /// // Only emit when complete
293    /// let status_field = Field::required("status", Schema::String)
294    ///     .with_stream_annotation(StreamAnnotation::Done);
295    /// ```
296    pub fn with_stream_annotation(mut self, annotation: StreamAnnotation) -> Self {
297        self.stream_annotation = annotation;
298        self
299    }
300}
301
302impl ObjectSchema {
303    /// Create a new object schema.
304    pub fn new(fields: Vec<Field>) -> Self {
305        ObjectSchema {
306            fields,
307            allow_additional_fields: false,
308        }
309    }
310
311    /// Allow additional fields beyond those defined in the schema.
312    pub fn allow_additional(mut self) -> Self {
313        self.allow_additional_fields = true;
314        self
315    }
316
317    /// Find a field by name (exact match).
318    pub fn get_field(&self, name: &str) -> Option<&Field> {
319        self.fields.iter().find(|f| f.name == name)
320    }
321
322    /// Get all field names (including aliases).
323    pub fn all_field_names(&self) -> Vec<String> {
324        let mut names = Vec::new();
325        for field in &self.fields {
326            names.push(field.name.clone());
327            names.extend(field.aliases.clone());
328        }
329        names
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    #[test]
338    fn test_simple_schemas() {
339        assert!(Schema::String.is_primitive());
340        assert!(Schema::Int.is_primitive());
341        assert!(!Schema::array(Schema::String).is_primitive());
342    }
343
344    #[test]
345    fn test_object_schema_creation() {
346        let schema = Schema::object(vec![
347            ("name".into(), Schema::String, true),
348            ("age".into(), Schema::Int, false),
349        ]);
350
351        if let Schema::Object(obj) = schema {
352            assert_eq!(obj.fields.len(), 2);
353            assert_eq!(obj.fields[0].name, "name");
354            assert!(obj.fields[0].required);
355            assert_eq!(obj.fields[1].name, "age");
356            assert!(!obj.fields[1].required);
357        } else {
358            panic!("Expected Object schema");
359        }
360    }
361
362    #[test]
363    fn test_field_builder() {
364        let field = Field::required("username", Schema::String)
365            .with_alias("user_name")
366            .with_description("The user's login name");
367
368        assert_eq!(field.name, "username");
369        assert!(field.required);
370        assert_eq!(field.aliases, vec!["user_name"]);
371        assert!(field.description.is_some());
372    }
373
374    #[test]
375    fn test_nullable_schema() {
376        assert!(Schema::Null.is_nullable());
377        assert!(!Schema::String.is_nullable());
378        assert!(Schema::union(vec![Schema::String, Schema::Null]).is_nullable());
379    }
380
381    #[test]
382    fn test_type_names() {
383        assert_eq!(Schema::String.type_name(), "string");
384        assert_eq!(Schema::Int.type_name(), "int");
385        assert_eq!(Schema::array(Schema::Bool).type_name(), "array");
386        assert_eq!(
387            Schema::object(vec![("x".into(), Schema::Float, true)]).type_name(),
388            "object"
389        );
390    }
391}