2022-01-15 02:01:59 +00:00
|
|
|
// Copyright 2022 Oxide Computer Company
|
2021-10-17 17:40:22 +00:00
|
|
|
|
2022-08-29 18:16:34 +00:00
|
|
|
use std::collections::HashSet;
|
|
|
|
|
2022-04-02 05:30:23 +00:00
|
|
|
use openapiv3::OpenAPI;
|
2021-10-17 17:40:22 +00:00
|
|
|
use proc_macro2::TokenStream;
|
2022-04-02 05:30:23 +00:00
|
|
|
use quote::quote;
|
2022-07-03 02:09:38 +00:00
|
|
|
use serde::Deserialize;
|
2021-10-17 17:40:22 +00:00
|
|
|
use thiserror::Error;
|
2022-08-18 18:58:55 +00:00
|
|
|
use typify::{TypeSpace, TypeSpaceSettings};
|
2021-10-17 17:40:22 +00:00
|
|
|
|
|
|
|
use crate::to_schema::ToSchema;
|
|
|
|
|
2022-04-02 05:30:23 +00:00
|
|
|
mod method;
|
2021-10-17 17:40:22 +00:00
|
|
|
mod template;
|
|
|
|
mod to_schema;
|
2022-04-02 05:30:23 +00:00
|
|
|
mod util;
|
2021-10-17 17:40:22 +00:00
|
|
|
|
|
|
|
#[derive(Error, Debug)]
|
|
|
|
pub enum Error {
|
2022-07-15 04:25:48 +00:00
|
|
|
#[error("unexpected value type {0}: {1}")]
|
2021-10-17 17:40:22 +00:00
|
|
|
BadValue(String, serde_json::Value),
|
2022-07-15 04:25:48 +00:00
|
|
|
#[error("type error {0}")]
|
2021-10-17 17:40:22 +00:00
|
|
|
TypeError(#[from] typify::Error),
|
2022-07-15 04:25:48 +00:00
|
|
|
#[error("unexpected or unhandled format in the OpenAPI document {0}")]
|
2022-05-22 01:00:05 +00:00
|
|
|
UnexpectedFormat(String),
|
2022-07-15 04:25:48 +00:00
|
|
|
#[error("invalid operation path {0}")]
|
2021-10-17 17:40:22 +00:00
|
|
|
InvalidPath(String),
|
2022-07-15 04:25:48 +00:00
|
|
|
#[error("internal error {0}")]
|
2022-05-22 01:00:05 +00:00
|
|
|
InternalError(String),
|
2021-10-17 17:40:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub type Result<T> = std::result::Result<T, Error>;
|
|
|
|
|
|
|
|
pub struct Generator {
|
|
|
|
type_space: TypeSpace,
|
2022-07-03 02:09:38 +00:00
|
|
|
settings: GenerationSettings,
|
|
|
|
uses_futures: bool,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Default, Clone)]
|
|
|
|
pub struct GenerationSettings {
|
|
|
|
interface: InterfaceStyle,
|
|
|
|
tag: TagStyle,
|
2021-10-29 14:16:39 +00:00
|
|
|
inner_type: Option<TokenStream>,
|
|
|
|
pre_hook: Option<TokenStream>,
|
|
|
|
post_hook: Option<TokenStream>,
|
2022-08-18 18:58:55 +00:00
|
|
|
extra_derives: Vec<String>,
|
2021-10-17 17:40:22 +00:00
|
|
|
}
|
|
|
|
|
2022-08-18 18:58:55 +00:00
|
|
|
#[derive(Clone, Deserialize, PartialEq, Eq)]
|
2022-07-03 02:09:38 +00:00
|
|
|
pub enum InterfaceStyle {
|
|
|
|
Positional,
|
|
|
|
Builder,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for InterfaceStyle {
|
|
|
|
fn default() -> Self {
|
|
|
|
Self::Positional
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Deserialize)]
|
|
|
|
pub enum TagStyle {
|
|
|
|
Merged,
|
|
|
|
Separate,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for TagStyle {
|
|
|
|
fn default() -> Self {
|
|
|
|
Self::Merged
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl GenerationSettings {
|
2021-10-17 17:40:22 +00:00
|
|
|
pub fn new() -> Self {
|
|
|
|
Self::default()
|
|
|
|
}
|
|
|
|
|
2022-07-03 02:09:38 +00:00
|
|
|
pub fn with_interface(&mut self, interface: InterfaceStyle) -> &mut Self {
|
|
|
|
self.interface = interface;
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn with_tag(&mut self, tag: TagStyle) -> &mut Self {
|
|
|
|
self.tag = tag;
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
2021-10-29 14:16:39 +00:00
|
|
|
pub fn with_inner_type(&mut self, inner_type: TokenStream) -> &mut Self {
|
|
|
|
self.inner_type = Some(inner_type);
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn with_pre_hook(&mut self, pre_hook: TokenStream) -> &mut Self {
|
|
|
|
self.pre_hook = Some(pre_hook);
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn with_post_hook(&mut self, post_hook: TokenStream) -> &mut Self {
|
|
|
|
self.post_hook = Some(post_hook);
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
2022-08-18 18:58:55 +00:00
|
|
|
pub fn with_derive(&mut self, derive: impl ToString) -> &mut Self {
|
|
|
|
self.extra_derives.push(derive.to_string());
|
2022-01-15 02:01:59 +00:00
|
|
|
self
|
|
|
|
}
|
2022-07-03 02:09:38 +00:00
|
|
|
}
|
|
|
|
|
2022-08-18 18:58:55 +00:00
|
|
|
impl Default for Generator {
|
|
|
|
fn default() -> Self {
|
|
|
|
Self {
|
|
|
|
type_space: TypeSpace::new(
|
|
|
|
TypeSpaceSettings::default().with_type_mod("types"),
|
|
|
|
),
|
|
|
|
settings: Default::default(),
|
|
|
|
uses_futures: Default::default(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-03 02:09:38 +00:00
|
|
|
impl Generator {
|
|
|
|
pub fn new(settings: &GenerationSettings) -> Self {
|
2022-08-18 18:58:55 +00:00
|
|
|
let mut type_settings = TypeSpaceSettings::default();
|
|
|
|
type_settings
|
|
|
|
.with_type_mod("types")
|
|
|
|
.with_struct_builder(settings.interface == InterfaceStyle::Builder);
|
|
|
|
settings.extra_derives.iter().for_each(|derive| {
|
|
|
|
let _ = type_settings.with_derive(derive.clone());
|
|
|
|
});
|
2022-07-03 02:09:38 +00:00
|
|
|
Self {
|
2022-08-18 18:58:55 +00:00
|
|
|
type_space: TypeSpace::new(&type_settings),
|
2022-07-03 02:09:38 +00:00
|
|
|
settings: settings.clone(),
|
|
|
|
uses_futures: false,
|
|
|
|
}
|
|
|
|
}
|
2022-01-15 02:01:59 +00:00
|
|
|
|
2021-10-17 17:40:22 +00:00
|
|
|
pub fn generate_tokens(&mut self, spec: &OpenAPI) -> Result<TokenStream> {
|
2022-08-29 18:16:34 +00:00
|
|
|
validate_openapi(spec)?;
|
|
|
|
|
2021-10-17 17:40:22 +00:00
|
|
|
// Convert our components dictionary to schemars
|
|
|
|
let schemas = spec
|
|
|
|
.components
|
|
|
|
.iter()
|
|
|
|
.flat_map(|components| {
|
|
|
|
components.schemas.iter().map(|(name, ref_or_schema)| {
|
|
|
|
(name.clone(), ref_or_schema.to_schema())
|
|
|
|
})
|
|
|
|
})
|
|
|
|
.collect::<Vec<(String, _)>>();
|
|
|
|
|
|
|
|
self.type_space.add_ref_types(schemas)?;
|
|
|
|
|
2021-12-10 02:15:24 +00:00
|
|
|
let raw_methods = spec
|
2021-10-29 14:16:39 +00:00
|
|
|
.paths
|
|
|
|
.iter()
|
|
|
|
.flat_map(|(path, ref_or_item)| {
|
2021-12-10 02:15:24 +00:00
|
|
|
// Exclude externally defined path items.
|
2021-10-29 14:16:39 +00:00
|
|
|
let item = ref_or_item.as_item().unwrap();
|
2022-05-22 01:00:05 +00:00
|
|
|
// TODO punt on parameters that apply to all path items for now.
|
2021-10-29 14:16:39 +00:00
|
|
|
assert!(item.parameters.is_empty());
|
|
|
|
item.iter().map(move |(method, operation)| {
|
|
|
|
(path.as_str(), method, operation)
|
|
|
|
})
|
|
|
|
})
|
2021-10-17 17:40:22 +00:00
|
|
|
.map(|(path, method, operation)| {
|
2021-10-29 14:16:39 +00:00
|
|
|
self.process_operation(
|
|
|
|
operation,
|
|
|
|
&spec.components,
|
|
|
|
path,
|
|
|
|
method,
|
|
|
|
)
|
2021-10-17 17:40:22 +00:00
|
|
|
})
|
|
|
|
.collect::<Result<Vec<_>>>()?;
|
|
|
|
|
2022-07-03 02:09:38 +00:00
|
|
|
let operation_code = match (
|
|
|
|
&self.settings.interface,
|
|
|
|
&self.settings.tag,
|
|
|
|
) {
|
|
|
|
(InterfaceStyle::Positional, TagStyle::Merged) => {
|
|
|
|
self.generate_tokens_positional_merged(&raw_methods)
|
|
|
|
}
|
|
|
|
(InterfaceStyle::Positional, TagStyle::Separate) => {
|
|
|
|
unimplemented!("positional arguments with separate tags are currently unsupported")
|
|
|
|
}
|
|
|
|
(InterfaceStyle::Builder, TagStyle::Merged) => {
|
|
|
|
self.generate_tokens_builder_merged(&raw_methods)
|
|
|
|
}
|
|
|
|
(InterfaceStyle::Builder, TagStyle::Separate) => {
|
|
|
|
self.generate_tokens_builder_separate(&raw_methods)
|
|
|
|
}
|
|
|
|
}?;
|
2021-12-10 02:15:24 +00:00
|
|
|
|
2022-08-18 18:58:55 +00:00
|
|
|
let types = self.type_space.to_stream();
|
2021-10-17 17:40:22 +00:00
|
|
|
|
2022-07-03 02:09:38 +00:00
|
|
|
let inner_property = self.settings.inner_type.as_ref().map(|inner| {
|
|
|
|
quote! {
|
|
|
|
pub (crate) inner: #inner,
|
|
|
|
}
|
|
|
|
});
|
|
|
|
let inner_parameter = self.settings.inner_type.as_ref().map(|inner| {
|
2021-10-29 14:16:39 +00:00
|
|
|
quote! {
|
|
|
|
inner: #inner,
|
|
|
|
}
|
|
|
|
});
|
2022-07-03 02:09:38 +00:00
|
|
|
let inner_value = self.settings.inner_type.as_ref().map(|_| {
|
2021-10-29 14:16:39 +00:00
|
|
|
quote! {
|
|
|
|
inner
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2021-10-17 17:40:22 +00:00
|
|
|
let file = quote! {
|
2022-02-08 16:59:38 +00:00
|
|
|
// Re-export ResponseValue and Error since those are used by the
|
|
|
|
// public interface of Client.
|
2022-03-15 23:11:47 +00:00
|
|
|
pub use progenitor_client::{ByteStream, Error, ResponseValue};
|
2022-07-03 02:09:38 +00:00
|
|
|
#[allow(unused_imports)]
|
2022-07-08 01:35:36 +00:00
|
|
|
use progenitor_client::{encode_path, RequestBuilderExt};
|
2021-10-17 17:40:22 +00:00
|
|
|
|
|
|
|
pub mod types {
|
|
|
|
use serde::{Deserialize, Serialize};
|
2022-07-18 18:55:21 +00:00
|
|
|
|
|
|
|
// This may be used by some impl Deserialize, but not all.
|
|
|
|
#[allow(unused_imports)]
|
|
|
|
use std::convert::TryFrom;
|
2022-08-18 18:58:55 +00:00
|
|
|
|
|
|
|
#types
|
2021-10-17 17:40:22 +00:00
|
|
|
}
|
|
|
|
|
2022-08-03 01:18:43 +00:00
|
|
|
#[derive(Clone, Debug)]
|
2021-10-17 17:40:22 +00:00
|
|
|
pub struct Client {
|
2022-07-03 02:09:38 +00:00
|
|
|
pub(crate) baseurl: String,
|
|
|
|
pub(crate) client: reqwest::Client,
|
2021-10-29 14:16:39 +00:00
|
|
|
#inner_property
|
2021-10-17 17:40:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Client {
|
2021-10-29 14:16:39 +00:00
|
|
|
pub fn new(
|
|
|
|
baseurl: &str,
|
2022-07-03 02:09:38 +00:00
|
|
|
#inner_parameter
|
2021-10-29 14:16:39 +00:00
|
|
|
) -> Self {
|
2021-10-17 17:40:22 +00:00
|
|
|
let dur = std::time::Duration::from_secs(15);
|
|
|
|
let client = reqwest::ClientBuilder::new()
|
|
|
|
.connect_timeout(dur)
|
|
|
|
.timeout(dur)
|
|
|
|
.build()
|
|
|
|
.unwrap();
|
2021-10-29 14:16:39 +00:00
|
|
|
Self::new_with_client(baseurl, client, #inner_value)
|
2021-10-17 17:40:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn new_with_client(
|
|
|
|
baseurl: &str,
|
|
|
|
client: reqwest::Client,
|
2022-07-03 02:09:38 +00:00
|
|
|
#inner_parameter
|
2021-10-29 14:16:39 +00:00
|
|
|
) -> Self {
|
|
|
|
Self {
|
2021-10-17 17:40:22 +00:00
|
|
|
baseurl: baseurl.to_string(),
|
|
|
|
client,
|
2021-10-29 14:16:39 +00:00
|
|
|
#inner_value
|
2021-10-17 17:40:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-07 06:24:03 +00:00
|
|
|
pub fn baseurl(&self) -> &String {
|
|
|
|
&self.baseurl
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn client(&self) -> &reqwest::Client {
|
|
|
|
&self.client
|
|
|
|
}
|
2021-10-17 17:40:22 +00:00
|
|
|
}
|
2022-07-03 02:09:38 +00:00
|
|
|
|
|
|
|
#operation_code
|
2021-10-17 17:40:22 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
Ok(file)
|
|
|
|
}
|
|
|
|
|
2022-07-03 02:09:38 +00:00
|
|
|
fn generate_tokens_positional_merged(
|
|
|
|
&mut self,
|
|
|
|
input_methods: &[method::OperationMethod],
|
|
|
|
) -> Result<TokenStream> {
|
|
|
|
let methods = input_methods
|
|
|
|
.iter()
|
|
|
|
.map(|method| self.positional_method(method))
|
|
|
|
.collect::<Result<Vec<_>>>()?;
|
|
|
|
let out = quote! {
|
|
|
|
impl Client {
|
|
|
|
#(#methods)*
|
|
|
|
}
|
2022-08-27 15:23:02 +00:00
|
|
|
|
|
|
|
pub mod prelude {
|
|
|
|
pub use super::Client;
|
|
|
|
}
|
2022-07-03 02:09:38 +00:00
|
|
|
};
|
|
|
|
Ok(out)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn generate_tokens_builder_merged(
|
|
|
|
&mut self,
|
|
|
|
input_methods: &[method::OperationMethod],
|
|
|
|
) -> Result<TokenStream> {
|
|
|
|
let builder_struct = input_methods
|
|
|
|
.iter()
|
|
|
|
.map(|method| self.builder_struct(method, TagStyle::Merged))
|
|
|
|
.collect::<Result<Vec<_>>>()?;
|
|
|
|
|
|
|
|
let builder_methods = input_methods
|
|
|
|
.iter()
|
|
|
|
.map(|method| self.builder_impl(method))
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
|
|
let out = quote! {
|
|
|
|
impl Client {
|
|
|
|
#(#builder_methods)*
|
|
|
|
}
|
|
|
|
|
|
|
|
pub mod builder {
|
|
|
|
use super::types;
|
|
|
|
#[allow(unused_imports)]
|
2022-07-08 01:35:36 +00:00
|
|
|
use super::{
|
|
|
|
encode_path,
|
|
|
|
ByteStream,
|
|
|
|
Error,
|
|
|
|
RequestBuilderExt,
|
|
|
|
ResponseValue,
|
|
|
|
};
|
2022-07-18 18:55:21 +00:00
|
|
|
#[allow(unused_imports)]
|
|
|
|
use std::convert::TryInto;
|
2022-07-03 02:09:38 +00:00
|
|
|
|
|
|
|
#(#builder_struct)*
|
|
|
|
}
|
2022-08-27 15:23:02 +00:00
|
|
|
|
|
|
|
pub mod prelude {
|
|
|
|
pub use self::super::Client;
|
|
|
|
}
|
2022-07-03 02:09:38 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
Ok(out)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn generate_tokens_builder_separate(
|
|
|
|
&mut self,
|
|
|
|
input_methods: &[method::OperationMethod],
|
|
|
|
) -> Result<TokenStream> {
|
|
|
|
let builder_struct = input_methods
|
|
|
|
.iter()
|
|
|
|
.map(|method| self.builder_struct(method, TagStyle::Separate))
|
|
|
|
.collect::<Result<Vec<_>>>()?;
|
|
|
|
|
2022-08-27 15:23:02 +00:00
|
|
|
let (traits_and_impls, trait_preludes) =
|
|
|
|
self.builder_tags(input_methods);
|
2022-07-03 02:09:38 +00:00
|
|
|
|
|
|
|
let out = quote! {
|
|
|
|
#traits_and_impls
|
|
|
|
|
|
|
|
pub mod builder {
|
|
|
|
use super::types;
|
|
|
|
#[allow(unused_imports)]
|
2022-07-08 01:35:36 +00:00
|
|
|
use super::{
|
|
|
|
encode_path,
|
|
|
|
ByteStream,
|
|
|
|
Error,
|
|
|
|
RequestBuilderExt,
|
|
|
|
ResponseValue,
|
|
|
|
};
|
2022-07-03 02:09:38 +00:00
|
|
|
|
|
|
|
#(#builder_struct)*
|
2022-08-27 15:23:02 +00:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
pub mod prelude {
|
|
|
|
pub use super::Client;
|
|
|
|
#trait_preludes
|
2022-07-03 02:09:38 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok(out)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Render text output.
|
2021-10-17 17:40:22 +00:00
|
|
|
pub fn generate_text(&mut self, spec: &OpenAPI) -> Result<String> {
|
2022-07-03 02:09:38 +00:00
|
|
|
self.generate_text_impl(
|
|
|
|
spec,
|
|
|
|
rustfmt_wrapper::config::Config::default(),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Render text output and normalize doc comments
|
|
|
|
///
|
|
|
|
/// Requires a nightly install of `rustfmt` (even if the target project is
|
|
|
|
/// not using nightly).
|
|
|
|
pub fn generate_text_normalize_comments(
|
|
|
|
&mut self,
|
|
|
|
spec: &OpenAPI,
|
|
|
|
) -> Result<String> {
|
|
|
|
self.generate_text_impl(
|
|
|
|
spec,
|
|
|
|
rustfmt_wrapper::config::Config {
|
|
|
|
normalize_doc_attributes: Some(true),
|
|
|
|
wrap_comments: Some(true),
|
|
|
|
..Default::default()
|
|
|
|
},
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn generate_text_impl(
|
|
|
|
&mut self,
|
|
|
|
spec: &OpenAPI,
|
|
|
|
config: rustfmt_wrapper::config::Config,
|
|
|
|
) -> Result<String> {
|
2021-10-17 17:40:22 +00:00
|
|
|
let output = self.generate_tokens(spec)?;
|
|
|
|
|
2022-01-05 20:02:46 +00:00
|
|
|
// Format the file with rustfmt.
|
2022-07-03 02:09:38 +00:00
|
|
|
let content = rustfmt_wrapper::rustfmt_config(config, output).unwrap();
|
2021-10-17 17:40:22 +00:00
|
|
|
|
2022-01-05 20:02:46 +00:00
|
|
|
// Add newlines after end-braces at <= two levels of indentation.
|
2021-10-17 17:40:22 +00:00
|
|
|
Ok(if cfg!(not(windows)) {
|
2022-01-05 20:02:46 +00:00
|
|
|
let regex = regex::Regex::new(r#"(})(\n\s{0,8}[^} ])"#).unwrap();
|
2021-10-17 17:40:22 +00:00
|
|
|
regex.replace_all(&content, "$1\n$2").to_string()
|
|
|
|
} else {
|
2022-01-05 20:02:46 +00:00
|
|
|
let regex = regex::Regex::new(r#"(})(\r\n\s{0,8}[^} ])"#).unwrap();
|
2021-10-17 17:40:22 +00:00
|
|
|
regex.replace_all(&content, "$1\r\n$2").to_string()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn dependencies(&self) -> Vec<String> {
|
|
|
|
let mut deps = vec![
|
2022-07-12 17:57:06 +00:00
|
|
|
"bytes = \"1.1\"",
|
|
|
|
"futures-core = \"0.3\"",
|
2021-11-02 18:16:55 +00:00
|
|
|
"percent-encoding = \"2.1\"",
|
|
|
|
"reqwest = { version = \"0.11\", features = [\"json\", \"stream\"] }",
|
2022-07-08 01:35:36 +00:00
|
|
|
"serde = { version = \"1.0\", features = [\"derive\"] }",
|
2022-07-15 20:59:09 +00:00
|
|
|
"serde_urlencoded = \"0.7\"",
|
2021-10-17 17:40:22 +00:00
|
|
|
];
|
2022-07-12 17:57:06 +00:00
|
|
|
if self.type_space.uses_regress() {
|
2022-07-17 16:54:16 +00:00
|
|
|
deps.push("regress = \"0.4\"")
|
2022-07-12 17:57:06 +00:00
|
|
|
}
|
2021-10-17 17:40:22 +00:00
|
|
|
if self.type_space.uses_uuid() {
|
|
|
|
deps.push(
|
2022-05-20 21:52:37 +00:00
|
|
|
"uuid = { version = \">=0.8.0, <2.0.0\", features = [\"serde\", \"v4\"] }",
|
2021-10-17 17:40:22 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
if self.type_space.uses_chrono() {
|
2021-11-02 18:16:55 +00:00
|
|
|
deps.push("chrono = { version = \"0.4\", features = [\"serde\"] }")
|
|
|
|
}
|
2022-01-05 20:02:46 +00:00
|
|
|
if self.uses_futures {
|
|
|
|
deps.push("futures = \"0.3\"")
|
|
|
|
}
|
2021-11-02 18:16:55 +00:00
|
|
|
if self.type_space.uses_serde_json() {
|
|
|
|
deps.push("serde_json = \"1.0\"")
|
2021-10-17 17:40:22 +00:00
|
|
|
}
|
|
|
|
deps.sort_unstable();
|
|
|
|
deps.iter().map(ToString::to_string).collect()
|
|
|
|
}
|
|
|
|
|
2022-08-18 18:58:55 +00:00
|
|
|
// TODO deprecate?
|
2021-10-17 17:40:22 +00:00
|
|
|
pub fn get_type_space(&self) -> &TypeSpace {
|
|
|
|
&self.type_space
|
|
|
|
}
|
|
|
|
}
|
2022-07-15 04:25:48 +00:00
|
|
|
|
2022-08-29 18:16:34 +00:00
|
|
|
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(())
|
|
|
|
}
|
|
|
|
|
2022-07-15 04:25:48 +00:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use serde_json::json;
|
|
|
|
|
|
|
|
use crate::Error;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_bad_value() {
|
|
|
|
assert_eq!(
|
|
|
|
Error::BadValue("nope".to_string(), json! { "nope"},).to_string(),
|
|
|
|
"unexpected value type nope: \"nope\"",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_type_error() {
|
|
|
|
assert_eq!(
|
|
|
|
Error::UnexpectedFormat("nope".to_string()).to_string(),
|
|
|
|
"unexpected or unhandled format in the OpenAPI document nope",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_invalid_path() {
|
|
|
|
assert_eq!(
|
|
|
|
Error::InvalidPath("nope".to_string()).to_string(),
|
|
|
|
"invalid operation path nope",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_internal_error() {
|
|
|
|
assert_eq!(
|
|
|
|
Error::InternalError("nope".to_string()).to_string(),
|
|
|
|
"internal error nope",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|