This repository has been archived on 2025-03-28. You can view files and clone it, but cannot push or open issues or pull requests.

248 lines
6.8 KiB
Rust

use clap::{Parser, Subcommand};
use openapi_parser::{
build_tree, parse_schema, parse_schema_fix_example_issue, NodeType, ObjectChild, TreeNode,
};
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,
/// 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 => {
println!("{}", serde_json::to_string(&tree.example_value()).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();
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{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");
}
_ => 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()
}