openapi-parser 0.6.1

Extract schemas definitions tree from OpenAPI documents
Documentation
use clap::{Parser, Subcommand};
use openapi_parser::{
    build_tree, parse_schema, parse_schema_fix_example_issue, NodeType, ObjectChild, TreeNode,
    REF_OBJECT,
};
use std::collections::HashSet;
use std::fmt::Write;

/// Dump the tree structure of a schema element as dot file
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
    /// The name of the file to dump
    ///
    /// If this value is unspecified, the default petstore schema
    /// is used instead
    #[arg(short, long)]
    file_name: Option<String>,

    /// The name of the structure to dump
    #[arg(short, long, default_value = "Pet")]
    struct_name: String,

    /// Treat "example" field as "examples"
    #[arg(long)]
    fix_example: bool,

    /// The action to perform
    #[clap(subcommand)]
    action: Action,
}

#[derive(Subcommand, Debug, Eq, PartialEq)]
enum Action {
    /// Dump as JSON
    Json,

    /// Dump JSON schema
    JsonSchema,

    /// Dump JSON example
    JsonExample {
        /// Maximum recursion in dump
        #[arg(short, long, default_value_t = 100)]
        max_recursion: usize,
    },

    /// Dump as Graphviz graph
    Graph,

    /// Dump as Tex list
    Tex {
        /// Dump a single entry
        #[arg(short, long)]
        single: bool,
    },
}

fn main() {
    env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));

    let args = Args::parse();

    let file_content = match args.file_name {
        None => include_str!("../examples/petstore.yaml").to_string(),
        Some(path) => std::fs::read_to_string(path).expect("Unable to load schema file!"),
    };

    let schema = match args.fix_example {
        true => parse_schema_fix_example_issue(&file_content),
        false => parse_schema(&file_content),
    };
    let components = schema.components.as_ref().unwrap();

    if args.action == (Action::Tex { single: false }) {
        for entry in components.schemas.keys() {
            let tree = build_tree(entry, components);
            println!("{}", tex_export(&tree));
        }

        return;
    }

    let tree = build_tree(&args.struct_name, components);

    match args.action {
        Action::Json => println!("{}", serde_json::to_string(&tree).unwrap()),
        Action::Graph => println!("{}", graphviz_export(&tree)),
        Action::Tex { .. } => println!("{}", tex_export(&tree)),
        Action::JsonSchema => println!("{}", json_schema(&tree)),
        Action::JsonExample { max_recursion } => {
            println!(
                "{}",
                serde_json::to_string(&tree.example_value(max_recursion)).unwrap()
            )
        }
    }
}

fn recurse_export(
    node: &TreeNode,
    parent_name: &str,
    out: &mut String,
    already_processed: &mut HashSet<String>,
) {
    let key = format!("{} #> {}", parent_name, node.name);
    if already_processed.contains(&key) {
        return;
    }
    already_processed.insert(key);

    if !parent_name.is_empty() && matches!(node.r#type, NodeType::Object { .. }) {
        writeln!(out, "\"{}\" -> \"{}\";", parent_name, node.name).unwrap();
    }

    match &node.r#type {
        NodeType::Array { item } => {
            let mut item = item.clone();
            item.name.push_str(" []");
            recurse_export(&item, parent_name, out, already_processed);
        }
        NodeType::Object { children, .. } => {
            for child in children {
                recurse_export(&child.node, &node.name, out, already_processed);
            }
        }

        _ => {}
    }
}

fn graphviz_export(tree: &TreeNode) -> String {
    let mut out = "digraph G {\n".to_string();

    let mut already_processed = HashSet::new();

    recurse_export(tree, "", &mut out, &mut already_processed);

    out.push_str("}\n");
    out
}

fn tex_escape_str(s: &str) -> String {
    s.replace('_', "\\_")
        .trim_matches('\n')
        .replace("\n\n", "\n")
        .replace('\n', "\\newline\n")
}

