use anyhow::Context;
use clap::{Parser, Subcommand, ValueEnum};
use std::collections::BTreeSet;
use std::io::Write;
use std::path::PathBuf;
#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
#[arg(short, long, action = clap::ArgAction::Count)]
debug: u8,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Codegen {
#[arg(short, long, value_name = "FILE")]
schema: Option<PathBuf>,
#[arg(short, long, value_name = "FILE")]
output: Option<PathBuf>,
#[arg(short, long)]
language: Language,
#[arg(long, value_delimiter = ',')]
shared_modules: Option<Vec<String>>,
#[arg(short, long, value_delimiter = ',')]
include_tags: Vec<String>,
#[arg(short, long, value_delimiter = ',')]
exclude_tags: Vec<String>,
#[arg(short, long, default_value = "false")]
typecheck: bool,
#[arg(short, long, default_value = "true")]
format: bool,
#[arg(short = 'I', long, default_value = "false")]
instrument: bool,
#[arg(long, default_value = "api_client")]
python_package_name: String,
#[arg(long, default_value = "true")]
python_async: bool,
#[arg(long, default_value = "false")]
python_sync: bool,
#[arg(long, default_value = "false")]
python_testing: bool,
},
#[command(subcommand)]
Doc(DocSubcommand),
}
#[derive(Subcommand)]
enum DocSubcommand {
Open {
#[arg(short, long, default_value = "8080")]
port: u16,
#[arg(default_value = "reflectapi.json")]
path: PathBuf,
},
}
#[derive(ValueEnum, Clone, PartialEq)]
enum Language {
Typescript,
Rust,
Python,
Openapi,
}
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::Doc(doc) => match doc {
DocSubcommand::Open { port, path } => {
let mut path = path.canonicalize()?;
if path.is_dir() {
path.push("reflectapi.json");
}
let schema: reflectapi::Schema = serde_json::from_reader(std::fs::File::open(
&path,
)?)
.context("Failed to parse schema file as JSON into reflectapi::Schema object")?;
let addr = format!("0.0.0.0:{port}");
eprintln!("Serving {} on http://{addr}", path.display());
let openapi = reflectapi::codegen::openapi::Spec::from(&schema);
rouille::start_server(addr, move |request| {
rouille::router!(request,
(GET) (/) => { rouille::Response::html(include_str!("../redoc.html")) },
(GET) (/openapi) => { rouille::Response::json(&openapi) },
_ => rouille::Response::empty_404()
)
})
}
},
Commands::Codegen {
schema,
output,
language,
shared_modules,
include_tags,
exclude_tags,
typecheck,
format,
instrument,
python_package_name,
python_async,
python_sync,
python_testing,
} => {
let include_tags = BTreeSet::from_iter(include_tags);
let exclude_tags = BTreeSet::from_iter(exclude_tags);
let schema_path = schema.unwrap_or(std::path::PathBuf::from("reflectapi.json"));
let schema_as_json = std::fs::read_to_string(schema_path.clone())
.context(format!("Failed to read schema file: {schema_path:?}"))?;
let schema: reflectapi::Schema = serde_json::from_str(&schema_as_json)
.context("Failed to parse schema file as JSON into reflectapi::Schema object")?;
let files: std::collections::BTreeMap<String, String> = match language {
Language::Typescript => {
let content = reflectapi::codegen::typescript::generate(
schema,
reflectapi::codegen::typescript::Config::default()
.format(format)
.typecheck(typecheck)
.include_tags(include_tags)
.exclude_tags(exclude_tags),
)?;
let mut files = std::collections::BTreeMap::new();
files.insert("generated.ts".to_string(), content);
files
}
Language::Rust => {
let content = reflectapi::codegen::rust::generate(
schema,
reflectapi::codegen::rust::Config::default()
.format(format)
.typecheck(typecheck)
.instrument(instrument)
.include_tags(include_tags)
.exclude_tags(exclude_tags)
.shared_modules(
shared_modules.unwrap_or_default().into_iter().collect(),
),
)?;
let mut files = std::collections::BTreeMap::new();
files.insert("generated.rs".to_string(), content);
files
}
Language::Python => {
let config = reflectapi::codegen::python::Config {
package_name: python_package_name,
generate_async: python_async,
generate_sync: python_sync,
generate_testing: python_testing,
format,
base_url: None,
};
reflectapi::codegen::python::generate_files(schema, &config)?
}
Language::Openapi => {
let content = reflectapi::codegen::openapi::generate(
&schema,
reflectapi::codegen::openapi::Config::default()
.include_tags(include_tags)
.exclude_tags(exclude_tags),
)?;
let mut files = std::collections::BTreeMap::new();
files.insert("openapi.json".to_string(), content);
files
}
};
if output == Some(std::path::PathBuf::from("-")) {
if let Some(content) = files.values().next() {
println!("{content}");
}
return Ok(());
}
let output_path = output.unwrap_or_else(|| std::path::PathBuf::from("./"));
if files.len() == 1 {
let (filename, content) = files.iter().next().unwrap();
let final_path =
if output_path.is_dir() || output_path.to_string_lossy().ends_with('/') {
output_path.join(filename)
} else {
output_path
};
let mut file = std::fs::File::create(&final_path)
.context(format!("Failed to create file: {final_path:?}"))?;
file.write_all(content.as_bytes())
.context(format!("Failed to write to file: {final_path:?}"))?;
} else {
std::fs::create_dir_all(&output_path).context(format!(
"Failed to create output directory: {output_path:?}"
))?;
for (filename, content) in files {
let file_path = output_path.join(&filename);
let mut file = std::fs::File::create(&file_path)
.context(format!("Failed to create file: {file_path:?}"))?;
file.write_all(content.as_bytes())
.context(format!("Failed to write to file: {file_path:?}"))?;
}
}
Ok(())
}
}
}