250 lines
7.0 KiB
Rust
250 lines
7.0 KiB
Rust
|
// Copyright 2021 Oxide Computer Company
|
||
|
|
||
|
use std::{
|
||
|
collections::HashSet,
|
||
|
fs::{File, OpenOptions},
|
||
|
io::Write,
|
||
|
path::{Path, PathBuf},
|
||
|
};
|
||
|
|
||
|
use anyhow::{bail, Result};
|
||
|
use openapiv3::OpenAPI;
|
||
|
use progenitor::Generator;
|
||
|
use serde::Deserialize;
|
||
|
|
||
|
fn save<P>(p: P, data: &str) -> Result<()>
|
||
|
where
|
||
|
P: AsRef<Path>,
|
||
|
{
|
||
|
let p = p.as_ref();
|
||
|
let mut f = OpenOptions::new()
|
||
|
.create(true)
|
||
|
.truncate(true)
|
||
|
.write(true)
|
||
|
.open(p)?;
|
||
|
f.write_all(data.as_bytes())?;
|
||
|
f.flush()?;
|
||
|
Ok(())
|
||
|
}
|
||
|
|
||
|
fn main() -> Result<()> {
|
||
|
let mut opts = getopts::Options::new();
|
||
|
opts.parsing_style(getopts::ParsingStyle::StopAtFirstFree);
|
||
|
opts.reqopt("i", "", "OpenAPI definition document (JSON)", "INPUT");
|
||
|
opts.reqopt("o", "", "Generated Rust crate directory", "OUTPUT");
|
||
|
opts.reqopt("n", "", "Target Rust crate name", "CRATE");
|
||
|
opts.reqopt("v", "", "Target Rust crate version", "VERSION");
|
||
|
|
||
|
let args = match opts.parse(std::env::args().skip(1)) {
|
||
|
Ok(args) => {
|
||
|
if !args.free.is_empty() {
|
||
|
eprintln!("{}", opts.usage("progenitor"));
|
||
|
bail!("unexpected positional arguments");
|
||
|
}
|
||
|
args
|
||
|
}
|
||
|
Err(e) => {
|
||
|
eprintln!("{}", opts.usage("progenitor"));
|
||
|
bail!(e);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
let api = load_api(&args.opt_str("i").unwrap())?;
|
||
|
|
||
|
let mut builder = Generator::new();
|
||
|
|
||
|
let fail = match builder.generate_text(&api) {
|
||
|
Ok(out) => {
|
||
|
let type_space = builder.get_type_space();
|
||
|
|
||
|
println!("-----------------------------------------------------");
|
||
|
println!(" TYPE SPACE");
|
||
|
println!("-----------------------------------------------------");
|
||
|
for (idx, type_entry) in type_space.iter_types().enumerate() {
|
||
|
let n = type_entry.describe();
|
||
|
println!("{:>4} {}", idx, n);
|
||
|
}
|
||
|
println!("-----------------------------------------------------");
|
||
|
println!();
|
||
|
|
||
|
let name = args.opt_str("n").unwrap();
|
||
|
let version = args.opt_str("v").unwrap();
|
||
|
|
||
|
/*
|
||
|
* Create the top-level crate directory:
|
||
|
*/
|
||
|
let root = PathBuf::from(args.opt_str("o").unwrap());
|
||
|
std::fs::create_dir_all(&root)?;
|
||
|
|
||
|
/*
|
||
|
* Write the Cargo.toml file:
|
||
|
*/
|
||
|
let mut toml = root.clone();
|
||
|
toml.push("Cargo.toml");
|
||
|
|
||
|
let tomlout = format!(
|
||
|
"[package]\n\
|
||
|
name = \"{}\"\n\
|
||
|
version = \"{}\"\n\
|
||
|
edition = \"2018\"\n\
|
||
|
\n\
|
||
|
[dependencies]\n\
|
||
|
{}",
|
||
|
name,
|
||
|
version,
|
||
|
builder.dependencies().join("\n"),
|
||
|
);
|
||
|
|
||
|
save(&toml, tomlout.as_str())?;
|
||
|
|
||
|
/*
|
||
|
* Create the src/ directory:
|
||
|
*/
|
||
|
let mut src = root;
|
||
|
src.push("src");
|
||
|
std::fs::create_dir_all(&src)?;
|
||
|
|
||
|
/*
|
||
|
* Create the Rust source file containing the generated client:
|
||
|
*/
|
||
|
let mut librs = src;
|
||
|
librs.push("lib.rs");
|
||
|
save(librs, out.as_str())?;
|
||
|
false
|
||
|
}
|
||
|
Err(e) => {
|
||
|
println!("gen fail: {:?}", e);
|
||
|
true
|
||
|
}
|
||
|
};
|
||
|
|
||
|
if fail {
|
||
|
bail!("generation experienced errors");
|
||
|
}
|
||
|
|
||
|
Ok(())
|
||
|
}
|
||
|
|
||
|
fn load<P, T>(p: P) -> Result<T>
|
||
|
where
|
||
|
P: AsRef<Path>,
|
||
|
for<'de> T: Deserialize<'de>,
|
||
|
{
|
||
|
let p = p.as_ref();
|
||
|
let f = File::open(p)?;
|
||
|
Ok(serde_json::from_reader(f)?)
|
||
|
}
|
||
|
|
||
|
pub fn load_api<P>(p: P) -> Result<OpenAPI>
|
||
|
where
|
||
|
P: AsRef<Path>,
|
||
|
{
|
||
|
let api: OpenAPI = load(p)?;
|
||
|
|
||
|
if api.openapi != "3.0.3" {
|
||
|
/*
|
||
|
* XXX During development we are being very strict, but this should
|
||
|
* probably be relaxed.
|
||
|
*/
|
||
|
bail!("unexpected version {}", api.openapi);
|
||
|
}
|
||
|
|
||
|
if !api.servers.is_empty() {
|
||
|
bail!("servers not presently supported");
|
||
|
}
|
||
|
|
||
|
if api.security.is_some() {
|
||
|
bail!("security not presently supported");
|
||
|
}
|
||
|
|
||
|
if !api.tags.is_empty() {
|
||
|
bail!("tags not presently supported");
|
||
|
}
|
||
|
|
||
|
if let Some(components) = api.components.as_ref() {
|
||
|
if !components.security_schemes.is_empty() {
|
||
|
bail!("component security schemes not supported");
|
||
|
}
|
||
|
|
||
|
if !components.responses.is_empty() {
|
||
|
bail!("component responses not supported");
|
||
|
}
|
||
|
|
||
|
if !components.parameters.is_empty() {
|
||
|
bail!("component parameters not supported");
|
||
|
}
|
||
|
|
||
|
if !components.request_bodies.is_empty() {
|
||
|
bail!("component request bodies not supported");
|
||
|
}
|
||
|
|
||
|
if !components.headers.is_empty() {
|
||
|
bail!("component headers not supported");
|
||
|
}
|
||
|
|
||
|
if !components.links.is_empty() {
|
||
|
bail!("component links not supported");
|
||
|
}
|
||
|
|
||
|
if !components.callbacks.is_empty() {
|
||
|
bail!("component callbacks not supported");
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* XXX Ignoring "examples" and "extensions" for now.
|
||
|
* Explicitly allowing "schemas" through.
|
||
|
*/
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* XXX Ignoring "external_docs" and "extensions" for now, as they seem not
|
||
|
* to immediately affect our code generation.
|
||
|
*/
|
||
|
let mut opids = HashSet::new();
|
||
|
for p in api.paths.paths.iter() {
|
||
|
match p.1 {
|
||
|
openapiv3::ReferenceOr::Reference { reference: _ } => {
|
||
|
bail!("path {} uses reference, unsupported", p.0);
|
||
|
}
|
||
|
openapiv3::ReferenceOr::Item(item) => {
|
||
|
/*
|
||
|
* Make sure every operation has an operation ID, and that each
|
||
|
* operation ID is only used once in the document.
|
||
|
*/
|
||
|
item.iter().try_for_each(|(_, o)| {
|
||
|
if let Some(oid) = o.operation_id.as_ref() {
|
||
|
if !opids.insert(oid.to_string()) {
|
||
|
bail!("duplicate operation ID: {}", oid);
|
||
|
}
|
||
|
|
||
|
if !o.tags.is_empty() {
|
||
|
bail!("op {}: tags, unsupported", oid);
|
||
|
}
|
||
|
|
||
|
if !o.servers.is_empty() {
|
||
|
bail!("op {}: servers, unsupported", oid);
|
||
|
}
|
||
|
|
||
|
if o.security.is_some() {
|
||
|
bail!("op {}: security, unsupported", oid);
|
||
|
}
|
||
|
|
||
|
if o.responses.default.is_some() {
|
||
|
bail!("op {}: has response default", oid);
|
||
|
}
|
||
|
} else {
|
||
|
bail!("path {} is missing operation ID", p.0);
|
||
|
}
|
||
|
Ok(())
|
||
|
})?;
|
||
|
|
||
|
if !item.servers.is_empty() {
|
||
|
bail!("path {} has servers; unsupported", p.0);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Ok(api)
|
||
|
}
|