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}