1use crate::Provider;
7use serde::{Deserialize, Serialize};
8use serde_json::{Map, Value};
9
10#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
12#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum SchemaFormat {
15 #[default]
16 MeerkatV1,
17}
18
19#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
21#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "snake_case")]
23pub enum SchemaCompat {
24 #[default]
25 Lossy,
26 Strict,
27}
28
29#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "snake_case")]
33pub struct SchemaWarning {
34 pub provider: Provider,
35 pub path: String,
36 pub message: String,
37}
38
39#[derive(Debug, thiserror::Error)]
41pub enum SchemaError {
42 #[error("Schema must be a JSON object at the root")]
43 InvalidRoot,
44 #[error("Schema contains unsupported features for {provider:?}: {warnings:?}")]
45 UnsupportedFeatures {
46 provider: Provider,
47 warnings: Vec<SchemaWarning>,
48 },
49}
50
51#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54#[serde(transparent)]
55pub struct MeerkatSchema(Value);
56
57impl MeerkatSchema {
58 pub fn new(schema: Value) -> Result<Self, SchemaError> {
60 if !schema.is_object() {
61 return Err(SchemaError::InvalidRoot);
62 }
63 let mut normalized = schema;
64 normalize_schema(&mut normalized);
65 Ok(Self(normalized))
66 }
67
68 pub fn as_value(&self) -> &Value {
70 &self.0
71 }
72}
73
74#[derive(Debug, Clone)]
76pub struct CompiledSchema {
77 pub schema: Value,
78 pub warnings: Vec<SchemaWarning>,
79}
80
81fn normalize_schema(value: &mut Value) {
82 match value {
83 Value::Object(obj) => {
84 let is_object_type = match obj.get("type") {
85 Some(Value::String(t)) => t == "object",
86 Some(Value::Array(types)) => types.iter().any(|t| t.as_str() == Some("object")),
87 _ => obj.contains_key("properties") || obj.contains_key("required"),
88 };
89
90 if is_object_type {
91 obj.entry("properties".to_string())
92 .or_insert_with(|| Value::Object(Map::new()));
93 obj.entry("required".to_string())
94 .or_insert_with(|| Value::Array(Vec::new()));
95 }
96
97 for value in obj.values_mut() {
98 normalize_schema(value);
99 }
100 }
101 Value::Array(items) => {
102 for item in items.iter_mut() {
103 normalize_schema(item);
104 }
105 }
106 _ => {}
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::MeerkatSchema;
113 use serde_json::json;
114
115 #[test]
116 fn test_normalize_adds_properties_and_required() -> Result<(), Box<dyn std::error::Error>> {
117 let schema = json!({"type": "object"});
118 let schema = MeerkatSchema::new(schema)?;
119 assert!(schema.as_value().get("properties").is_some());
120 assert!(schema.as_value().get("required").is_some());
121 Ok(())
122 }
123
124 #[test]
125 fn test_invalid_root_rejected() {
126 assert!(MeerkatSchema::new(json!("string")).is_err());
127 assert!(MeerkatSchema::new(json!(42)).is_err());
128 }
129
130 #[test]
131 fn test_normalize_recurses_nested_objects() -> Result<(), Box<dyn std::error::Error>> {
132 let schema = json!({
133 "type": "object",
134 "properties": {
135 "profile": {
136 "type": "object",
137 "properties": {
138 "city": {"type": "string"}
139 }
140 },
141 "items": {
142 "type": "array",
143 "items": {
144 "type": "object"
145 }
146 },
147 "variant": {
148 "anyOf": [
149 {"type": "object"},
150 {"type": "string"}
151 ]
152 }
153 }
154 });
155
156 let schema = MeerkatSchema::new(schema)?;
157 let root = schema.as_value();
158
159 assert!(root.get("required").is_some());
160 assert!(root["properties"]["profile"].get("required").is_some());
161 assert!(
162 root["properties"]["items"]["items"]
163 .get("properties")
164 .is_some()
165 );
166 assert!(
167 root["properties"]["variant"]["anyOf"][0]
168 .get("required")
169 .is_some()
170 );
171 Ok(())
172 }
173
174 #[test]
175 fn test_normalize_preserves_existing_object_shape() -> Result<(), Box<dyn std::error::Error>> {
176 let schema = json!({
177 "type": "object",
178 "properties": {
179 "name": {"type": "string"}
180 },
181 "required": ["name"]
182 });
183
184 let schema = MeerkatSchema::new(schema)?;
185 let root = schema.as_value();
186
187 assert_eq!(root["required"], json!(["name"]));
188 assert_eq!(root["properties"]["name"]["type"], "string");
189 Ok(())
190 }
191}