improve openapi verification
This commit is contained in:
		
							parent
							
								
									2c467ea6cf
								
							
						
					
					
						commit
						19a0dc3a60
					
				
					 4 changed files with 500 additions and 64 deletions
				
			
		| 
						 | 
				
			
			@ -21,6 +21,7 @@ fn get_spec() -> eyre::Result<OpenApiV2> {
 | 
			
		|||
        .unwrap_or_else(|| OsString::from("./swagger.v1.json"));
 | 
			
		||||
    let file = std::fs::read(path)?;
 | 
			
		||||
    let spec = serde_json::from_slice::<OpenApiV2>(&file)?;
 | 
			
		||||
    spec.validate()?;
 | 
			
		||||
    Ok(spec)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -106,7 +106,7 @@ fn method_docs(op: &Operation) -> eyre::Result<String> {
 | 
			
		|||
                MaybeRef::Ref { _ref } => eyre::bail!("pipis"),
 | 
			
		||||
            };
 | 
			
		||||
            match param._in {
 | 
			
		||||
                ParameterIn::Path | ParameterIn::Body | ParameterIn::FormData => {
 | 
			
		||||
                ParameterIn::Path { param: _ } | ParameterIn::Body { schema: _ } | ParameterIn::FormData { param: _ } => {
 | 
			
		||||
                    write!(&mut out, "/// - `{}`", param.name)?;
 | 
			
		||||
                    if let Some(description) = ¶m.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;
 | 
			
		||||
    if let Some(params) = &op.parameters {
 | 
			
		||||
        for param in params {
 | 
			
		||||
            let param = match ¶m {
 | 
			
		||||
            let full_param = match ¶m {
 | 
			
		||||
                MaybeRef::Value { value } => value,
 | 
			
		||||
                MaybeRef::Ref { _ref } => eyre::bail!("todo: add deref parameters"),
 | 
			
		||||
            };
 | 
			
		||||
            match param._in {
 | 
			
		||||
                ParameterIn::Path => {
 | 
			
		||||
            match &full_param._in {
 | 
			
		||||
                ParameterIn::Path { param } => {
 | 
			
		||||
                    let type_name = param_type(¶m, false)?;
 | 
			
		||||
                    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(&type_name);
 | 
			
		||||
                }
 | 
			
		||||
                ParameterIn::Query => has_query = true,
 | 
			
		||||
                ParameterIn::Header => has_headers = true,
 | 
			
		||||
                ParameterIn::Body => {
 | 
			
		||||
                    let schema_ref = param.schema.as_ref().unwrap();
 | 
			
		||||
                    let ty = crate::schema_ref_type_name(spec, &schema_ref)?;
 | 
			
		||||
                ParameterIn::Query { param } => has_query = true,
 | 
			
		||||
                ParameterIn::Header { param }=> has_headers = true,
 | 
			
		||||
                ParameterIn::Body { schema } => {
 | 
			
		||||
                    let ty = crate::schema_ref_type_name(spec, schema)?;
 | 
			
		||||
                    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(&ty);
 | 
			
		||||
                }
 | 
			
		||||
                ParameterIn::FormData => {
 | 
			
		||||
                ParameterIn::FormData { param } => {
 | 
			
		||||
                    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>");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			@ -179,12 +178,8 @@ fn fn_args_from_op(spec: &OpenApiV2, op: &Operation) -> eyre::Result<String> {
 | 
			
		|||
    Ok(args)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn param_type(param: &Parameter, owned: bool) -> eyre::Result<String> {
 | 
			
		||||
    let _type = param
 | 
			
		||||
        ._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)
 | 
			
		||||
pub fn param_type(param: &NonBodyParameter, owned: bool) -> eyre::Result<String> {
 | 
			
		||||
    param_type_inner(¶m._type, param.format.as_deref(), param.items.as_ref(), owned)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn param_type_inner(
 | 
			
		||||
| 
						 | 
				
			
			@ -328,11 +323,11 @@ fn create_method_request(
 | 
			
		|||
                MaybeRef::Ref { _ref } => eyre::bail!("todo: add deref parameters"),
 | 
			
		||||
            };
 | 
			
		||||
            let name = crate::sanitize_ident(¶m.name);
 | 
			
		||||
            match param._in {
 | 
			
		||||
                ParameterIn::Path => (/* do nothing */),
 | 
			
		||||
                ParameterIn::Query => has_query = true,
 | 
			
		||||
                ParameterIn::Header => has_headers = true,
 | 
			
		||||
                ParameterIn::Body => {
 | 
			
		||||
            match ¶m._in {
 | 
			
		||||
                ParameterIn::Path { param } => (/* do nothing */),
 | 
			
		||||
                ParameterIn::Query { param }  => has_query = true,
 | 
			
		||||
                ParameterIn::Header { param }  => has_headers = true,
 | 
			
		||||
                ParameterIn::Body { schema } => {
 | 
			
		||||
                    if !body_method.is_empty() {
 | 
			
		||||
                        eyre::bail!("cannot have more than one body parameter");
 | 
			
		||||
                    }
 | 
			
		||||
| 
						 | 
				
			
			@ -342,7 +337,7 @@ fn create_method_request(
 | 
			
		|||
                        body_method = format!(".json(&{name})");
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                ParameterIn::FormData => {
 | 
			
		||||
                ParameterIn::FormData { param }  => {
 | 
			
		||||
                    if !body_method.is_empty() {
 | 
			
		||||
                        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> {
 | 
			
		||||
    match param._in {
 | 
			
		||||
        ParameterIn::Body => {
 | 
			
		||||
            let schema_ref = param
 | 
			
		||||
                .schema
 | 
			
		||||
                .as_ref()
 | 
			
		||||
                .ok_or_else(|| eyre::eyre!("body param did not have schema"))?;
 | 
			
		||||
            crate::schema_is_string(spec, schema_ref)
 | 
			
		||||
    match ¶m._in {
 | 
			
		||||
        ParameterIn::Body { schema } => {
 | 
			
		||||
            crate::schema_is_string(spec, schema)
 | 
			
		||||
        }
 | 
			
		||||
        _ => {
 | 
			
		||||
        ParameterIn::Path { param } | ParameterIn::Query { param } | ParameterIn::Header { param } | ParameterIn::FormData { param } => {
 | 
			
		||||
            let is_str = match param._type {
 | 
			
		||||
                Some(ParameterType::String) => true,
 | 
			
		||||
                ParameterType::String => true,
 | 
			
		||||
                _ => false,
 | 
			
		||||
            };
 | 
			
		||||
            Ok(is_str)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
use std::collections::BTreeMap;
 | 
			
		||||
use std::collections::{BTreeMap, BTreeSet};
 | 
			
		||||
 | 
			
		||||
use eyre::WrapErr;
 | 
			
		||||
use url::Url;
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
| 
						 | 
				
			
			@ -7,7 +8,7 @@ use url::Url;
 | 
			
		|||
pub struct OpenApiV2 {
 | 
			
		||||
    pub swagger: String,
 | 
			
		||||
    pub info: SpecInfo,
 | 
			
		||||
    pub host: Option<Url>,
 | 
			
		||||
    pub host: Option<String>,
 | 
			
		||||
    pub base_path: Option<String>,
 | 
			
		||||
    pub schemes: Option<Vec<String>>,
 | 
			
		||||
    pub consumes: Option<Vec<String>>,
 | 
			
		||||
| 
						 | 
				
			
			@ -22,6 +23,62 @@ pub struct OpenApiV2 {
 | 
			
		|||
    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)]
 | 
			
		||||
#[serde(rename_all(deserialize = "camelCase"))]
 | 
			
		||||
pub struct SpecInfo {
 | 
			
		||||
| 
						 | 
				
			
			@ -63,6 +120,40 @@ pub struct PathItem {
 | 
			
		|||
    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)]
 | 
			
		||||
#[serde(rename_all(deserialize = "camelCase"))]
 | 
			
		||||
pub struct Operation {
 | 
			
		||||
| 
						 | 
				
			
			@ -80,6 +171,32 @@ pub struct Operation {
 | 
			
		|||
    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)]
 | 
			
		||||
#[serde(rename_all(deserialize = "camelCase"))]
 | 
			
		||||
pub struct ExternalDocs {
 | 
			
		||||
| 
						 | 
				
			
			@ -91,13 +208,71 @@ pub struct ExternalDocs {
 | 
			
		|||
#[serde(rename_all(deserialize = "camelCase"))]
 | 
			
		||||
pub struct Parameter {
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    #[serde(rename = "in")]
 | 
			
		||||
    pub _in: ParameterIn,
 | 
			
		||||
    pub description: Option<String>,
 | 
			
		||||
    pub required: Option<bool>,
 | 
			
		||||
    pub schema: Option<MaybeRef<Schema>>,
 | 
			
		||||
    #[serde(flatten)]
 | 
			
		||||
    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")]
 | 
			
		||||
    pub _type: Option<ParameterType>,
 | 
			
		||||
    pub _type: ParameterType,
 | 
			
		||||
    pub format: Option<String>,
 | 
			
		||||
    pub allow_empty_value: Option<bool>,
 | 
			
		||||
    pub items: Option<Items>,
 | 
			
		||||
| 
						 | 
				
			
			@ -118,14 +293,48 @@ pub struct Parameter {
 | 
			
		|||
    pub multiple_of: Option<u64>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
#[serde(rename_all(deserialize = "camelCase"))]
 | 
			
		||||
pub enum ParameterIn {
 | 
			
		||||
    Path,
 | 
			
		||||
    Query,
 | 
			
		||||
    Header,
 | 
			
		||||
    Body,
 | 
			
		||||
    FormData,
 | 
			
		||||
impl NonBodyParameter {
 | 
			
		||||
    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 {
 | 
			
		||||
            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)]
 | 
			
		||||
| 
						 | 
				
			
			@ -139,6 +348,19 @@ pub enum ParameterType {
 | 
			
		|||
    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)]
 | 
			
		||||
#[serde(rename_all(deserialize = "camelCase"))]
 | 
			
		||||
pub enum CollectionFormat {
 | 
			
		||||
| 
						 | 
				
			
			@ -173,6 +395,55 @@ pub struct Items {
 | 
			
		|||
    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)]
 | 
			
		||||
#[serde(rename_all(deserialize = "camelCase"))]
 | 
			
		||||
pub struct Responses {
 | 
			
		||||
| 
						 | 
				
			
			@ -181,6 +452,28 @@ pub struct Responses {
 | 
			
		|||
    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)]
 | 
			
		||||
#[serde(rename_all(deserialize = "camelCase"))]
 | 
			
		||||
pub struct Response {
 | 
			
		||||
| 
						 | 
				
			
			@ -190,6 +483,20 @@ pub struct Response {
 | 
			
		|||
    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)]
 | 
			
		||||
#[serde(rename_all(deserialize = "camelCase"))]
 | 
			
		||||
pub struct Header {
 | 
			
		||||
| 
						 | 
				
			
			@ -215,6 +522,52 @@ pub struct Header {
 | 
			
		|||
    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)]
 | 
			
		||||
#[serde(rename_all(deserialize = "camelCase"))]
 | 
			
		||||
pub struct Tag {
 | 
			
		||||
| 
						 | 
				
			
			@ -259,6 +612,89 @@ pub struct Schema {
 | 
			
		|||
    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)]
 | 
			
		||||
#[serde(untagged)]
 | 
			
		||||
pub enum SchemaType {
 | 
			
		||||
| 
						 | 
				
			
			@ -278,6 +714,19 @@ pub enum Primitive {
 | 
			
		|||
    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)]
 | 
			
		||||
#[serde(rename_all(deserialize = "camelCase"))]
 | 
			
		||||
pub struct Xml {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -151,14 +151,13 @@ fn create_query_struct(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Re
 | 
			
		|||
            MaybeRef::Value { value } => value,
 | 
			
		||||
            MaybeRef::Ref { _ref } => eyre::bail!("todo: add deref parameters"),
 | 
			
		||||
        };
 | 
			
		||||
        if param._in == ParameterIn::Query {
 | 
			
		||||
            let ty = crate::methods::param_type(param, true)?;
 | 
			
		||||
        if let ParameterIn::Query { param: query_param} = ¶m._in {
 | 
			
		||||
            let ty = crate::methods::param_type(query_param, true)?;
 | 
			
		||||
            let field_name = crate::sanitize_ident(¶m.name);
 | 
			
		||||
            let required = param.required.unwrap_or_default();
 | 
			
		||||
            fields.push_str("pub ");
 | 
			
		||||
            fields.push_str(&field_name);
 | 
			
		||||
            fields.push_str(": ");
 | 
			
		||||
            if required {
 | 
			
		||||
            if query_param.required {
 | 
			
		||||
                fields.push_str(&ty);
 | 
			
		||||
            } else {
 | 
			
		||||
                fields.push_str("Option<");
 | 
			
		||||
| 
						 | 
				
			
			@ -168,11 +167,7 @@ fn create_query_struct(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Re
 | 
			
		|||
            fields.push_str(",\n");
 | 
			
		||||
 | 
			
		||||
            let mut handler = String::new();
 | 
			
		||||
            let ty = param
 | 
			
		||||
                ._type
 | 
			
		||||
                .as_ref()
 | 
			
		||||
                .ok_or_else(|| eyre::eyre!("no type provided for query field"))?;
 | 
			
		||||
            if required {
 | 
			
		||||
            if query_param.required {
 | 
			
		||||
                writeln!(&mut handler, "let {field_name} = &self.{field_name};")?;
 | 
			
		||||
            } else {
 | 
			
		||||
                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} {{"
 | 
			
		||||
                )?;
 | 
			
		||||
            }
 | 
			
		||||
            match ty {
 | 
			
		||||
                ParameterType::String => match param.format.as_deref() {
 | 
			
		||||
            match &query_param._type {
 | 
			
		||||
                ParameterType::String => match query_param.format.as_deref() {
 | 
			
		||||
                    Some("date-time" | "date") => {
 | 
			
		||||
                        writeln!(
 | 
			
		||||
                            &mut handler,
 | 
			
		||||
| 
						 | 
				
			
			@ -206,14 +201,14 @@ fn create_query_struct(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Re
 | 
			
		|||
                    )?;
 | 
			
		||||
                }
 | 
			
		||||
                ParameterType::Array => {
 | 
			
		||||
                    let format = param.collection_format.unwrap_or(CollectionFormat::Csv);
 | 
			
		||||
                    let item = param
 | 
			
		||||
                    let format = query_param.collection_format.unwrap_or(CollectionFormat::Csv);
 | 
			
		||||
                    let item = query_param
 | 
			
		||||
                        .items
 | 
			
		||||
                        .as_ref()
 | 
			
		||||
                        .ok_or_else(|| eyre::eyre!("array must have item type defined"))?;
 | 
			
		||||
                    let item_pusher = match item._type {
 | 
			
		||||
                        ParameterType::String => {
 | 
			
		||||
                            match param.format.as_deref() {
 | 
			
		||||
                            match query_param.format.as_deref() {
 | 
			
		||||
                                Some("date-time" | "date") => {
 | 
			
		||||
                                    "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"),
 | 
			
		||||
            }
 | 
			
		||||
            if !required {
 | 
			
		||||
            if !query_param.required {
 | 
			
		||||
                writeln!(&mut handler, "}}")?;
 | 
			
		||||
            }
 | 
			
		||||
            imp.push_str(&handler);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue