1use serde_json::{Map, Value};
26
27use super::error::CodegenError;
28
29pub fn build_input_skeleton(
43 schema_file: &std::path::Path,
44 schema: &Value,
45) -> Result<Value, CodegenError> {
46 let builder = SkeletonBuilder { schema_file };
47 builder.walk(schema, "")
48}
49
50struct SkeletonBuilder<'a> {
51 schema_file: &'a std::path::Path,
52}
53
54impl SkeletonBuilder<'_> {
55 fn unsupported(&self, pointer: String, construct: String) -> CodegenError {
56 CodegenError::UnsupportedConstruct {
57 file: self.schema_file.to_path_buf(),
58 pointer,
59 construct,
60 }
61 }
62
63 fn walk(&self, node: &Value, pointer: &str) -> Result<Value, CodegenError> {
64 let Value::Object(entries) = node else {
65 return Err(self.unsupported(
66 pointer.to_owned(),
67 "schema must be a JSON object".to_owned(),
68 ));
69 };
70
71 if let Some(values) = entries.get("enum") {
75 return self.walk_enum(values, pointer);
76 }
77
78 let Some(declared) = entries.get("type").and_then(Value::as_str) else {
79 return Err(self.unsupported(
80 pointer.to_owned(),
81 "schema has neither a single-string `type` nor `enum`".to_owned(),
82 ));
83 };
84
85 match declared {
86 "string" => Ok(Value::String(String::new())),
87 "integer" => Ok(Value::Number(0.into())),
88 "number" => Ok(serde_json::json!(0.0)),
89 "boolean" => Ok(Value::Bool(false)),
90 "array" => Ok(Value::Array(Vec::new())),
94 "object" => self.walk_object(entries, pointer),
95 other => Err(self.unsupported(
96 join(pointer, "type"),
97 format!("unsupported `type` value `{other}`"),
98 )),
99 }
100 }
101
102 fn walk_enum(&self, values: &Value, pointer: &str) -> Result<Value, CodegenError> {
103 let enum_pointer = join(pointer, "enum");
104 let Value::Array(values) = values else {
105 return Err(self.unsupported(enum_pointer, "`enum` must be an array".to_owned()));
106 };
107 let Some(first) = values.first() else {
108 return Err(self.unsupported(enum_pointer, "`enum` must not be empty".to_owned()));
109 };
110 let Some(wire) = first.as_str() else {
111 return Err(self.unsupported(
112 enum_pointer,
113 "only string `enum` values are supported".to_owned(),
114 ));
115 };
116 Ok(Value::String(wire.to_owned()))
117 }
118
119 fn walk_object(
120 &self,
121 entries: &Map<String, Value>,
122 pointer: &str,
123 ) -> Result<Value, CodegenError> {
124 let Some(properties) = entries.get("properties") else {
125 return Err(self.unsupported(
126 pointer.to_owned(),
127 "`object` schema without `properties`".to_owned(),
128 ));
129 };
130 let Value::Object(properties) = properties else {
131 return Err(self.unsupported(
132 join(pointer, "properties"),
133 "`properties` must be an object".to_owned(),
134 ));
135 };
136 let required = required_names(entries);
137
138 let properties_pointer = join(pointer, "properties");
139 let mut skeleton = Map::new();
140 for (property, child) in crate::canonical::sorted_entries(properties) {
144 if !required.iter().any(|name| name == property) {
147 continue;
148 }
149 let child_pointer = join(&properties_pointer, property);
150 let value = self.walk(child, &child_pointer)?;
151 skeleton.insert(property.clone(), value);
152 }
153 Ok(Value::Object(skeleton))
154 }
155}
156
157fn required_names(entries: &Map<String, Value>) -> Vec<String> {
159 entries
160 .get("required")
161 .and_then(Value::as_array)
162 .map(|names| {
163 names
164 .iter()
165 .filter_map(|value| value.as_str().map(str::to_owned))
166 .collect()
167 })
168 .unwrap_or_default()
169}
170
171fn join(pointer: &str, segment: &str) -> String {
173 format!("{pointer}/{segment}")
174}
175
176#[cfg(test)]
177mod tests {
178 use std::path::Path;
179
180 use serde_json::json;
181
182 use super::build_input_skeleton;
183 use crate::codegen::error::CodegenError;
184
185 fn skeleton(schema: &serde_json::Value) -> Result<serde_json::Value, CodegenError> {
186 build_input_skeleton(Path::new("schemas/input.json"), schema)
187 }
188
189 #[test]
190 fn required_scalars_get_type_shaped_placeholders() -> Result<(), Box<dyn std::error::Error>> {
191 let schema = json!({
192 "type": "object",
193 "required": ["name", "count", "ratio", "active"],
194 "additionalProperties": false,
195 "properties": {
196 "name": { "type": "string" },
197 "count": { "type": "integer" },
198 "ratio": { "type": "number" },
199 "active": { "type": "boolean" },
200 "note": { "type": "string" }
201 }
202 });
203 let result = skeleton(&schema)?;
204 assert_eq!(
207 result,
208 json!({ "name": "", "count": 0, "ratio": 0.0, "active": false })
209 );
210 Ok(())
211 }
212
213 #[test]
214 fn nested_required_objects_recurse() -> Result<(), Box<dyn std::error::Error>> {
215 let schema = json!({
216 "type": "object",
217 "required": ["workspace"],
218 "properties": {
219 "workspace": {
220 "type": "object",
221 "required": ["path"],
222 "properties": { "path": { "type": "string" } }
223 }
224 }
225 });
226 assert_eq!(skeleton(&schema)?, json!({ "workspace": { "path": "" } }));
227 Ok(())
228 }
229
230 #[test]
231 fn required_enum_uses_the_first_wire_value() -> Result<(), Box<dyn std::error::Error>> {
232 let schema = json!({
233 "type": "object",
234 "required": ["isolation"],
235 "properties": {
236 "isolation": { "type": "string", "enum": ["worktree", "copy", "vm"] }
237 }
238 });
239 assert_eq!(skeleton(&schema)?, json!({ "isolation": "worktree" }));
240 Ok(())
241 }
242
243 #[test]
244 fn required_arrays_are_empty() -> Result<(), Box<dyn std::error::Error>> {
245 let schema = json!({
246 "type": "object",
247 "required": ["tags"],
248 "properties": {
249 "tags": { "type": "array", "items": { "type": "string" } }
250 }
251 });
252 assert_eq!(skeleton(&schema)?, json!({ "tags": [] }));
253 Ok(())
254 }
255
256 #[test]
257 fn unsupported_construct_fails_with_pointer() {
258 let schema = json!({
259 "type": "object",
260 "required": ["w"],
261 "properties": { "w": { "$ref": "#/$defs/w" } }
262 });
263 let result = skeleton(&schema);
264 let Err(CodegenError::UnsupportedConstruct { pointer, .. }) = result else {
265 unreachable!("expected UnsupportedConstruct, got {result:?}");
266 };
267 assert_eq!(pointer, "/properties/w");
268 }
269
270 #[test]
271 fn empty_enum_fails() {
272 let schema = json!({ "type": "string", "enum": [] });
273 assert!(matches!(
274 skeleton(&schema),
275 Err(CodegenError::UnsupportedConstruct { .. })
276 ));
277 }
278}