1use std::{cell::Cell, collections::BTreeMap};
2
3use error::{SchemaError, SchemaResult};
4use inquire::{Confirm, CustomType, Select, Text};
5use log::debug;
6use schemars::schema::{
7 ArrayValidation, InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec,
8 SubschemaValidation,
9};
10use serde_json::{json, Map, Value};
11use undo::clear_lines;
12
13use crate::undo::{RecurseIter, RecurseLoop, Undo};
14
15pub mod error;
16pub mod traits;
17pub mod undo;
18
19pub use traits::*;
20
21pub(crate) fn parse_schema(
22 definitions: &BTreeMap<String, Schema>,
23 title: Option<String>,
24 name: String,
25 schema: SchemaObject,
26 current_depth: &Cell<u16>,
27) -> SchemaResult<Value> {
28 let depth_checkpoint = current_depth.get();
29 match parse_schema_inner(
30 definitions,
31 title.clone(),
32 name.clone(),
33 schema.clone(),
34 current_depth,
35 ) {
36 Ok(value) => Ok(value),
37 Err(SchemaError::Undo { depth }) => {
38 if depth <= depth_checkpoint && depth_checkpoint != 0 {
39 debug!("forwarding error in parse schema, depth: {depth}, depth_checkpoint: {depth_checkpoint}");
40 Err(SchemaError::Undo { depth })
41 } else {
42 current_depth.set(depth_checkpoint);
43 clear_lines(depth - depth_checkpoint + 1);
44 parse_schema(definitions, title, name, schema, current_depth)
45 }
46 }
47 Err(e) => Err(e),
48 }
49}
50
51pub(crate) fn parse_schema_inner(
52 definitions: &BTreeMap<String, Schema>,
53 title: Option<String>,
54 name: String,
55 schema: SchemaObject,
56 current_depth: &Cell<u16>,
57) -> SchemaResult<Value> {
58 debug!("Entered parse_schema");
59 let description = get_description(&schema);
60 debug!("description: {}", description);
61 match schema.instance_type.clone() {
62 Some(SingleOrVec::Single(instance_type)) => get_single_instance(
63 definitions,
64 schema.array,
65 schema.object,
66 schema.subschemas,
67 instance_type,
68 title,
69 name,
70 description,
71 current_depth,
72 ),
73 Some(SingleOrVec::Vec(vec)) => {
74 let instance_type =
76 Box::new(vec.into_iter().find(|x| x != &InstanceType::Null).unwrap());
77 if Confirm::new("Add optional value?")
78 .with_help_message(format!("{}{}", get_title_str(&title), name).as_str())
79 .prompt_skippable()?
80 .undo(current_depth)?
81 {
82 get_single_instance(
83 definitions,
84 schema.array,
85 schema.object,
86 schema.subschemas,
87 instance_type,
88 title,
89 name,
90 description,
91 current_depth,
92 )
93 } else {
94 Ok(Value::Null)
95 }
96 }
97 None => {
98 if let Some(reference) = schema.reference {
100 let reference = reference.strip_prefix("#/definitions/").unwrap();
101 let schema = definitions.get(reference).unwrap();
102 let schema = get_schema_object_ref(schema)?;
103 parse_schema(
104 definitions,
105 Some(reference.to_string()),
106 name,
107 schema.clone(),
108 current_depth,
109 )
110 }
111 else {
113 get_subschema(
114 definitions,
115 title,
116 name,
117 schema.subschemas,
118 description,
119 current_depth,
120 )
121 }
122 }
123 }
124}
125
126fn update_title(mut title: Option<String>, schema: &SchemaObject) -> Option<String> {
127 if let Some(metadata) = &schema.metadata {
128 title = metadata.title.clone();
129 }
130 title
131}
132
133fn get_title_str(title: &Option<String>) -> String {
134 let mut title_str = String::new();
135 if let Some(title) = title {
136 title_str.push_str(format!("<{title}> ").as_str());
137 }
138 title_str
139}
140
141fn get_description(schema: &SchemaObject) -> String {
142 match &schema.metadata {
143 Some(metadata) => match &metadata.description {
144 Some(description_ref) => {
145 let mut description = description_ref.clone();
146 if description.len() > 60 {
147 description.truncate(60);
148 description.push_str("...");
149 }
150 format!(": {description}")
151 }
152 None => String::default(),
153 },
154 None => String::default(),
155 }
156}
157
158#[allow(clippy::too_many_arguments)]
159#[allow(clippy::boxed_local)]
160fn get_single_instance(
161 definitions: &BTreeMap<String, Schema>,
162 array_info: Option<Box<ArrayValidation>>,
163 object_info: Option<Box<ObjectValidation>>,
164 subschema: Option<Box<SubschemaValidation>>,
165 instance: Box<InstanceType>,
166 title: Option<String>,
167 name: String,
168 description: String,
169 current_depth: &Cell<u16>,
170) -> SchemaResult<Value> {
171 debug!("Entered get_single_instance");
172 match *instance {
173 InstanceType::String => get_string(name, description, current_depth),
174 InstanceType::Number => get_num(name, description, current_depth),
175 InstanceType::Integer => get_int(name, description, current_depth),
176 InstanceType::Boolean => get_bool(name, description, current_depth),
177 InstanceType::Array => get_array(
178 definitions,
179 array_info,
180 title,
181 name,
182 description,
183 current_depth,
184 ),
185 InstanceType::Object => get_object(
186 definitions,
187 object_info,
188 title,
189 name,
190 description,
191 current_depth,
192 ),
193 InstanceType::Null => {
194 get_subschema(
197 definitions,
198 title,
199 name,
200 subschema,
201 description,
202 current_depth,
203 )
204 }
205 }
206}
207
208fn get_subschema(
209 definitions: &BTreeMap<String, Schema>,
210 title: Option<String>,
211 name: String,
212 subschema: Option<Box<SubschemaValidation>>,
213 description: String,
214 current_depth: &Cell<u16>,
215) -> SchemaResult<Value> {
216 debug!("Entered get_subschema");
217 let subschema = subschema.unwrap();
218 if let Some(schema_vec) = subschema.one_of {
220 let mut options = Vec::new();
221 for schema in &schema_vec {
222 let Schema::Object(schema_object) = schema else {
223 panic!("invalid schema");
224 };
225 let name = if let Some(object) = schema_object.clone().object {
227 object.properties.into_iter().next().unwrap().0
228 } else if let Some(enum_values) = schema_object.clone().enum_values {
229 if let Value::String(name) = enum_values.get(0).expect("invalid schema") {
230 name.clone()
231 } else {
232 panic!("invalid schema");
233 }
234 } else {
235 panic!("invalid schema")
236 };
237 options.push(name);
238 }
239 let option = Select::new("Select one:", options.clone())
240 .with_help_message(
241 format!("{}{}{}", get_title_str(&title), name, description.as_str()).as_str(),
242 )
243 .prompt_skippable()?
244 .undo(current_depth)?;
245 let position = options.iter().position(|x| x == &option).unwrap();
246 let schema_object = get_schema_object(schema_vec[position].clone())?;
247 if schema_object.object.is_some() {
248 let title = update_title(title, &schema_object);
249 Ok(parse_schema(
250 definitions,
251 title,
252 name,
253 schema_object,
254 current_depth,
255 )?)
256 } else if let Some(enum_values) = schema_object.enum_values {
257 Ok(enum_values.get(0).expect("invalid schema").clone())
258 } else {
259 panic!("invalid schema")
260 }
261 }
262 else if let Some(schema_vec) = subschema.all_of {
264 let mut values = Vec::new();
265 for schema in schema_vec {
266 let object = get_schema_object(schema)?;
267 let title = update_title(title.clone(), &object);
268 values.push(parse_schema(
269 definitions,
270 title.clone(),
271 name.clone(),
272 object,
273 current_depth,
274 )?)
275 }
276 match values.len() {
277 1 => Ok(values.pop().unwrap()),
278 _ => Ok(Value::Array(values)),
279 }
280 }
281 else if let Some(schema_vec) = subschema.any_of {
284 let non_null = schema_vec
285 .into_iter()
286 .find(|x| {
287 let Schema::Object(object) = x else {
288 panic!("invalid schema");
289 };
290 object.instance_type != Some(SingleOrVec::Single(Box::new(InstanceType::Null)))
291 })
292 .unwrap();
293 let Schema::Object(object) = non_null else {
294 panic!("invalid schema");
295 };
296 let title = update_title(title, &object);
297
298 if Confirm::new("Add optional value?")
299 .with_help_message(format!("{}{}", get_title_str(&title), name).as_str())
300 .prompt_skippable()?
301 .undo(current_depth)?
302 {
303 parse_schema(definitions, title, name, object, current_depth)
304 } else {
305 Ok(Value::Null)
306 }
307 } else {
308 panic!("invalid schema");
309 }
310}
311
312fn get_int(name: String, description: String, current_depth: &Cell<u16>) -> SchemaResult<Value> {
313 debug!("Entered get_int");
314 Ok(json!(CustomType::<i64>::new(name.as_str())
315 .with_help_message(format!("int{description}").as_str())
316 .prompt_skippable()?
317 .undo(current_depth)?))
318}
319
320fn get_string(name: String, description: String, current_depth: &Cell<u16>) -> SchemaResult<Value> {
321 debug!("Entered get_string");
322 Ok(Value::String(
323 Text::new(name.as_str())
324 .with_help_message(format!("string{description}").as_str())
325 .prompt_skippable()?
326 .undo(current_depth)?,
327 ))
328}
329
330fn get_num(name: String, description: String, current_depth: &Cell<u16>) -> SchemaResult<Value> {
331 debug!("Entered get_num");
332 Ok(json!(CustomType::<f64>::new(name.as_str())
333 .with_help_message(format!("num{description}").as_str())
334 .prompt_skippable()?
335 .undo(current_depth)?))
336}
337
338fn get_bool(name: String, description: String, current_depth: &Cell<u16>) -> SchemaResult<Value> {
339 debug!("Entered get_bool");
340 Ok(json!(CustomType::<bool>::new(name.as_str())
341 .with_help_message(format!("bool{description}").as_str())
342 .prompt_skippable()?
343 .undo(current_depth)?))
344}
345
346fn get_array(
347 definitions: &BTreeMap<String, Schema>,
348 array_info: Option<Box<ArrayValidation>>,
349 title: Option<String>,
350 name: String,
351 description: String,
352 current_depth: &Cell<u16>,
353) -> SchemaResult<Value> {
354 debug!("Entered get_array");
355 let array_info = array_info.unwrap();
356 let range = array_info.min_items..array_info.max_items;
357 debug!("array range: {range:?}");
358
359 let mut array = Vec::new();
360 match array_info.items.unwrap() {
361 SingleOrVec::Single(schema) => {
362 debug!("Single type array");
363 array = (0..).recurse_iter(current_depth, |i| {
364 if let Some(end) = range.end {
365 if array.len() == end as usize {
366 return Ok(RecurseLoop::Return(None));
367 }
368 }
369
370 let start = range.start.unwrap_or_default();
371 if i >= start as usize
372 && !Confirm::new("Add element?")
373 .with_help_message(
374 format!("{}{}{}", get_title_str(&title), name, description).as_str(),
375 )
376 .prompt_skippable()?
377 .undo(current_depth)?
378 {
379 return Ok(RecurseLoop::Return(None));
380 }
381
382 let object = get_schema_object(*schema.clone())?;
383 let value = parse_schema(
384 definitions,
385 title.clone(),
386 format!("{}[{}]", name.clone(), i),
387 object,
388 current_depth,
389 )?;
390 Ok(RecurseLoop::Continue(value))
391 })?;
392 }
393 SingleOrVec::Vec(schemas) => {
394 debug!("Vec type array");
395 array = (0..).recurse_iter(current_depth, |i| {
396 if let Some(end) = range.end {
397 if i == end as usize {
398 return Ok(RecurseLoop::Return(None));
399 }
400 }
401
402 let schema = schemas[i].clone();
403
404 let start = range.start.unwrap_or_default();
405 if i >= start as usize
406 && !Confirm::new("Add element?")
407 .with_help_message(
408 format!("{}{}{}", get_title_str(&title), name, description).as_str(),
409 )
410 .prompt_skippable()?
411 .undo(current_depth)?
412 {
413 return Ok(RecurseLoop::Return(None));
414 }
415 let object = get_schema_object(schema)?;
416 let value = parse_schema(
417 definitions,
418 title.clone(),
419 format!("{}.{}", name.clone(), i),
420 object,
421 current_depth,
422 )?;
423
424 Ok(RecurseLoop::Continue(value))
425 })?;
426 }
427 };
428 Ok(Value::Array(array))
429}
430
431fn get_object(
432 definitions: &BTreeMap<String, Schema>,
433 object_info: Option<Box<ObjectValidation>>,
434 title: Option<String>,
435 _name: String,
436 _description: String,
437 current_depth: &Cell<u16>,
438) -> SchemaResult<Value> {
439 debug!("Entered get_object");
440 let map = object_info
441 .unwrap()
442 .properties
443 .iter()
444 .recurse_iter(current_depth, |(name, schema)| {
445 let schema_object = get_schema_object(schema.clone())?;
446 let object = parse_schema(
447 definitions,
448 title.clone(),
449 name.to_string(),
450 schema_object,
451 current_depth,
452 )?;
453 Ok(RecurseLoop::Continue((name, object)))
454 })?
455 .into_iter()
456 .map(|(name, object)| (name.clone(), object))
457 .collect::<Map<String, Value>>();
458 Ok(Value::Object(map))
459}
460
461fn get_schema_object(schema: Schema) -> SchemaResult<SchemaObject> {
462 debug!("Entered get_schema_object");
463 match schema {
464 Schema::Bool(_) => Err(SchemaError::SchemaIsBool),
465 Schema::Object(object) => Ok(object),
466 }
467}
468
469fn get_schema_object_ref(schema: &Schema) -> SchemaResult<&SchemaObject> {
470 debug!("Entered get_schema_object_ref");
471 match schema {
472 Schema::Bool(_) => Err(SchemaError::SchemaIsBool),
473 Schema::Object(object) => Ok(object),
474 }
475}
476
477#[cfg(test)]
478mod tests {
479
480 use inquire::Text;
481 use schemars::JsonSchema;
482 use serde::{Deserialize, Serialize};
483
484 use crate::{clear_lines, traits::InteractiveParseObj};
485
486 #[derive(JsonSchema, Serialize, Deserialize, Debug)]
488 pub struct MyStruct {
489 pub my_int: Option<i32>,
491 pub my_bool: bool,
493 pub my_tuple: Option<(i32, Option<i32>)>,
495 pub my_vec: Vec<i32>,
497 pub my_enum: Option<MyEnum>,
499 pub str_2: Option<MyStruct2>,
501 pub vec_map: MyVecMap,
503 }
504
505 #[derive(JsonSchema, Serialize, Deserialize, Debug)]
507 pub struct MyStruct2 {
508 pub option_int: Option<i32>,
510 }
511
512 #[derive(JsonSchema, Serialize, Deserialize, Debug)]
514 pub struct MyStruct3 {
515 pub option_int: Option<f64>,
517 }
518
519 #[derive(JsonSchema, Serialize, Deserialize, Debug)]
521 pub enum MyEnum {
522 Unit,
524 Unit2,
526 StringNewType(Option<String>, u32),
528 StructVariant {
530 floats: Vec<f32>,
532 },
533 }
534
535 #[derive(JsonSchema, Serialize, Deserialize, Debug)]
538 pub struct MyVecMap(Vec<(String, u32)>);
539
540 fn log_init() {
541 let _ =
542 env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("debug"))
543 .is_test(true)
544 .try_init();
545 }
546
547 #[ignore]
548 #[test]
549 fn test() {
550 log_init();
551 let my_struct = MyStruct::parse_to_obj().unwrap();
552 dbg!(my_struct);
553 }
554
555 #[ignore]
556 #[test]
557 fn test_enum() {
558 let my_struct = MyEnum::parse_to_obj().unwrap();
560 dbg!(my_struct);
561 }
562
563 #[ignore]
564 #[test]
565 fn test_vec_map() {
566 let my_vec_map = MyVecMap::parse_to_obj().unwrap();
568 dbg!(my_vec_map);
569 }
570
571 #[ignore]
572 #[test]
573 fn test_clear_lines() {
574 Text::new("Enter a string")
575 .with_help_message("This is a help message")
576 .prompt_skippable()
577 .unwrap();
578 Text::new("Enter a string")
579 .with_help_message("This is a help message")
580 .prompt_skippable()
581 .unwrap();
582 clear_lines(2);
583 }
584}