interactive_parse/
lib.rs

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            // This usually represents an optional regular type
75            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            // This represents a referenced type
99            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            // Or it could be a subschema
112            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            // This represents an optional enum
195            // Likely the subschema will have info here.
196            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    // First we check the one_of field.
219    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            // debug!("schema: {schema:#?}");
226            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    // Next check the all_of field.
263    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    // Next check the any_of field.
282    // This seems to be a weird way to get options
283    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    /// This is the struct used for testing.
487    #[derive(JsonSchema, Serialize, Deserialize, Debug)]
488    pub struct MyStruct {
489        /// This is an integer.
490        pub my_int: Option<i32>,
491        /// This is a boolean.
492        pub my_bool: bool,
493        /// This is an optional tuple of ints.
494        pub my_tuple: Option<(i32, Option<i32>)>,
495        /// This is a vec of ints.
496        pub my_vec: Vec<i32>,
497        /// This is an enumerated type.
498        pub my_enum: Option<MyEnum>,
499        /// This is an object.
500        pub str_2: Option<MyStruct2>,
501        /// This is a vec of tuples.
502        pub vec_map: MyVecMap,
503    }
504
505    /// Doc comment on struct
506    #[derive(JsonSchema, Serialize, Deserialize, Debug)]
507    pub struct MyStruct2 {
508        /// Doc comment on field
509        pub option_int: Option<i32>,
510    }
511
512    /// Doc comment on struct
513    #[derive(JsonSchema, Serialize, Deserialize, Debug)]
514    pub struct MyStruct3 {
515        /// Doc comment on field
516        pub option_int: Option<f64>,
517    }
518
519    /// Doc comment on enum
520    #[derive(JsonSchema, Serialize, Deserialize, Debug)]
521    pub enum MyEnum {
522        /// This is a unit variant.
523        Unit,
524        /// This is a unit variant.
525        Unit2,
526        /// This is a tuple variant.
527        StringNewType(Option<String>, u32),
528        /// This is a struct variant.
529        StructVariant {
530            /// This is a vec of floats.
531            floats: Vec<f32>,
532        },
533    }
534
535    // This type is exiting early in the vec.
536    /// Doc comment on struct
537    #[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        // log_init();
559        let my_struct = MyEnum::parse_to_obj().unwrap();
560        dbg!(my_struct);
561    }
562
563    #[ignore]
564    #[test]
565    fn test_vec_map() {
566        // log_init();
567        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}