fn tex_type_str(t: &TreeNode) -> String {
    match &t.r#type {
        NodeType::Null => "NULL".to_string(),
        NodeType::Boolean => "bool".to_string(),
        NodeType::Array { item } => format!("{}[]", tex_type_str(item)),
        NodeType::Object { .. } => t.name.to_string(),
        NodeType::String => "string".to_string(),
        NodeType::Number => "number".to_string(),
        NodeType::Integer => "integer".to_string(),
    }
}

fn tex_export_inner(tree: &ObjectChild, out: &mut String, required: bool) {
    let type_str = tex_type_str(&tree.node);

    writeln!(
        out,
        "\\schemaprop{{{}}}{{{}}}{{{}}}{{{}}}{{{}}}",
        tex_escape_str(&tree.name),
        match required {
            true => "true",
            false => "false",
        },
        tex_escape_str(&type_str),
        match &tree.node.description {
            None => "".to_string(),
            Some(d) => tex_escape_str(d),
        },
        match (&tree.node.r#type, tree.node.examples.get(0)) {
            (_, Some(e)) => tex_escape_str(e),
            (NodeType::Array { item }, _) if !item.examples.is_empty() => {
                format!("[{}]", &item.examples.get(0).unwrap())
            }
            _ => "".to_string(),
        }
    )
    .unwrap();
}

fn tex_adapt_name(i: &str) -> String {
    i.replace('0', "zero")
        .replace('1', "one")
        .replace('2', "two")
        .replace('3', "three")
        .replace('4', "four")
        .replace('5', "five")
        .replace('6', "six")
        .replace('7', "seven")
        .replace('8', "height")
        .replace('9', "nine")
}

fn tex_export(tree: &TreeNode) -> String {
    let mut out = String::new();
    writeln!(out, "% START OF EXPORT OF SCHEMA {}", tree.name).unwrap();

    let box_name = format!("\\codeBox{}", tex_adapt_name(&tree.name));
    if matches!(tree.r#type, NodeType::Object { .. }) {
        // JSON export
        out.push_str(&format!("\\newsavebox{{{box_name}}}\n"));
        out.push_str(&format!("\\begin{{lrbox}}{{{box_name}}}\n"));
        out.push_str(&format!(
            "\\begin{{jsonsample}}{{{}}}\n",
            tex_escape_str(&tree.name)
        ));
        out.push_str("\\begin{lstlisting}[language=json]\n");
        let json_doc = serde_json::to_string_pretty(&tree.example_value(1)).unwrap();
        let replace_key = serde_json::to_string(REF_OBJECT).unwrap();
        out.push_str(&json_doc.replace(&format!("{replace_key}:"), "$ref"));
        out.push_str("\n\\end{lstlisting}\n");
        out.push_str("\\end{jsonsample}\n");
        out.push_str("\\end{lrbox}\n");
    }

    writeln!(
        out,
        "\\newcommand{{\\schemadef{}}}{{",
        tex_adapt_name(&tree.name)
    )
    .unwrap();

    match &tree.r#type {
        NodeType::Object { children, required } => {
            writeln!(out, "\\schemaname{{{}}}", tex_escape_str(&tree.name)).unwrap();

            if let Some(description) = &tree.description {
                writeln!(
                    out,
                    "\\schemadescription{{{}}}",
                    tex_escape_str(description)
                )
                .unwrap();
            }

            out.push_str("\\begin{schemabody}\n");
            out.push_str("\\begin{schemaprops}\n");
            for child in children {
                tex_export_inner(
                    child,
                    &mut out,
                    required
                        .as_ref()
                        .map(|r| r.contains(&child.name))
                        .unwrap_or(false),
                );
            }
            out.push_str("\\end{schemaprops}\n");

            out.push_str(&format!("\\usebox{{{box_name}}}\n"));
            out.push_str("\\end{schemabody}\n");
        }
        _ => tex_export_inner(
            &ObjectChild {
                name: tree.name.to_string(),
                node: tree.clone(),
            },
            &mut out,
            false,
        ),
    }

    out.push_str("}\n");
    writeln!(out, "% END OF EXPORT OF SCHEMAS {}", tree.name).unwrap();
    out
}

fn json_schema(tree: &TreeNode) -> String {
    serde_json::to_string(&tree.json_schema()).unwrap()
}