From 9fd0bceaf4a4c6dbb3b7df2d6da4a6d77f3d1bdf Mon Sep 17 00:00:00 2001 From: Dominic Date: Mon, 27 Apr 2020 02:12:51 +0200 Subject: [PATCH] move openapi operation extraction code into its own mod --- gotham_restful/src/openapi/mod.rs | 1 + gotham_restful/src/openapi/operation.rs | 217 +++++++++++++++++++++ gotham_restful/src/openapi/router.rs | 244 +----------------------- 3 files changed, 227 insertions(+), 235 deletions(-) create mode 100644 gotham_restful/src/openapi/operation.rs diff --git a/gotham_restful/src/openapi/mod.rs b/gotham_restful/src/openapi/mod.rs index aff6b1e..141ea22 100644 --- a/gotham_restful/src/openapi/mod.rs +++ b/gotham_restful/src/openapi/mod.rs @@ -3,5 +3,6 @@ const SECURITY_NAME : &str = "authToken"; pub mod builder; pub mod handler; +pub mod operation; pub mod router; pub mod types; diff --git a/gotham_restful/src/openapi/operation.rs b/gotham_restful/src/openapi/operation.rs new file mode 100644 index 0000000..77d0698 --- /dev/null +++ b/gotham_restful/src/openapi/operation.rs @@ -0,0 +1,217 @@ +use crate::{ + resource::*, + result::*, + OpenapiSchema, + OpenapiType, + RequestBody +}; +use super::SECURITY_NAME; +use indexmap::IndexMap; +use mime::{Mime, STAR_STAR}; +use openapiv3::{ + MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr, + ReferenceOr::Item, RequestBody as OARequestBody, Response, Responses, Schema, SchemaKind, + StatusCode, Type +}; + + +#[derive(Default)] +struct OperationParams<'a> +{ + path_params : Vec<&'a str>, + query_params : Option +} + +impl<'a> OperationParams<'a> +{ + fn add_path_params(&self, params : &mut Vec>) + { + for param in &self.path_params + { + params.push(Item(Parameter::Path { + parameter_data: ParameterData { + name: (*param).to_string(), + description: None, + required: true, + deprecated: None, + format: ParameterSchemaOrContent::Schema(Item(String::schema().into_schema())), + example: None, + examples: IndexMap::new() + }, + style: Default::default(), + })); + } + } + + fn add_query_params(self, params : &mut Vec>) + { + let query_params = match self.query_params { + Some(qp) => qp.schema, + None => return + }; + let query_params = match query_params { + SchemaKind::Type(Type::Object(ty)) => ty, + _ => panic!("Query Parameters needs to be a plain struct") + }; + for (name, schema) in query_params.properties + { + let required = query_params.required.contains(&name); + params.push(Item(Parameter::Query { + parameter_data: ParameterData { + name, + description: None, + required, + deprecated: None, + format: ParameterSchemaOrContent::Schema(schema.unbox()), + example: None, + examples: IndexMap::new() + }, + allow_reserved: false, + style: Default::default(), + allow_empty_value: None + })) + } + } + + fn into_params(self) -> Vec> + { + let mut params : Vec> = Vec::new(); + self.add_path_params(&mut params); + self.add_query_params(&mut params); + params + } +} + +pub struct OperationDescription<'a> +{ + operation_id : Option, + default_status : crate::StatusCode, + accepted_types : Option>, + schema : ReferenceOr, + params : OperationParams<'a>, + body_schema : Option>, + supported_types : Option>, + requires_auth : bool +} + +impl<'a> OperationDescription<'a> +{ + pub fn new(schema : ReferenceOr) -> Self + { + Self { + operation_id: Handler::operation_id(), + default_status: Handler::Res::default_status(), + accepted_types: Handler::Res::accepted_types(), + schema, + params: Default::default(), + body_schema: None, + supported_types: None, + requires_auth: Handler::wants_auth() + } + } + + pub fn with_path_params(mut self, params : Vec<&'a str>) -> Self + { + self.params.path_params = params; + self + } + + pub fn with_query_params(mut self, params : OpenapiSchema) -> Self + { + self.params.query_params = Some(params); + self + } + + pub fn with_body(mut self, schema : ReferenceOr) -> Self + { + self.body_schema = Some(schema); + self.supported_types = Body::supported_types(); + self + } + + + fn schema_to_content(types : Vec, schema : ReferenceOr) -> IndexMap + { + let mut content : IndexMap = IndexMap::new(); + for ty in types + { + content.insert(ty.to_string(), MediaType { + schema: Some(schema.clone()), + ..Default::default() + }); + } + content + } + + pub fn into_operation(self) -> Operation + { + // this is unfortunately neccessary to prevent rust from complaining about partially moving self + let (operation_id, default_status, accepted_types, schema, params, body_schema, supported_types, requires_auth) = ( + self.operation_id, self.default_status, self.accepted_types, self.schema, self.params, self.body_schema, self.supported_types, self.requires_auth); + + let content = Self::schema_to_content(accepted_types.unwrap_or_else(|| vec![STAR_STAR]), schema); + + let mut responses : IndexMap> = IndexMap::new(); + responses.insert(StatusCode::Code(default_status.as_u16()), Item(Response { + description: default_status.canonical_reason().map(|d| d.to_string()).unwrap_or_default(), + content, + ..Default::default() + })); + + let request_body = body_schema.map(|schema| Item(OARequestBody { + description: None, + content: Self::schema_to_content(supported_types.unwrap_or_else(|| vec![STAR_STAR]), schema), + required: true + })); + + let mut security = Vec::new(); + if requires_auth + { + let mut sec = IndexMap::new(); + sec.insert(SECURITY_NAME.to_owned(), Vec::new()); + security.push(sec); + } + + Operation { + tags: Vec::new(), + operation_id, + parameters: params.into_params(), + request_body, + responses: Responses { + default: None, + responses + }, + deprecated: false, + security, + ..Default::default() + } + } +} + + +#[cfg(test)] +mod test +{ + use crate::ResourceResult; + use super::*; + + #[test] + fn no_content_schema_to_content() + { + let types = NoContent::accepted_types(); + let schema = ::schema(); + let content = OperationDescription::schema_to_content(types.unwrap_or_else(|| vec![STAR_STAR]), Item(schema.into_schema())); + assert!(content.is_empty()); + } + + #[test] + fn raw_schema_to_content() + { + let types = Raw::<&str>::accepted_types(); + let schema = as OpenapiType>::schema(); + let content = OperationDescription::schema_to_content(types.unwrap_or_else(|| vec![STAR_STAR]), Item(schema.into_schema())); + assert_eq!(content.len(), 1); + let json = serde_json::to_string(&content.values().nth(0).unwrap()).unwrap(); + assert_eq!(json, r#"{"schema":{"type":"string","format":"binary"}}"#); + } +} diff --git a/gotham_restful/src/openapi/router.rs b/gotham_restful/src/openapi/router.rs index 0298656..36783d3 100644 --- a/gotham_restful/src/openapi/router.rs +++ b/gotham_restful/src/openapi/router.rs @@ -1,23 +1,13 @@ use crate::{ resource::*, - result::*, routing::*, - OpenapiSchema, OpenapiType, - RequestBody }; -use super::{builder::OpenapiBuilder, handler::OpenapiHandler, SECURITY_NAME}; +use super::{builder::OpenapiBuilder, handler::OpenapiHandler, operation::OperationDescription}; use gotham::{ pipeline::chain::PipelineHandleChain, router::builder::* }; -use indexmap::IndexMap; -use mime::{Mime, STAR_STAR}; -use openapiv3::{ - MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr, - ReferenceOr::Item, RequestBody as OARequestBody, Response, Responses, Schema, SchemaKind, - StatusCode, Type -}; use std::panic::RefUnwindSafe; /// This trait adds the `get_openapi` method to an OpenAPI-aware router. @@ -26,150 +16,6 @@ pub trait GetOpenapi fn get_openapi(&mut self, path : &str); } -fn schema_to_content(types : Vec, schema : ReferenceOr) -> IndexMap -{ - let mut content : IndexMap = IndexMap::new(); - for ty in types - { - content.insert(ty.to_string(), MediaType { - schema: Some(schema.clone()), - ..Default::default() - }); - } - content -} - -#[derive(Default)] -struct OperationParams<'a> -{ - path_params : Vec<&'a str>, - query_params : Option -} - -impl<'a> OperationParams<'a> -{ - fn new(path_params : Vec<&'a str>, query_params : Option) -> Self - { - Self { path_params, query_params } - } - - fn from_path_params(path_params : Vec<&'a str>) -> Self - { - Self::new(path_params, None) - } - - fn from_query_params(query_params : OpenapiSchema) -> Self - { - Self::new(Vec::new(), Some(query_params)) - } - - fn add_path_params(&self, params : &mut Vec>) - { - for param in &self.path_params - { - params.push(Item(Parameter::Path { - parameter_data: ParameterData { - name: (*param).to_string(), - description: None, - required: true, - deprecated: None, - format: ParameterSchemaOrContent::Schema(Item(String::schema().into_schema())), - example: None, - examples: IndexMap::new() - }, - style: Default::default(), - })); - } - } - - fn add_query_params(self, params : &mut Vec>) - { - let query_params = match self.query_params { - Some(qp) => qp.schema, - None => return - }; - let query_params = match query_params { - SchemaKind::Type(Type::Object(ty)) => ty, - _ => panic!("Query Parameters needs to be a plain struct") - }; - for (name, schema) in query_params.properties - { - let required = query_params.required.contains(&name); - params.push(Item(Parameter::Query { - parameter_data: ParameterData { - name, - description: None, - required, - deprecated: None, - format: ParameterSchemaOrContent::Schema(schema.unbox()), - example: None, - examples: IndexMap::new() - }, - allow_reserved: false, - style: Default::default(), - allow_empty_value: None - })) - } - } - - fn into_params(self) -> Vec> - { - let mut params : Vec> = Vec::new(); - self.add_path_params(&mut params); - self.add_query_params(&mut params); - params - } -} - -fn new_operation( - operation_id : Option, - default_status : crate::StatusCode, - accepted_types : Option>, - schema : ReferenceOr, - params : OperationParams, - body_schema : Option>, - supported_types : Option>, - requires_auth : bool -) -> Operation -{ - let content = schema_to_content(accepted_types.unwrap_or_else(|| vec![STAR_STAR]), schema); - - let mut responses : IndexMap> = IndexMap::new(); - responses.insert(StatusCode::Code(default_status.as_u16()), Item(Response { - description: default_status.canonical_reason().map(|d| d.to_string()).unwrap_or_default(), - content, - ..Default::default() - })); - - let request_body = body_schema.map(|schema| Item(OARequestBody { - description: None, - content: schema_to_content(supported_types.unwrap_or_else(|| vec![STAR_STAR]), schema), - required: true - })); - - let mut security = Vec::new(); - if requires_auth - { - let mut sec = IndexMap::new(); - sec.insert(SECURITY_NAME.to_owned(), Vec::new()); - security.push(sec); - } - - Operation { - tags: Vec::new(), - operation_id, - parameters: params.into_params(), - request_body, - responses: Responses { - default: None, - responses - }, - deprecated: false, - security, - ..Default::default() - } -} - macro_rules! implOpenapiRouter { ($implType:ident) => { @@ -206,7 +52,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.get = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::default(), None, None, Handler::wants_auth())); + item.get = Some(OperationDescription::new::(schema).into_operation()); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1).read_all::() @@ -218,7 +64,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}/{{id}}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.get = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::from_path_params(vec!["id"]), None, None, Handler::wants_auth())); + item.get = Some(OperationDescription::new::(schema).with_path_params(vec!["id"]).into_operation()); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1).read::() @@ -230,7 +76,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}/search", &self.1); let mut item = (self.0).1.remove_path(&self.1); - item.get = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::from_query_params(Handler::Query::schema()), None, None, Handler::wants_auth())); + item.get = Some(OperationDescription::new::(schema).with_query_params(Handler::Query::schema()).into_operation()); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1).search::() @@ -246,7 +92,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.post = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::default(), Some(body_schema), Handler::Body::supported_types(), Handler::wants_auth())); + item.post = Some(OperationDescription::new::(schema).with_body::(body_schema).into_operation()); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1).create::() @@ -262,7 +108,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.put = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::default(), Some(body_schema), Handler::Body::supported_types(), Handler::wants_auth())); + item.put = Some(OperationDescription::new::(schema).with_body::(body_schema).into_operation()); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1).update_all::() @@ -278,7 +124,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}/{{id}}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.put = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::from_path_params(vec!["id"]), Some(body_schema), Handler::Body::supported_types(), Handler::wants_auth())); + item.put = Some(OperationDescription::new::(schema).with_path_params(vec!["id"]).with_body::(body_schema).into_operation()); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1).update::() @@ -290,7 +136,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.delete = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::default(), None, None, Handler::wants_auth())); + item.delete = Some(OperationDescription::new::(schema).into_operation()); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1).delete_all::() @@ -302,7 +148,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}/{{id}}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.delete = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::from_path_params(vec!["id"]), None, None, Handler::wants_auth())); + item.delete = Some(OperationDescription::new::(schema).with_path_params(vec!["id"]).into_operation()); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1).delete::() @@ -314,75 +160,3 @@ macro_rules! implOpenapiRouter { implOpenapiRouter!(RouterBuilder); implOpenapiRouter!(ScopeBuilder); - - -#[cfg(test)] -mod test -{ - use crate::ResourceResult; - use super::*; - - #[derive(OpenapiType)] - #[allow(dead_code)] - struct QueryParams - { - id : isize - } - - #[test] - fn params_empty() - { - let op_params = OperationParams::default(); - let params = op_params.into_params(); - assert!(params.is_empty()); - } - - #[test] - fn params_from_path_params() - { - let name = "id"; - let op_params = OperationParams::from_path_params(vec![name]); - let params = op_params.into_params(); - let json = serde_json::to_string(¶ms).unwrap(); - assert_eq!(json, format!(r#"[{{"in":"path","name":"{}","required":true,"schema":{{"type":"string"}},"style":"simple"}}]"#, name)); - } - - #[test] - fn params_from_query_params() - { - let op_params = OperationParams::from_query_params(QueryParams::schema()); - let params = op_params.into_params(); - let json = serde_json::to_string(¶ms).unwrap(); - assert_eq!(json, r#"[{"in":"query","name":"id","required":true,"schema":{"type":"integer"},"style":"form"}]"#); - } - - #[test] - fn params_both() - { - let name = "id"; - let op_params = OperationParams::new(vec![name], Some(QueryParams::schema())); - let params = op_params.into_params(); - let json = serde_json::to_string(¶ms).unwrap(); - assert_eq!(json, format!(r#"[{{"in":"path","name":"{}","required":true,"schema":{{"type":"string"}},"style":"simple"}},{{"in":"query","name":"id","required":true,"schema":{{"type":"integer"}},"style":"form"}}]"#, name)); - } - - #[test] - fn no_content_schema_to_content() - { - let types = NoContent::accepted_types(); - let schema = ::schema(); - let content = schema_to_content(types.unwrap_or_else(|| vec![STAR_STAR]), Item(schema.into_schema())); - assert!(content.is_empty()); - } - - #[test] - fn raw_schema_to_content() - { - let types = Raw::<&str>::accepted_types(); - let schema = as OpenapiType>::schema(); - let content = schema_to_content(types.unwrap_or_else(|| vec![STAR_STAR]), Item(schema.into_schema())); - assert_eq!(content.len(), 1); - let json = serde_json::to_string(&content.values().nth(0).unwrap()).unwrap(); - assert_eq!(json, r#"{"schema":{"type":"string","format":"binary"}}"#); - } -}