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 properties {
141 if !required.iter().any(|name| name == property) {
144 continue;
145 }
146 let child_pointer = join(&properties_pointer, property);
147 let value = self.walk(child, &child_pointer)?;
148 skeleton.insert(property.clone(), value);
149 }
150 Ok(Value::Object(skeleton))
151 }
152}
153
154fn required_names(entries: &Map<String, Value>) -> Vec<String> {
156 entries
157 .get("required")
158 .and_then(Value::as_array)
159 .map(|names| {
160 names
161 .iter()
162 .filter_map(|value| value.as_str().map(str::to_owned))
163 .collect()
164 })
165 .unwrap_or_default()
166}
167
168fn join(pointer: &str, segment: &str) -> String {
170 format!("{pointer}/{segment}")
171}
172
173#[cfg(test)]
174mod tests {
175 use std::path::Path;
176
177 use serde_json::json;
178
179 use super::build_input_skeleton;
180 use crate::codegen::error::CodegenError;
181
182 fn skeleton(schema: &serde_json::Value) -> Result<serde_json::Value, CodegenError> {
183 build_input_skeleton(Path::new("schemas/input.json"), schema)
184 }
185
186 #[test]
187 fn required_scalars_get_type_shaped_placeholders() -> Result<(), Box<dyn std::error::Error>> {
188 let schema = json!({
189 "type": "object",
190 "required": ["name", "count", "ratio", "active"],
191 "additionalProperties": false,
192 "properties": {
193 "name": { "type": "string" },
194 "count": { "type": "integer" },
195 "ratio": { "type": "number" },
196 "active": { "type": "boolean" },
197 "note": { "type": "string" }
198 }
199 });
200 let result = skeleton(&schema)?;
201 assert_eq!(
204 result,
205 json!({ "name": "", "count": 0, "ratio": 0.0, "active": false })
206 );
207 Ok(())
208 }
209
210 #[test]
211 fn nested_required_objects_recurse() -> Result<(), Box<dyn std::error::Error>> {
212 let schema = json!({
213 "type": "object",
214 "required": ["workspace"],
215 "properties": {
216 "workspace": {
217 "type": "object",
218 "required": ["path"],
219 "properties": { "path": { "type": "string" } }
220 }
221 }
222 });
223 assert_eq!(skeleton(&schema)?, json!({ "workspace": { "path": "" } }));
224 Ok(())
225 }
226
227 #[test]
228 fn required_enum_uses_the_first_wire_value() -> Result<(), Box<dyn std::error::Error>> {
229 let schema = json!({
230 "type": "object",
231 "required": ["isolation"],
232 "properties": {
233 "isolation": { "type": "string", "enum": ["worktree", "copy", "vm"] }
234 }
235 });
236 assert_eq!(skeleton(&schema)?, json!({ "isolation": "worktree" }));
237 Ok(())
238 }
239
240 #[test]
241 fn required_arrays_are_empty() -> Result<(), Box<dyn std::error::Error>> {
242 let schema = json!({
243 "type": "object",
244 "required": ["tags"],
245 "properties": {
246 "tags": { "type": "array", "items": { "type": "string" } }
247 }
248 });
249 assert_eq!(skeleton(&schema)?, json!({ "tags": [] }));
250 Ok(())
251 }
252
253 #[test]
254 fn unsupported_construct_fails_with_pointer() {
255 let schema = json!({
256 "type": "object",
257 "required": ["w"],
258 "properties": { "w": { "$ref": "#/$defs/w" } }
259 });
260 let result = skeleton(&schema);
261 let Err(CodegenError::UnsupportedConstruct { pointer, .. }) = result else {
262 unreachable!("expected UnsupportedConstruct, got {result:?}");
263 };
264 assert_eq!(pointer, "/properties/w");
265 }
266
267 #[test]
268 fn empty_enum_fails() {
269 let schema = json!({ "type": "string", "enum": [] });
270 assert!(matches!(
271 skeleton(&schema),
272 Err(CodegenError::UnsupportedConstruct { .. })
273 ));
274 }
275}