use std::collections::{BTreeMap, BTreeSet}; use eyre::WrapErr; use url::Url; trait JsonDeref: std::any::Any { fn deref_any<'a>(&'a self, path: &str) -> eyre::Result<&'a dyn std::any::Any>; } impl JsonDeref for bool { fn deref_any<'a>(&'a self, path: &str) -> eyre::Result<&'a dyn std::any::Any> { if path.is_empty() { Ok(self) } else { Err(eyre::eyre!("not found")) } } } impl JsonDeref for u64 { fn deref_any<'a>(&'a self, path: &str) -> eyre::Result<&'a dyn std::any::Any> { if path.is_empty() { Ok(self) } else { Err(eyre::eyre!("not found")) } } } impl JsonDeref for f64 { fn deref_any<'a>(&'a self, path: &str) -> eyre::Result<&'a dyn std::any::Any> { if path.is_empty() { Ok(self) } else { Err(eyre::eyre!("not found")) } } } impl JsonDeref for String { fn deref_any<'a>(&'a self, path: &str) -> eyre::Result<&'a dyn std::any::Any> { if path.is_empty() { Ok(self) } else { Err(eyre::eyre!("not found")) } } } impl JsonDeref for Url { fn deref_any<'a>(&'a self, path: &str) -> eyre::Result<&'a dyn std::any::Any> { if path.is_empty() { Ok(self) } else { Err(eyre::eyre!("not found")) } } } impl JsonDeref for Option { fn deref_any<'a>(&'a self, path: &str) -> eyre::Result<&'a dyn std::any::Any> { match self { Some(x) => x.deref_any(path), None => Err(eyre::eyre!("not found")), } } } impl JsonDeref for Box { fn deref_any<'a>(&'a self, path: &str) -> eyre::Result<&'a dyn std::any::Any> { T::deref_any(&**self, path) } } impl JsonDeref for Vec { fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { if path.is_empty() { return Ok(self); } let (head, tail) = path.split_once("/").unwrap_or((path, "")); let idx = head.parse::().wrap_err("not found")?; let value = self.get(idx).ok_or_else(|| eyre::eyre!("not found"))?; value.deref_any(tail) } } impl JsonDeref for BTreeMap { fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { if path.is_empty() { return Ok(self); } let (head, tail) = path.split_once("/").unwrap_or((path, "")); let value = self.get(head).ok_or_else(|| eyre::eyre!("not found"))?; value.deref_any(tail) } } impl JsonDeref for serde_json::Value { fn deref_any<'a>(&'a self, path: &str) -> eyre::Result<&'a dyn std::any::Any> { match self { serde_json::Value::Null => eyre::bail!("not found"), serde_json::Value::Bool(b) => b.deref_any(path), serde_json::Value::Number(x) => x.deref_any(path), serde_json::Value::String(s) => s.deref_any(path), serde_json::Value::Array(list) => list.deref_any(path), serde_json::Value::Object(map) => map.deref_any(path), } } } impl JsonDeref for serde_json::Map { fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { if path.is_empty() { return Ok(self); } let (head, tail) = path.split_once("/").unwrap_or((path, "")); let value = self.get(head).ok_or_else(|| eyre::eyre!("not found"))?; value.deref_any(tail) } } impl JsonDeref for serde_json::Number { fn deref_any<'a>(&'a self, path: &str) -> eyre::Result<&'a dyn std::any::Any> { if path.is_empty() { Ok(self) } else { eyre::bail!("not found") } } } #[derive(serde::Deserialize, Debug, PartialEq)] #[serde(rename_all(deserialize = "camelCase"))] pub struct OpenApiV2 { pub swagger: String, pub info: SpecInfo, pub host: Option, pub base_path: Option, pub schemes: Option>, pub consumes: Option>, pub produces: Option>, pub paths: BTreeMap, pub definitions: Option>, pub parameters: Option>, pub responses: Option>, pub security_definitions: Option>, pub security: Option>>>, pub tags: Option>, pub external_docs: Option, } impl JsonDeref for OpenApiV2 { fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { let path = path .strip_prefix("#/") .ok_or_else(|| eyre::eyre!("invalid ref prefix"))?; if path.is_empty() { return Ok(self); } let (head, tail) = path.split_once("/").unwrap_or((path, "")); match head { "swagger" => self.swagger.deref_any(tail), "info" => self.info.deref_any(tail), "host" => self.host.deref_any(tail), "base_path" => self.base_path.deref_any(tail), "schemes" => self.schemes.deref_any(tail), "consumes" => self.consumes.deref_any(tail), "produces" => self.produces.deref_any(tail), "paths" => self.paths.deref_any(tail), "definitions" => self.definitions.deref_any(tail), "parameters" => self.parameters.deref_any(tail), "responses" => self.responses.deref_any(tail), "security_definitions" => self.security_definitions.deref_any(tail), "security" => self.security.deref_any(tail), "tags" => self.tags.deref_any(tail), "external_docs" => self.external_docs.deref_any(tail), _ => eyre::bail!("not found: {head}"), } } } 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(()) } pub fn deref(&self, path: &str) -> eyre::Result<&T> { self.deref_any(path).and_then(|a| { a.downcast_ref::() .ok_or_else(|| eyre::eyre!("incorrect type found at reference")) }) } } #[derive(serde::Deserialize, Debug, PartialEq)] #[serde(rename_all(deserialize = "camelCase"))] pub struct SpecInfo { pub title: String, pub description: Option, pub terms_of_service: Option, pub contact: Option, pub license: Option, pub version: String, } impl JsonDeref for SpecInfo { fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { if path.is_empty() { return Ok(self); } let (head, tail) = path.split_once("/").unwrap_or((path, "")); match head { "title" => self.title.deref_any(tail), "description" => self.description.deref_any(tail), "terms_of_service" => self.terms_of_service.deref_any(tail), "contact" => self.contact.deref_any(tail), "license" => self.license.deref_any(tail), "version" => self.version.deref_any(tail), _ => eyre::bail!("not found: {head}"), } } } #[derive(serde::Deserialize, Debug, PartialEq)] #[serde(rename_all(deserialize = "camelCase"))] pub struct Contact { pub name: Option, pub url: Option, pub email: Option, } impl JsonDeref for Contact { fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { if path.is_empty() { return Ok(self); } let (head, tail) = path.split_once("/").unwrap_or((path, "")); match head { "name" => self.name.deref_any(tail), "url" => self.url.deref_any(tail), "email" => self.email.deref_any(tail), _ => eyre::bail!("not found: {head}"), } } } #[derive(serde::Deserialize, Debug, PartialEq)] #[serde(rename_all(deserialize = "camelCase"))] pub struct License { pub name: String, pub url: Option, } impl JsonDeref for License { fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { if path.is_empty() { return Ok(self); } let (head, tail) = path.split_once("/").unwrap_or((path, "")); match head { "name" => self.name.deref_any(tail), "url" => self.url.deref_any(tail), _ => eyre::bail!("not found: {head}"), } } } #[derive(serde::Deserialize, Debug, PartialEq)] #[serde(rename_all(deserialize = "camelCase"))] pub struct PathItem { #[serde(rename = "$ref")] pub _ref: Option, pub get: Option, pub put: Option, pub post: Option, pub delete: Option, pub options: Option, pub head: Option, pub patch: Option, pub parameters: Option>>, } impl JsonDeref for PathItem { fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { if path.is_empty() { return Ok(self); } let (head, tail) = path.split_once("/").unwrap_or((path, "")); match head { "$ref" => self._ref.deref_any(tail), "get" => self.get.deref_any(tail), "put" => self.put.deref_any(tail), "post" => self.post.deref_any(tail), "delete" => self.delete.deref_any(tail), "options" => self.options.deref_any(tail), "head" => self.head.deref_any(tail), "patch" => self.patch.deref_any(tail), "parameters" => self.parameters.deref_any(tail), _ => eyre::bail!("not found: {head}"), } } } 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 { pub tags: Option>, pub summary: Option, pub description: Option, pub external_docs: Option, pub operation_id: Option, pub consumes: Option>, pub produces: Option>, pub parameters: Option>>, pub responses: Responses, pub schemes: Option>, pub deprecated: Option, pub security: Option>>>, } impl JsonDeref for Operation { fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { if path.is_empty() { return Ok(self); } let (head, tail) = path.split_once("/").unwrap_or((path, "")); match head { "tags" => self.tags.deref_any(tail), "summary" => self.summary.deref_any(tail), "description" => self.description.deref_any(tail), "external_docs" => self.external_docs.deref_any(tail), "operation_id" => self.operation_id.deref_any(tail), "consumes" => self.consumes.deref_any(tail), "produces" => self.produces.deref_any(tail), "parameters" => self.parameters.deref_any(tail), "responses" => self.responses.deref_any(tail), "schemes" => self.schemes.deref_any(tail), "deprecated" => self.deprecated.deref_any(tail), "security" => self.security.deref_any(tail), _ => eyre::bail!("not found: {head}"), } } } 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 { pub description: Option, pub url: Url, } impl JsonDeref for ExternalDocs { fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { if path.is_empty() { return Ok(self); } let (head, tail) = path.split_once("/").unwrap_or((path, "")); match head { "description" => self.description.deref_any(tail), "url" => self.url.deref_any(tail), _ => eyre::bail!("not found: {head}"), } } } #[derive(serde::Deserialize, Debug, PartialEq)] #[serde(rename_all(deserialize = "camelCase"))] pub struct Parameter { pub name: String, pub description: Option, #[serde(flatten)] pub _in: ParameterIn, } impl JsonDeref for Parameter { fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { if path.is_empty() { return Ok(self); } let (head, tail) = path.split_once("/").unwrap_or((path, "")); match head { "name" => self.name.deref_any(tail), "description" => self.description.deref_any(tail), "in" => { if tail.is_empty() { Ok(match &self._in { ParameterIn::Body { schema: _ } => &"body" as _, ParameterIn::Path { param: _ } => &"path" as _, ParameterIn::Query { param: _ } => &"query" as _, ParameterIn::Header { param: _ } => &"header" as _, ParameterIn::FormData { param: _ } => &"formData" as _, }) } else { eyre::bail!("not found") } } _ => self._in.deref_any(path), } } } 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, }, Path { #[serde(flatten)] param: NonBodyParameter, }, Query { #[serde(flatten)] param: NonBodyParameter, }, Header { #[serde(flatten)] param: NonBodyParameter, }, FormData { #[serde(flatten)] param: NonBodyParameter, }, } impl JsonDeref for ParameterIn { fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { let (head, tail) = path.split_once("/").unwrap_or((path, "")); match self { ParameterIn::Body { schema } => match head { "schema" => schema.deref_any(tail), _ => eyre::bail!("not found: {head}"), }, ParameterIn::Path { param } | ParameterIn::Query { param } | ParameterIn::Header { param } | ParameterIn::FormData { param } => param.deref_any(path), } } } 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: ParameterType, pub format: Option, pub allow_empty_value: Option, pub items: Option, pub collection_format: Option, pub default: Option, pub maximum: Option, pub exclusive_maximum: Option, pub minimum: Option, pub exclusive_minimum: Option, pub max_length: Option, pub min_length: Option, pub pattern: Option, // should be regex pub max_items: Option, pub min_items: Option, pub unique_items: Option, #[serde(rename = "enum")] pub _enum: Option>, pub multiple_of: Option, } impl JsonDeref for NonBodyParameter { fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { if path.is_empty() { return Ok(self); } let (head, tail) = path.split_once("/").unwrap_or((path, "")); match head { "required" => self.required.deref_any(tail), "type" => self._type.deref_any(tail), "format" => self.format.deref_any(tail), "allow_empty_value" => self.allow_empty_value.deref_any(tail), "items" => self.items.deref_any(tail), "collection_format" => self.collection_format.deref_any(tail), "default" => self.default.deref_any(tail), "maximum" => self.maximum.deref_any(tail), "exclusive_maximum" => self.exclusive_maximum.deref_any(tail), "minimum" => self.minimum.deref_any(tail), "exclusive_minimum" => self.exclusive_minimum.deref_any(tail), "max_length" => self.max_length.deref_any(tail), "min_length" => self.min_length.deref_any(tail), "pattern" => self.pattern.deref_any(tail), // should be regex "max_items" => self.max_items.deref_any(tail), "min_items" => self.min_items.deref_any(tail), "unique_items" => self.unique_items.deref_any(tail), "enum" => self._enum.deref_any(tail), "multiple_of" => self.multiple_of.deref_any(tail), _ => eyre::bail!("not found: {head}"), } } } 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)] #[serde(rename_all(deserialize = "camelCase"))] pub enum ParameterType { String, Number, Integer, Boolean, Array, File, } impl JsonDeref for ParameterType { fn deref_any<'a>(&'a self, path: &str) -> eyre::Result<&'a dyn std::any::Any> { if path.is_empty() { Ok(self) } else { Err(eyre::eyre!("not found")) } } } 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 { Csv, Ssv, Tsv, Pipes, Multi, } impl JsonDeref for CollectionFormat { fn deref_any<'a>(&'a self, path: &str) -> eyre::Result<&'a dyn std::any::Any> { if path.is_empty() { Ok(self) } else { Err(eyre::eyre!("not found")) } } } #[derive(serde::Deserialize, Debug, PartialEq)] #[serde(rename_all(deserialize = "camelCase"))] pub struct Items { #[serde(rename = "type")] pub _type: ParameterType, pub format: Option, pub items: Option>, pub collection_format: Option, pub default: Option, pub maximum: Option, pub exclusive_maximum: Option, pub minimum: Option, pub exclusive_minimum: Option, pub max_length: Option, pub min_length: Option, pub pattern: Option, // should be regex pub max_items: Option, pub min_items: Option, pub unique_items: Option, #[serde(rename = "enum")] pub _enum: Option>, pub multiple_of: Option, } impl JsonDeref for Items { fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { if path.is_empty() { return Ok(self); } let (head, tail) = path.split_once("/").unwrap_or((path, "")); match head { "type" => self._type.deref_any(tail), "format" => self.format.deref_any(tail), "items" => self.items.deref_any(tail), "collection_format" => self.collection_format.deref_any(tail), "default" => self.default.deref_any(tail), "maximum" => self.maximum.deref_any(tail), "exclusive_maximum" => self.exclusive_maximum.deref_any(tail), "minimum" => self.minimum.deref_any(tail), "exclusive_minimum" => self.exclusive_minimum.deref_any(tail), "max_length" => self.max_length.deref_any(tail), "min_length" => self.min_length.deref_any(tail), "pattern" => self.pattern.deref_any(tail), // should be regex "max_items" => self.max_items.deref_any(tail), "min_items" => self.min_items.deref_any(tail), "unique_items" => self.unique_items.deref_any(tail), "enum" => self._enum.deref_any(tail), "multiple_of" => self.multiple_of.deref_any(tail), _ => eyre::bail!("not found: {head}"), } } } 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 { pub default: Option>, #[serde(flatten)] pub http_codes: BTreeMap>, } impl JsonDeref for Responses { fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { if path.is_empty() { return Ok(self); } let (head, tail) = path.split_once("/").unwrap_or((path, "")); match head { "default" => self.default.deref_any(tail), code => self .http_codes .get(code) .map(|r| r as _) .ok_or_else(|| eyre::eyre!("not found")), } } } 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::().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 { pub description: String, pub schema: Option>, pub headers: Option>, pub examples: Option>, } impl JsonDeref for Response { fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { if path.is_empty() { return Ok(self); } let (head, tail) = path.split_once("/").unwrap_or((path, "")); match head { "description" => self.description.deref_any(tail), "schema" => self.schema.deref_any(tail), "headers" => self.headers.deref_any(tail), "examples" => self.examples.deref_any(tail), _ => eyre::bail!("not found: {head}"), } } } 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 { pub description: Option, #[serde(rename = "type")] pub _type: ParameterType, pub format: Option, pub items: Option, pub collection_format: Option, pub default: Option, pub maximum: Option, pub exclusive_maximum: Option, pub minimum: Option, pub exclusive_minimum: Option, pub max_length: Option, pub min_length: Option, pub pattern: Option, // should be regex pub max_items: Option, pub min_items: Option, pub unique_items: Option, #[serde(rename = "enum")] pub _enum: Option>, pub multiple_of: Option, } impl JsonDeref for Header { fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { if path.is_empty() { return Ok(self); } let (head, tail) = path.split_once("/").unwrap_or((path, "")); match head { "description" => self.description.deref_any(tail), "type" => self._type.deref_any(tail), "format" => self.format.deref_any(tail), "items" => self.items.deref_any(tail), "collection_format" => self.collection_format.deref_any(tail), "default" => self.default.deref_any(tail), "maximum" => self.maximum.deref_any(tail), "exclusive_maximum" => self.exclusive_maximum.deref_any(tail), "minimum" => self.minimum.deref_any(tail), "exclusive_minimum" => self.exclusive_minimum.deref_any(tail), "max_length" => self.max_length.deref_any(tail), "min_length" => self.min_length.deref_any(tail), "pattern" => self.pattern.deref_any(tail), // should be regex "max_items" => self.max_items.deref_any(tail), "min_items" => self.min_items.deref_any(tail), "unique_items" => self.unique_items.deref_any(tail), "enum" => self._enum.deref_any(tail), "multiple_of" => self.multiple_of.deref_any(tail), _ => eyre::bail!("not found: {head}"), } } } 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 { pub name: String, pub description: Option, pub external_docs: Option, } impl JsonDeref for Tag { fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { if path.is_empty() { return Ok(self); } let (head, tail) = path.split_once("/").unwrap_or((path, "")); match head { "name" => self.name.deref_any(tail), "description" => self.description.deref_any(tail), "external_docs" => self.external_docs.deref_any(tail), _ => eyre::bail!("not found: {head}"), } } } #[derive(serde::Deserialize, Debug, PartialEq)] #[serde(rename_all(deserialize = "camelCase"))] pub struct Schema { pub format: Option, pub title: Option, pub description: Option, pub default: Option, pub multiple_of: Option, pub maximum: Option, pub exclusive_maximum: Option, pub minimum: Option, pub exclusive_minimum: Option, pub max_length: Option, pub min_length: Option, pub pattern: Option, // should be regex pub max_items: Option, pub min_items: Option, pub unique_items: Option, pub max_properties: Option, pub min_properties: Option, pub required: Option>, #[serde(rename = "enum")] pub _enum: Option>, #[serde(rename = "type")] pub _type: Option, pub properties: Option>>, pub additional_properties: Option>>, pub items: Option>>, pub discriminator: Option, pub read_only: Option, pub xml: Option, pub external_docs: Option, pub example: Option, #[serde(flatten)] pub extensions: BTreeMap, } impl JsonDeref for Schema { fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { if path.is_empty() { return Ok(self); } let (head, tail) = path.split_once("/").unwrap_or((path, "")); match head { "format" => self.format.deref_any(tail), "title" => self.title.deref_any(tail), "description" => self.description.deref_any(tail), "default" => self.default.deref_any(tail), "multiple_of" => self.multiple_of.deref_any(tail), "maximum" => self.maximum.deref_any(tail), "exclusive_maximum" => self.exclusive_maximum.deref_any(tail), "minimum" => self.minimum.deref_any(tail), "exclusive_minimum" => self.exclusive_minimum.deref_any(tail), "max_length" => self.max_length.deref_any(tail), "min_length" => self.min_length.deref_any(tail), "pattern" => self.pattern.deref_any(tail), // should be regex "max_items" => self.max_items.deref_any(tail), "min_items" => self.min_items.deref_any(tail), "unique_items" => self.unique_items.deref_any(tail), "max_properties" => self.max_properties.deref_any(tail), "min_properties" => self.min_properties.deref_any(tail), "required" => self.required.deref_any(tail), "enum" => self._enum.deref_any(tail), "type" => self._type.deref_any(tail), "properties" => self.properties.deref_any(tail), "additional_properties" => self.additional_properties.deref_any(tail), "items" => self.items.deref_any(tail), "discriminator" => self.discriminator.deref_any(tail), "read_only" => self.read_only.deref_any(tail), "xml" => self.xml.deref_any(tail), "external_docs" => self.external_docs.deref_any(tail), "example" => self.example.deref_any(tail), _ => eyre::bail!("not found: {head}"), } } } 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 { One(Primitive), List(Vec), } impl JsonDeref for SchemaType { fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { match self { SchemaType::One(i) => i.deref_any(path), SchemaType::List(list) => list.deref_any(path), } } } #[derive(serde::Deserialize, Debug, PartialEq)] #[serde(rename_all(deserialize = "camelCase"))] pub enum Primitive { Array, Boolean, Integer, Number, Null, Object, String, } impl JsonDeref for Primitive { fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { if path.is_empty() { Ok(self) } else { Err(eyre::eyre!("not found")) } } } 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 { pub name: Option, pub namespace: Option, pub prefix: Option, pub attribute: Option, pub wrapped: Option, } impl JsonDeref for Xml { fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { if path.is_empty() { return Ok(self); } let (head, tail) = path.split_once("/").unwrap_or((path, "")); match head { "name" => self.name.deref_any(tail), "namespace" => self.namespace.deref_any(tail), "prefix" => self.prefix.deref_any(tail), "attribute" => self.attribute.deref_any(tail), "wrapped" => self.wrapped.deref_any(tail), _ => eyre::bail!("not found: {head}"), } } } #[derive(serde::Deserialize, Debug, PartialEq)] #[serde(rename_all(deserialize = "camelCase"))] pub struct SecurityScheme { #[serde(flatten)] pub _type: SecurityType, pub description: Option, } impl JsonDeref for SecurityScheme { fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { if path.is_empty() { return Ok(self); } let (head, tail) = path.split_once("/").unwrap_or((path, "")); match head { "type" => self._type.deref_any(tail), "description" => self.description.deref_any(tail), _ => eyre::bail!("not found: {head}"), } } } #[derive(serde::Deserialize, Debug, PartialEq)] #[serde(rename_all(deserialize = "camelCase"), tag = "type")] pub enum SecurityType { Basic, ApiKey { name: String, #[serde(rename = "in")] _in: KeyIn, }, OAuth2 { #[serde(flatten)] flow: OAuth2Flow, scopes: BTreeMap, }, } impl JsonDeref for SecurityType { fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { if path.is_empty() { return Ok(self); } let (head, tail) = path.split_once("/").unwrap_or((path, "")); match self { SecurityType::Basic => eyre::bail!("not found: {head}"), SecurityType::ApiKey { name, _in } => match head { "name" => name.deref_any(tail), "in" => _in.deref_any(tail), _ => eyre::bail!("not found: {head}"), }, SecurityType::OAuth2 { flow, scopes } => match head { "flow" => flow.deref_any(tail), "scopes" => scopes.deref_any(tail), _ => eyre::bail!("not found: {head}"), }, } } } #[derive(serde::Deserialize, Debug, PartialEq)] #[serde(rename_all(deserialize = "camelCase"))] pub enum KeyIn { Query, Header, } impl JsonDeref for KeyIn { fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { if path.is_empty() { Ok(self) } else { eyre::bail!("not found") } } } #[derive(serde::Deserialize, Debug, PartialEq)] #[serde(rename_all(deserialize = "camelCase"), tag = "flow")] pub enum OAuth2Flow { Implicit { authorization_url: Url, }, Password { token_url: Url, }, Application { token_url: Url, }, AccessCode { authorization_url: Url, token_url: Url, }, } impl JsonDeref for OAuth2Flow { fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { if path.is_empty() { return Ok(self); } let (head, tail) = path.split_once("/").unwrap_or((path, "")); match self { OAuth2Flow::Implicit { authorization_url } => match head { "authorizationUrl" => authorization_url.deref_any(tail), _ => eyre::bail!("not found: {head}"), }, OAuth2Flow::Password { token_url } => match head { "tokenUrl" => token_url.deref_any(tail), _ => eyre::bail!("not found: {head}"), }, OAuth2Flow::Application { token_url } => match head { "tokenUrl" => token_url.deref_any(tail), _ => eyre::bail!("not found: {head}"), }, OAuth2Flow::AccessCode { authorization_url, token_url, } => match head { "authorizationUrl" => authorization_url.deref_any(tail), "tokenUrl" => token_url.deref_any(tail), _ => eyre::bail!("not found: {head}"), }, } } } #[derive(serde::Deserialize, Debug, PartialEq)] #[serde(untagged)] pub enum MaybeRef { Ref { #[serde(rename = "$ref")] _ref: String, }, Value { #[serde(flatten)] value: T, }, } impl JsonDeref for MaybeRef { fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { if path.is_empty() { return Ok(self); } let (head, tail) = path.split_once("/").unwrap_or((path, "")); match self { MaybeRef::Ref { _ref } => match head { "$ref" => _ref.deref_any(tail), _ => eyre::bail!("not found: {head}"), }, MaybeRef::Value { value } => value.deref_any(path), } } } impl MaybeRef { pub fn deref<'a>(&'a self, spec: &'a OpenApiV2) -> eyre::Result<&'a T> { match self { MaybeRef::Ref { _ref } => spec.deref(_ref), MaybeRef::Value { value } => Ok(value), } } }