move format checking from the command to the library (#179)

This commit is contained in:
Adam Leventhal 2022-08-29 11:16:34 -07:00 committed by GitHub
parent fa4e09f00a
commit eac966effc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 60 additions and 72 deletions

View File

@ -1,5 +1,7 @@
// Copyright 2022 Oxide Computer Company // Copyright 2022 Oxide Computer Company
use std::collections::HashSet;
use openapiv3::OpenAPI; use openapiv3::OpenAPI;
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
use quote::quote; use quote::quote;
@ -135,6 +137,8 @@ impl Generator {
} }
pub fn generate_tokens(&mut self, spec: &OpenAPI) -> Result<TokenStream> { pub fn generate_tokens(&mut self, spec: &OpenAPI) -> Result<TokenStream> {
validate_openapi(spec)?;
// Convert our components dictionary to schemars // Convert our components dictionary to schemars
let schemas = spec let schemas = spec
.components .components
@ -455,6 +459,52 @@ impl Generator {
} }
} }
fn validate_openapi(spec: &OpenAPI) -> Result<()> {
match spec.openapi.as_str() {
"3.0.0" | "3.0.1" | "3.0.2" | "3.0.3" => (),
v => {
return Err(Error::UnexpectedFormat(format!(
"invalid version: {}",
v
)))
}
}
let mut opids = HashSet::new();
spec.paths.paths.iter().try_for_each(|p| {
match p.1 {
openapiv3::ReferenceOr::Reference { reference: _ } => {
Err(Error::UnexpectedFormat(format!(
"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()) {
return Err(Error::UnexpectedFormat(format!(
"duplicate operation ID: {}",
oid,
)));
}
} else {
return Err(Error::UnexpectedFormat(format!(
"path {} is missing operation ID",
p.0,
)));
}
Ok(())
})
}
}
})?;
Ok(())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use serde_json::json; use serde_json::json;

View File

@ -1699,7 +1699,11 @@ impl Generator {
match (body.content.first(), body.content.len()) { match (body.content.first(), body.content.len()) {
(None, _) => return Ok(None), (None, _) => return Ok(None),
(Some(first), 1) => first, (Some(first), 1) => first,
(_, n) => todo!("more media types than expected: {}", n), (_, n) => todo!(
"more media types than expected for {}: {}",
operation.operation_id.as_ref().unwrap(),
n,
),
}; };
let schema = media_type.schema.as_ref().ok_or_else(|| { let schema = media_type.schema.as_ref().ok_or_else(|| {

View File

@ -1,7 +1,6 @@
// Copyright 2021 Oxide Computer Company // Copyright 2022 Oxide Computer Company
use std::{ use std::{
collections::HashSet,
fs::{File, OpenOptions}, fs::{File, OpenOptions},
io::Write, io::Write,
path::{Path, PathBuf}, path::{Path, PathBuf},
@ -11,7 +10,6 @@ use anyhow::{bail, Result};
use clap::{Parser, ValueEnum}; use clap::{Parser, ValueEnum};
use openapiv3::OpenAPI; use openapiv3::OpenAPI;
use progenitor::{GenerationSettings, Generator, InterfaceStyle, TagStyle}; use progenitor::{GenerationSettings, Generator, InterfaceStyle, TagStyle};
use serde::Deserialize;
#[derive(Parser)] #[derive(Parser)]
struct Args { struct Args {
@ -124,7 +122,7 @@ fn main() -> Result<()> {
"[package]\n\ "[package]\n\
name = \"{}\"\n\ name = \"{}\"\n\
version = \"{}\"\n\ version = \"{}\"\n\
edition = \"2018\"\n\ edition = \"2021\"\n\
\n\ \n\
[dependencies]\n\ [dependencies]\n\
{}\n\ {}\n\
@ -169,75 +167,11 @@ fn main() -> Result<()> {
Ok(()) 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)?)
}
// TODO some or all of this validation should be in progenitor-impl
pub fn load_api<P>(p: P) -> Result<OpenAPI> pub fn load_api<P>(p: P) -> Result<OpenAPI>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
let api: OpenAPI = load(p)?; let f = File::open(p)?;
let api = serde_json::from_reader(f)?;
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");
}
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.servers.is_empty() {
bail!("op {}: servers, unsupported", oid);
}
if o.security.is_some() {
bail!("op {}: security, unsupported", 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) Ok(api)
} }

View File

@ -1,2 +1,2 @@
edition = "2018" edition = "2021"
max_width = 80 max_width = 80