improve openapi verification
This commit is contained in:
parent
2c467ea6cf
commit
19a0dc3a60
|
@ -21,6 +21,7 @@ fn get_spec() -> eyre::Result<OpenApiV2> {
|
||||||
.unwrap_or_else(|| OsString::from("./swagger.v1.json"));
|
.unwrap_or_else(|| OsString::from("./swagger.v1.json"));
|
||||||
let file = std::fs::read(path)?;
|
let file = std::fs::read(path)?;
|
||||||
let spec = serde_json::from_slice::<OpenApiV2>(&file)?;
|
let spec = serde_json::from_slice::<OpenApiV2>(&file)?;
|
||||||
|
spec.validate()?;
|
||||||
Ok(spec)
|
Ok(spec)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -106,7 +106,7 @@ fn method_docs(op: &Operation) -> eyre::Result<String> {
|
||||||
MaybeRef::Ref { _ref } => eyre::bail!("pipis"),
|
MaybeRef::Ref { _ref } => eyre::bail!("pipis"),
|
||||||
};
|
};
|
||||||
match param._in {
|
match param._in {
|
||||||
ParameterIn::Path | ParameterIn::Body | ParameterIn::FormData => {
|
ParameterIn::Path { param: _ } | ParameterIn::Body { schema: _ } | ParameterIn::FormData { param: _ } => {
|
||||||
write!(&mut out, "/// - `{}`", param.name)?;
|
write!(&mut out, "/// - `{}`", param.name)?;
|
||||||
if let Some(description) = ¶m.description {
|
if let Some(description) = ¶m.description {
|
||||||
write!(&mut out, ": {}", description)?;
|
write!(&mut out, ": {}", description)?;
|
||||||
|
@ -141,31 +141,30 @@ fn fn_args_from_op(spec: &OpenApiV2, op: &Operation) -> eyre::Result<String> {
|
||||||
let mut has_form = false;
|
let mut has_form = false;
|
||||||
if let Some(params) = &op.parameters {
|
if let Some(params) = &op.parameters {
|
||||||
for param in params {
|
for param in params {
|
||||||
let param = match ¶m {
|
let full_param = match ¶m {
|
||||||
MaybeRef::Value { value } => value,
|
MaybeRef::Value { value } => value,
|
||||||
MaybeRef::Ref { _ref } => eyre::bail!("todo: add deref parameters"),
|
MaybeRef::Ref { _ref } => eyre::bail!("todo: add deref parameters"),
|
||||||
};
|
};
|
||||||
match param._in {
|
match &full_param._in {
|
||||||
ParameterIn::Path => {
|
ParameterIn::Path { param } => {
|
||||||
let type_name = param_type(¶m, false)?;
|
let type_name = param_type(¶m, false)?;
|
||||||
args.push_str(", ");
|
args.push_str(", ");
|
||||||
args.push_str(&crate::sanitize_ident(¶m.name));
|
args.push_str(&crate::sanitize_ident(&full_param.name));
|
||||||
args.push_str(": ");
|
args.push_str(": ");
|
||||||
args.push_str(&type_name);
|
args.push_str(&type_name);
|
||||||
}
|
}
|
||||||
ParameterIn::Query => has_query = true,
|
ParameterIn::Query { param } => has_query = true,
|
||||||
ParameterIn::Header => has_headers = true,
|
ParameterIn::Header { param }=> has_headers = true,
|
||||||
ParameterIn::Body => {
|
ParameterIn::Body { schema } => {
|
||||||
let schema_ref = param.schema.as_ref().unwrap();
|
let ty = crate::schema_ref_type_name(spec, schema)?;
|
||||||
let ty = crate::schema_ref_type_name(spec, &schema_ref)?;
|
|
||||||
args.push_str(", ");
|
args.push_str(", ");
|
||||||
args.push_str(&crate::sanitize_ident(¶m.name));
|
args.push_str(&crate::sanitize_ident(&full_param.name));
|
||||||
args.push_str(": ");
|
args.push_str(": ");
|
||||||
args.push_str(&ty);
|
args.push_str(&ty);
|
||||||
}
|
}
|
||||||
ParameterIn::FormData => {
|
ParameterIn::FormData { param } => {
|
||||||
args.push_str(", ");
|
args.push_str(", ");
|
||||||
args.push_str(&crate::sanitize_ident(¶m.name));
|
args.push_str(&crate::sanitize_ident(&full_param.name));
|
||||||
args.push_str(": Vec<u8>");
|
args.push_str(": Vec<u8>");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -179,12 +178,8 @@ fn fn_args_from_op(spec: &OpenApiV2, op: &Operation) -> eyre::Result<String> {
|
||||||
Ok(args)
|
Ok(args)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn param_type(param: &Parameter, owned: bool) -> eyre::Result<String> {
|
pub fn param_type(param: &NonBodyParameter, owned: bool) -> eyre::Result<String> {
|
||||||
let _type = param
|
param_type_inner(¶m._type, param.format.as_deref(), param.items.as_ref(), owned)
|
||||||
._type
|
|
||||||
.as_ref()
|
|
||||||
.ok_or_else(|| eyre::eyre!("no type provided for path param"))?;
|
|
||||||
param_type_inner(_type, param.format.as_deref(), param.items.as_ref(), owned)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn param_type_inner(
|
fn param_type_inner(
|
||||||
|
@ -328,11 +323,11 @@ fn create_method_request(
|
||||||
MaybeRef::Ref { _ref } => eyre::bail!("todo: add deref parameters"),
|
MaybeRef::Ref { _ref } => eyre::bail!("todo: add deref parameters"),
|
||||||
};
|
};
|
||||||
let name = crate::sanitize_ident(¶m.name);
|
let name = crate::sanitize_ident(¶m.name);
|
||||||
match param._in {
|
match ¶m._in {
|
||||||
ParameterIn::Path => (/* do nothing */),
|
ParameterIn::Path { param } => (/* do nothing */),
|
||||||
ParameterIn::Query => has_query = true,
|
ParameterIn::Query { param } => has_query = true,
|
||||||
ParameterIn::Header => has_headers = true,
|
ParameterIn::Header { param } => has_headers = true,
|
||||||
ParameterIn::Body => {
|
ParameterIn::Body { schema } => {
|
||||||
if !body_method.is_empty() {
|
if !body_method.is_empty() {
|
||||||
eyre::bail!("cannot have more than one body parameter");
|
eyre::bail!("cannot have more than one body parameter");
|
||||||
}
|
}
|
||||||
|
@ -342,7 +337,7 @@ fn create_method_request(
|
||||||
body_method = format!(".json(&{name})");
|
body_method = format!(".json(&{name})");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ParameterIn::FormData => {
|
ParameterIn::FormData { param } => {
|
||||||
if !body_method.is_empty() {
|
if !body_method.is_empty() {
|
||||||
eyre::bail!("cannot have more than one body parameter");
|
eyre::bail!("cannot have more than one body parameter");
|
||||||
}
|
}
|
||||||
|
@ -368,17 +363,13 @@ fn create_method_request(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn param_is_string(spec: &OpenApiV2, param: &Parameter) -> eyre::Result<bool> {
|
fn param_is_string(spec: &OpenApiV2, param: &Parameter) -> eyre::Result<bool> {
|
||||||
match param._in {
|
match ¶m._in {
|
||||||
ParameterIn::Body => {
|
ParameterIn::Body { schema } => {
|
||||||
let schema_ref = param
|
crate::schema_is_string(spec, schema)
|
||||||
.schema
|
|
||||||
.as_ref()
|
|
||||||
.ok_or_else(|| eyre::eyre!("body param did not have schema"))?;
|
|
||||||
crate::schema_is_string(spec, schema_ref)
|
|
||||||
}
|
}
|
||||||
_ => {
|
ParameterIn::Path { param } | ParameterIn::Query { param } | ParameterIn::Header { param } | ParameterIn::FormData { param } => {
|
||||||
let is_str = match param._type {
|
let is_str = match param._type {
|
||||||
Some(ParameterType::String) => true,
|
ParameterType::String => true,
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
Ok(is_str)
|
Ok(is_str)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use std::collections::BTreeMap;
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
|
use eyre::WrapErr;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||||
|
@ -7,7 +8,7 @@ use url::Url;
|
||||||
pub struct OpenApiV2 {
|
pub struct OpenApiV2 {
|
||||||
pub swagger: String,
|
pub swagger: String,
|
||||||
pub info: SpecInfo,
|
pub info: SpecInfo,
|
||||||
pub host: Option<Url>,
|
pub host: Option<String>,
|
||||||
pub base_path: Option<String>,
|
pub base_path: Option<String>,
|
||||||
pub schemes: Option<Vec<String>>,
|
pub schemes: Option<Vec<String>>,
|
||||||
pub consumes: Option<Vec<String>>,
|
pub consumes: Option<Vec<String>>,
|
||||||
|
@ -22,6 +23,62 @@ pub struct OpenApiV2 {
|
||||||
pub external_docs: Option<ExternalDocs>,
|
pub external_docs: Option<ExternalDocs>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl OpenApiV2 {
|
||||||
|
pub fn validate(&self) -> eyre::Result<()> {
|
||||||
|
eyre::ensure!(self.swagger == "2.0", "swagger version must be 2.0");
|
||||||
|
if let Some(host) = &self.host {
|
||||||
|
eyre::ensure!(!host.contains("://"), "openapi.host cannot contain scheme");
|
||||||
|
eyre::ensure!(!host.contains("/"), "openapi.host cannot contain path");
|
||||||
|
}
|
||||||
|
if let Some(base_path) = &self.base_path {
|
||||||
|
eyre::ensure!(
|
||||||
|
base_path.starts_with("/"),
|
||||||
|
"openapi.base_path must start with a forward slash"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(schemes) = &self.schemes {
|
||||||
|
for scheme in schemes {
|
||||||
|
eyre::ensure!(
|
||||||
|
matches!(&**scheme, "http" | "https" | "ws" | "wss"),
|
||||||
|
"openapi.schemes must only be http, https, ws, or wss"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (path, path_item) in &self.paths {
|
||||||
|
eyre::ensure!(
|
||||||
|
path.starts_with("/"),
|
||||||
|
"members of openapi.paths must start with a forward slash; {path} does not"
|
||||||
|
);
|
||||||
|
let mut operation_ids = BTreeSet::new();
|
||||||
|
path_item
|
||||||
|
.validate(&mut operation_ids)
|
||||||
|
.wrap_err_with(|| format!("OpenApiV2.paths[\"{path}\"]"))?;
|
||||||
|
}
|
||||||
|
if let Some(definitions) = &self.definitions {
|
||||||
|
for (name, schema) in definitions {
|
||||||
|
schema
|
||||||
|
.validate()
|
||||||
|
.wrap_err_with(|| format!("OpenApiV2.definitions[\"{name}\"]"))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(params) = &self.parameters {
|
||||||
|
for (name, param) in params {
|
||||||
|
param
|
||||||
|
.validate()
|
||||||
|
.wrap_err_with(|| format!("OpenApiV2.parameters[\"{name}\"]"))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(responses) = &self.responses {
|
||||||
|
for (name, responses) in responses {
|
||||||
|
responses
|
||||||
|
.validate()
|
||||||
|
.wrap_err_with(|| format!("OpenApiV2.responses[\"{name}\"]"))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||||
#[serde(rename_all(deserialize = "camelCase"))]
|
#[serde(rename_all(deserialize = "camelCase"))]
|
||||||
pub struct SpecInfo {
|
pub struct SpecInfo {
|
||||||
|
@ -63,6 +120,40 @@ pub struct PathItem {
|
||||||
pub parameters: Option<Vec<MaybeRef<Parameter>>>,
|
pub parameters: Option<Vec<MaybeRef<Parameter>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PathItem {
|
||||||
|
fn validate<'a>(&'a self, ids: &mut BTreeSet<&'a str>) -> eyre::Result<()> {
|
||||||
|
if let Some(op) = &self.get {
|
||||||
|
op.validate(ids).wrap_err("PathItem.get")?;
|
||||||
|
}
|
||||||
|
if let Some(op) = &self.put {
|
||||||
|
op.validate(ids).wrap_err("PathItem.patch")?;
|
||||||
|
}
|
||||||
|
if let Some(op) = &self.post {
|
||||||
|
op.validate(ids).wrap_err("PathItem.patch")?;
|
||||||
|
}
|
||||||
|
if let Some(op) = &self.delete {
|
||||||
|
op.validate(ids).wrap_err("PathItem.patch")?;
|
||||||
|
}
|
||||||
|
if let Some(op) = &self.options {
|
||||||
|
op.validate(ids).wrap_err("PathItem.patch")?;
|
||||||
|
}
|
||||||
|
if let Some(op) = &self.head {
|
||||||
|
op.validate(ids).wrap_err("PathItem.patch")?;
|
||||||
|
}
|
||||||
|
if let Some(op) = &self.patch {
|
||||||
|
op.validate(ids).wrap_err("PathItem.patch")?;
|
||||||
|
}
|
||||||
|
if let Some(params) = &self.parameters {
|
||||||
|
for param in params {
|
||||||
|
if let MaybeRef::Value { value } = param {
|
||||||
|
value.validate().wrap_err("PathItem.parameters")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||||
#[serde(rename_all(deserialize = "camelCase"))]
|
#[serde(rename_all(deserialize = "camelCase"))]
|
||||||
pub struct Operation {
|
pub struct Operation {
|
||||||
|
@ -80,6 +171,32 @@ pub struct Operation {
|
||||||
pub security: Option<Vec<BTreeMap<String, Vec<String>>>>,
|
pub security: Option<Vec<BTreeMap<String, Vec<String>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Operation {
|
||||||
|
fn validate<'a>(&'a self, ids: &mut BTreeSet<&'a str>) -> eyre::Result<()> {
|
||||||
|
if let Some(operation_id) = self.operation_id.as_deref() {
|
||||||
|
let is_new = ids.insert(operation_id);
|
||||||
|
eyre::ensure!(is_new, "duplicate operation id");
|
||||||
|
}
|
||||||
|
if let Some(params) = &self.parameters {
|
||||||
|
for param in params {
|
||||||
|
if let MaybeRef::Value { value } = param {
|
||||||
|
value.validate().wrap_err("Operation.parameters")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.responses.validate().wrap_err("operation response")?;
|
||||||
|
if let Some(schemes) = &self.schemes {
|
||||||
|
for scheme in schemes {
|
||||||
|
eyre::ensure!(
|
||||||
|
matches!(&**scheme, "http" | "https" | "ws" | "wss"),
|
||||||
|
"openapi.schemes must only be http, https, ws, or wss"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||||
#[serde(rename_all(deserialize = "camelCase"))]
|
#[serde(rename_all(deserialize = "camelCase"))]
|
||||||
pub struct ExternalDocs {
|
pub struct ExternalDocs {
|
||||||
|
@ -91,13 +208,71 @@ pub struct ExternalDocs {
|
||||||
#[serde(rename_all(deserialize = "camelCase"))]
|
#[serde(rename_all(deserialize = "camelCase"))]
|
||||||
pub struct Parameter {
|
pub struct Parameter {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
#[serde(rename = "in")]
|
|
||||||
pub _in: ParameterIn,
|
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub required: Option<bool>,
|
#[serde(flatten)]
|
||||||
pub schema: Option<MaybeRef<Schema>>,
|
pub _in: ParameterIn,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parameter {
|
||||||
|
fn validate(&self) -> eyre::Result<()> {
|
||||||
|
self._in.validate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||||
|
#[serde(rename_all(deserialize = "camelCase"))]
|
||||||
|
#[serde(tag = "in")]
|
||||||
|
pub enum ParameterIn {
|
||||||
|
Body {
|
||||||
|
schema: MaybeRef<Schema>,
|
||||||
|
},
|
||||||
|
Path {
|
||||||
|
#[serde(flatten)]
|
||||||
|
param: NonBodyParameter,
|
||||||
|
},
|
||||||
|
Query {
|
||||||
|
#[serde(flatten)]
|
||||||
|
param: NonBodyParameter,
|
||||||
|
},
|
||||||
|
Header {
|
||||||
|
#[serde(flatten)]
|
||||||
|
param: NonBodyParameter,
|
||||||
|
},
|
||||||
|
FormData {
|
||||||
|
#[serde(flatten)]
|
||||||
|
param: NonBodyParameter,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParameterIn {
|
||||||
|
fn validate(&self) -> eyre::Result<()> {
|
||||||
|
match self {
|
||||||
|
ParameterIn::Path { param } => {
|
||||||
|
eyre::ensure!(
|
||||||
|
param.required,
|
||||||
|
"path parameters must be required"
|
||||||
|
);
|
||||||
|
param.validate().wrap_err("path param")
|
||||||
|
},
|
||||||
|
ParameterIn::Query { param } => param.validate().wrap_err("query param"),
|
||||||
|
ParameterIn::Header { param } => param.validate().wrap_err("header param"),
|
||||||
|
ParameterIn::Body { schema } => if let MaybeRef::Value { value } = schema {
|
||||||
|
value.validate().wrap_err("body param")
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
ParameterIn::FormData { param } => param.validate().wrap_err("form param"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||||
|
#[serde(rename_all(deserialize = "camelCase"))]
|
||||||
|
pub struct NonBodyParameter {
|
||||||
|
#[serde(default)]
|
||||||
|
pub required: bool,
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
||||||
pub _type: Option<ParameterType>,
|
pub _type: ParameterType,
|
||||||
pub format: Option<String>,
|
pub format: Option<String>,
|
||||||
pub allow_empty_value: Option<bool>,
|
pub allow_empty_value: Option<bool>,
|
||||||
pub items: Option<Items>,
|
pub items: Option<Items>,
|
||||||
|
@ -118,14 +293,48 @@ pub struct Parameter {
|
||||||
pub multiple_of: Option<u64>,
|
pub multiple_of: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
impl NonBodyParameter {
|
||||||
#[serde(rename_all(deserialize = "camelCase"))]
|
fn validate(&self) -> eyre::Result<()> {
|
||||||
pub enum ParameterIn {
|
if self._type == ParameterType::Array {
|
||||||
Path,
|
eyre::ensure!(
|
||||||
Query,
|
self.items.is_some(),
|
||||||
Header,
|
"array paramters must define their item types"
|
||||||
Body,
|
);
|
||||||
FormData,
|
}
|
||||||
|
if let Some(items) = &self.items {
|
||||||
|
items.validate()?;
|
||||||
|
}
|
||||||
|
if let Some(default) = &self.default {
|
||||||
|
eyre::ensure!(
|
||||||
|
self._type.matches_value(default),
|
||||||
|
"param's default must match its type"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(_enum) = &self._enum {
|
||||||
|
for variant in _enum {
|
||||||
|
eyre::ensure!(
|
||||||
|
self._type.matches_value(variant),
|
||||||
|
"header enum variant must match its type"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.exclusive_maximum.is_some() {
|
||||||
|
eyre::ensure!(
|
||||||
|
self.maximum.is_some(),
|
||||||
|
"presence of `exclusiveMaximum` requires `maximum` be there too"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if self.exclusive_minimum.is_some() {
|
||||||
|
eyre::ensure!(
|
||||||
|
self.minimum.is_some(),
|
||||||
|
"presence of `exclusiveMinimum` requires `minimum` be there too"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(multiple_of) = self.multiple_of {
|
||||||
|
eyre::ensure!(multiple_of > 0, "multipleOf must be greater than 0");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||||
|
@ -139,6 +348,19 @@ pub enum ParameterType {
|
||||||
File,
|
File,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ParameterType {
|
||||||
|
fn matches_value(&self, value: &serde_json::Value) -> bool {
|
||||||
|
match (self, value) {
|
||||||
|
(ParameterType::String, serde_json::Value::String(_))
|
||||||
|
| (ParameterType::Number, serde_json::Value::Number(_))
|
||||||
|
| (ParameterType::Integer, serde_json::Value::Number(_))
|
||||||
|
| (ParameterType::Boolean, serde_json::Value::Bool(_))
|
||||||
|
| (ParameterType::Array, serde_json::Value::Array(_)) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Debug, PartialEq, Clone, Copy)]
|
#[derive(serde::Deserialize, Debug, PartialEq, Clone, Copy)]
|
||||||
#[serde(rename_all(deserialize = "camelCase"))]
|
#[serde(rename_all(deserialize = "camelCase"))]
|
||||||
pub enum CollectionFormat {
|
pub enum CollectionFormat {
|
||||||
|
@ -173,6 +395,55 @@ pub struct Items {
|
||||||
pub multiple_of: Option<u64>,
|
pub multiple_of: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Items {
|
||||||
|
fn validate(&self) -> eyre::Result<()> {
|
||||||
|
if self._type == ParameterType::Array {
|
||||||
|
eyre::ensure!(
|
||||||
|
self.items.is_some(),
|
||||||
|
"array paramters must define their item types"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(items) = &self.items {
|
||||||
|
items.validate()?;
|
||||||
|
}
|
||||||
|
if let Some(default) = &self.default {
|
||||||
|
match (&self._type, default) {
|
||||||
|
(ParameterType::String, serde_json::Value::String(_))
|
||||||
|
| (ParameterType::Number, serde_json::Value::Number(_))
|
||||||
|
| (ParameterType::Integer, serde_json::Value::Number(_))
|
||||||
|
| (ParameterType::Boolean, serde_json::Value::Bool(_))
|
||||||
|
| (ParameterType::Array, serde_json::Value::Array(_)) => (),
|
||||||
|
(ParameterType::File, _) => eyre::bail!("file params cannot have default value"),
|
||||||
|
_ => eyre::bail!("param's default must match its type"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if let Some(_enum) = &self._enum {
|
||||||
|
for variant in _enum {
|
||||||
|
eyre::ensure!(
|
||||||
|
self._type.matches_value(variant),
|
||||||
|
"header enum variant must match its type"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.exclusive_maximum.is_some() {
|
||||||
|
eyre::ensure!(
|
||||||
|
self.maximum.is_some(),
|
||||||
|
"presence of `exclusiveMaximum` requires `maximum` be there too"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if self.exclusive_minimum.is_some() {
|
||||||
|
eyre::ensure!(
|
||||||
|
self.minimum.is_some(),
|
||||||
|
"presence of `exclusiveMinimum` requires `minimum` be there too"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(multiple_of) = self.multiple_of {
|
||||||
|
eyre::ensure!(multiple_of > 0, "multipleOf must be greater than 0");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||||
#[serde(rename_all(deserialize = "camelCase"))]
|
#[serde(rename_all(deserialize = "camelCase"))]
|
||||||
pub struct Responses {
|
pub struct Responses {
|
||||||
|
@ -181,6 +452,28 @@ pub struct Responses {
|
||||||
pub http_codes: BTreeMap<String, MaybeRef<Response>>,
|
pub http_codes: BTreeMap<String, MaybeRef<Response>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Responses {
|
||||||
|
fn validate(&self) -> eyre::Result<()> {
|
||||||
|
if self.default.is_none() && self.http_codes.is_empty() {
|
||||||
|
eyre::bail!("must have at least one response");
|
||||||
|
}
|
||||||
|
if let Some(MaybeRef::Value { value }) = &self.default {
|
||||||
|
value.validate().wrap_err("default response")?;
|
||||||
|
}
|
||||||
|
for (code, response) in &self.http_codes {
|
||||||
|
let code_int = code.parse::<u16>().wrap_err("http code must be a number")?;
|
||||||
|
eyre::ensure!(
|
||||||
|
code_int >= 100 && code_int < 1000,
|
||||||
|
"invalid http status code"
|
||||||
|
);
|
||||||
|
if let MaybeRef::Value { value } = response {
|
||||||
|
value.validate().wrap_err_with(|| code.to_string())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||||
#[serde(rename_all(deserialize = "camelCase"))]
|
#[serde(rename_all(deserialize = "camelCase"))]
|
||||||
pub struct Response {
|
pub struct Response {
|
||||||
|
@ -190,6 +483,20 @@ pub struct Response {
|
||||||
pub examples: Option<BTreeMap<String, serde_json::Value>>,
|
pub examples: Option<BTreeMap<String, serde_json::Value>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Response {
|
||||||
|
fn validate(&self) -> eyre::Result<()> {
|
||||||
|
if let Some(headers) = &self.headers {
|
||||||
|
for (_, value) in headers {
|
||||||
|
value.validate().wrap_err("response header")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(MaybeRef::Value { value }) = &self.schema {
|
||||||
|
value.validate().wrap_err("response")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||||
#[serde(rename_all(deserialize = "camelCase"))]
|
#[serde(rename_all(deserialize = "camelCase"))]
|
||||||
pub struct Header {
|
pub struct Header {
|
||||||
|
@ -215,6 +522,52 @@ pub struct Header {
|
||||||
pub multiple_of: Option<u64>,
|
pub multiple_of: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Header {
|
||||||
|
fn validate(&self) -> eyre::Result<()> {
|
||||||
|
if self._type == ParameterType::Array {
|
||||||
|
eyre::ensure!(
|
||||||
|
self.items.is_some(),
|
||||||
|
"array paramters must define their item types"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(default) = &self.default {
|
||||||
|
match (&self._type, default) {
|
||||||
|
(ParameterType::String, serde_json::Value::String(_))
|
||||||
|
| (ParameterType::Number, serde_json::Value::Number(_))
|
||||||
|
| (ParameterType::Integer, serde_json::Value::Number(_))
|
||||||
|
| (ParameterType::Boolean, serde_json::Value::Bool(_))
|
||||||
|
| (ParameterType::Array, serde_json::Value::Array(_)) => (),
|
||||||
|
(ParameterType::File, _) => eyre::bail!("file params cannot have default value"),
|
||||||
|
_ => eyre::bail!("param's default must match its type"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if let Some(_enum) = &self._enum {
|
||||||
|
for variant in _enum {
|
||||||
|
eyre::ensure!(
|
||||||
|
self._type.matches_value(variant),
|
||||||
|
"header enum variant must match its type"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.exclusive_maximum.is_some() {
|
||||||
|
eyre::ensure!(
|
||||||
|
self.maximum.is_some(),
|
||||||
|
"presence of `exclusiveMaximum` requires `maximum` be there too"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if self.exclusive_minimum.is_some() {
|
||||||
|
eyre::ensure!(
|
||||||
|
self.minimum.is_some(),
|
||||||
|
"presence of `exclusiveMinimum` requires `minimum` be there too"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(multiple_of) = self.multiple_of {
|
||||||
|
eyre::ensure!(multiple_of > 0, "multipleOf must be greater than 0");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||||
#[serde(rename_all(deserialize = "camelCase"))]
|
#[serde(rename_all(deserialize = "camelCase"))]
|
||||||
pub struct Tag {
|
pub struct Tag {
|
||||||
|
@ -259,6 +612,89 @@ pub struct Schema {
|
||||||
pub example: Option<serde_json::Value>,
|
pub example: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Schema {
|
||||||
|
fn validate(&self) -> eyre::Result<()> {
|
||||||
|
if let Some(_type) = &self._type {
|
||||||
|
match _type {
|
||||||
|
SchemaType::One(_type) => {
|
||||||
|
if _type == &Primitive::Array {
|
||||||
|
eyre::ensure!(
|
||||||
|
self.items.is_some(),
|
||||||
|
"array paramters must define their item types"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(default) = &self.default {
|
||||||
|
eyre::ensure!(
|
||||||
|
_type.matches_value(default),
|
||||||
|
"param's default must match its type"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(_enum) = &self._enum {
|
||||||
|
for variant in _enum {
|
||||||
|
eyre::ensure!(
|
||||||
|
_type.matches_value(variant),
|
||||||
|
"schema enum variant must match its type"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SchemaType::List(_) => {
|
||||||
|
eyre::bail!("sum types not supported");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eyre::ensure!(
|
||||||
|
self.default.is_none(),
|
||||||
|
"cannot have default when no type is specified"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(items) = &self.items {
|
||||||
|
if let MaybeRef::Value { value } = &**items {
|
||||||
|
value.validate()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(required) = &self.required {
|
||||||
|
let properties = self.properties.as_ref().ok_or_else(|| {
|
||||||
|
eyre::eyre!("required properties listed but no properties present")
|
||||||
|
})?;
|
||||||
|
for i in required {
|
||||||
|
eyre::ensure!(
|
||||||
|
properties.contains_key(i),
|
||||||
|
"property \"{i}\" required, but is not defined"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(properties) = &self.properties {
|
||||||
|
for (_, schema) in properties {
|
||||||
|
if let MaybeRef::Value { value } = schema {
|
||||||
|
value.validate().wrap_err("schema properties")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(additional_properties) = &self.additional_properties {
|
||||||
|
if let MaybeRef::Value { value } = &**additional_properties {
|
||||||
|
value.validate().wrap_err("schema additional properties")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.exclusive_maximum.is_some() {
|
||||||
|
eyre::ensure!(
|
||||||
|
self.maximum.is_some(),
|
||||||
|
"presence of `exclusiveMaximum` requires `maximum` be there too"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if self.exclusive_minimum.is_some() {
|
||||||
|
eyre::ensure!(
|
||||||
|
self.minimum.is_some(),
|
||||||
|
"presence of `exclusiveMinimum` requires `minimum` be there too"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(multiple_of) = self.multiple_of {
|
||||||
|
eyre::ensure!(multiple_of > 0, "multipleOf must be greater than 0");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum SchemaType {
|
pub enum SchemaType {
|
||||||
|
@ -278,6 +714,19 @@ pub enum Primitive {
|
||||||
String,
|
String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Primitive {
|
||||||
|
fn matches_value(&self, value: &serde_json::Value) -> bool {
|
||||||
|
match (self, value) {
|
||||||
|
(Primitive::String, serde_json::Value::String(_))
|
||||||
|
| (Primitive::Number, serde_json::Value::Number(_))
|
||||||
|
| (Primitive::Integer, serde_json::Value::Number(_))
|
||||||
|
| (Primitive::Boolean, serde_json::Value::Bool(_))
|
||||||
|
| (Primitive::Array, serde_json::Value::Array(_)) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||||
#[serde(rename_all(deserialize = "camelCase"))]
|
#[serde(rename_all(deserialize = "camelCase"))]
|
||||||
pub struct Xml {
|
pub struct Xml {
|
||||||
|
|
|
@ -151,14 +151,13 @@ fn create_query_struct(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Re
|
||||||
MaybeRef::Value { value } => value,
|
MaybeRef::Value { value } => value,
|
||||||
MaybeRef::Ref { _ref } => eyre::bail!("todo: add deref parameters"),
|
MaybeRef::Ref { _ref } => eyre::bail!("todo: add deref parameters"),
|
||||||
};
|
};
|
||||||
if param._in == ParameterIn::Query {
|
if let ParameterIn::Query { param: query_param} = ¶m._in {
|
||||||
let ty = crate::methods::param_type(param, true)?;
|
let ty = crate::methods::param_type(query_param, true)?;
|
||||||
let field_name = crate::sanitize_ident(¶m.name);
|
let field_name = crate::sanitize_ident(¶m.name);
|
||||||
let required = param.required.unwrap_or_default();
|
|
||||||
fields.push_str("pub ");
|
fields.push_str("pub ");
|
||||||
fields.push_str(&field_name);
|
fields.push_str(&field_name);
|
||||||
fields.push_str(": ");
|
fields.push_str(": ");
|
||||||
if required {
|
if query_param.required {
|
||||||
fields.push_str(&ty);
|
fields.push_str(&ty);
|
||||||
} else {
|
} else {
|
||||||
fields.push_str("Option<");
|
fields.push_str("Option<");
|
||||||
|
@ -168,11 +167,7 @@ fn create_query_struct(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Re
|
||||||
fields.push_str(",\n");
|
fields.push_str(",\n");
|
||||||
|
|
||||||
let mut handler = String::new();
|
let mut handler = String::new();
|
||||||
let ty = param
|
if query_param.required {
|
||||||
._type
|
|
||||||
.as_ref()
|
|
||||||
.ok_or_else(|| eyre::eyre!("no type provided for query field"))?;
|
|
||||||
if required {
|
|
||||||
writeln!(&mut handler, "let {field_name} = &self.{field_name};")?;
|
writeln!(&mut handler, "let {field_name} = &self.{field_name};")?;
|
||||||
} else {
|
} else {
|
||||||
writeln!(
|
writeln!(
|
||||||
|
@ -180,8 +175,8 @@ fn create_query_struct(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Re
|
||||||
"if let Some({field_name}) = &self.{field_name} {{"
|
"if let Some({field_name}) = &self.{field_name} {{"
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
match ty {
|
match &query_param._type {
|
||||||
ParameterType::String => match param.format.as_deref() {
|
ParameterType::String => match query_param.format.as_deref() {
|
||||||
Some("date-time" | "date") => {
|
Some("date-time" | "date") => {
|
||||||
writeln!(
|
writeln!(
|
||||||
&mut handler,
|
&mut handler,
|
||||||
|
@ -206,14 +201,14 @@ fn create_query_struct(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Re
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
ParameterType::Array => {
|
ParameterType::Array => {
|
||||||
let format = param.collection_format.unwrap_or(CollectionFormat::Csv);
|
let format = query_param.collection_format.unwrap_or(CollectionFormat::Csv);
|
||||||
let item = param
|
let item = query_param
|
||||||
.items
|
.items
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or_else(|| eyre::eyre!("array must have item type defined"))?;
|
.ok_or_else(|| eyre::eyre!("array must have item type defined"))?;
|
||||||
let item_pusher = match item._type {
|
let item_pusher = match item._type {
|
||||||
ParameterType::String => {
|
ParameterType::String => {
|
||||||
match param.format.as_deref() {
|
match query_param.format.as_deref() {
|
||||||
Some("date-time" | "date") => {
|
Some("date-time" | "date") => {
|
||||||
"write!(f, \"{{date}}\", item.format(&time::format_description::well_known::Rfc3339).unwrap())?;"
|
"write!(f, \"{{date}}\", item.format(&time::format_description::well_known::Rfc3339).unwrap())?;"
|
||||||
},
|
},
|
||||||
|
@ -280,7 +275,7 @@ fn create_query_struct(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Re
|
||||||
}
|
}
|
||||||
ParameterType::File => eyre::bail!("cannot send file in query"),
|
ParameterType::File => eyre::bail!("cannot send file in query"),
|
||||||
}
|
}
|
||||||
if !required {
|
if !query_param.required {
|
||||||
writeln!(&mut handler, "}}")?;
|
writeln!(&mut handler, "}}")?;
|
||||||
}
|
}
|
||||||
imp.push_str(&handler);
|
imp.push_str(&handler);
|
||||||
|
|
Loading…
Reference in a new issue