diff --git a/Cargo.toml b/Cargo.toml index 7a0fb44a8..e1485f87f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ required-features = ["cli"] paperclip-actix = { path = "plugins/actix-web", version = "0.7.0", optional = true } paperclip-core = { path = "core", version = "0.7.0" } paperclip-macros = { path = "macros", version = "0.6.3", optional = true } +paperclip-codegen = { path = "codegen", version = "0.1.0", optional = true } env_logger = { version = "0.8", optional = true } git2 = { version = "0.15", optional = true } @@ -40,7 +41,7 @@ url_dep = { version = ">=1.7,<3", package = "url" } thiserror = "1.0" anyhow = "1.0" once_cell = "1.4" -openapiv3 = { version = "1.0.3", optional = true } +openapiv3 = { version = "2.0.0", optional = true } [dev-dependencies] actix-rt1 = { version = "1.0", package = "actix-rt" } @@ -83,6 +84,8 @@ codegen = ["heck", "http", "log", "regex", "tinytemplate", "paperclip-core/codeg v2 = ["paperclip-macros/v2", "paperclip-core/v2"] # OpenAPI v2 to v3 support v3 = ["openapiv3", "v2", "paperclip-core/v3", "paperclip-actix/v3"] +# Experimental V3 CodeGen +v3-poc = ["paperclip-codegen", "openapiv3"] # Features for implementing traits for dependencies. @@ -106,6 +109,7 @@ members = [ "core", "macros", "plugins/actix-web", + "codegen" ] [[test]] diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml new file mode 100644 index 000000000..b90a77791 --- /dev/null +++ b/codegen/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "paperclip-codegen" +version = "0.1.0" +edition = "2018" +license = "MIT OR Apache-2.0" +keywords = [ "openapi", "openapiv3", "codegen" ] +description = "Experimental OpenAPI V3.0.3 Code Generator" +homepage = "https://github.com/paperclip-rs/paperclip" +repository = "https://github.com/paperclip-rs/paperclip" + +[lib] + +[[bin]] +name = "paperclip-cli" +path = "src/bin/main.rs" + +[dependencies] +ramhorns = { version = "1.0", default-features = false, features = ["indexes"], optional = true } +ramhorns-derive = { version = "1.0", optional = true } +openapiv3 = { version = "2.0.0", optional = true } +heck = { version = "0.4", optional = true } +itertools = { version = "0.10", optional = true } +log = { version = "0.4", features = ["kv_unstable"] } + + +[features] +default = [ "poc" ] +ramhorns-feat = [ "ramhorns", "ramhorns-derive" ] +poc = [ "ramhorns-feat", "openapiv3", "heck", "itertools" ] \ No newline at end of file diff --git a/codegen/src/bin/main.rs b/codegen/src/bin/main.rs new file mode 100644 index 000000000..e7a11a969 --- /dev/null +++ b/codegen/src/bin/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/codegen/src/lib.rs b/codegen/src/lib.rs new file mode 100644 index 000000000..be3939ebb --- /dev/null +++ b/codegen/src/lib.rs @@ -0,0 +1,9 @@ +mod v3; + +pub mod v3_03 { + pub use super::v3::{OpenApiV3, PackageInfo}; +} + +#[cfg_attr(feature = "ramhorns-feat", macro_use)] +#[cfg(feature = "ramhorns-feat")] +extern crate log; diff --git a/codegen/src/v3/mod.rs b/codegen/src/v3/mod.rs new file mode 100644 index 000000000..9e0661559 --- /dev/null +++ b/codegen/src/v3/mod.rs @@ -0,0 +1,519 @@ +mod operation; +mod parameter; +mod property; +mod templates; + +use std::{cell::RefCell, collections::HashSet, ops::Deref}; + +use operation::Operation; +use parameter::Parameter; +use property::Property; +use templates::*; + +use itertools::Itertools; +use ramhorns::Template; +use ramhorns_derive::Content; + +/// OpenApiV3 code generator. +#[derive(Debug)] +pub struct OpenApiV3 { + api: openapiv3::OpenAPI, + + output_path: std::path::PathBuf, + package_info: PackageInfo, + + api_template: Vec, + model_templates: Vec, + supporting_templates: Vec, + + suppress_errors: bool, + circ_ref_checker: RefCell, +} +impl OpenApiV3 { + /// Creates a new OpenApi V3 Generator. + pub fn new( + api: openapiv3::OpenAPI, + output_path: Option, + package_info: PackageInfo, + ) -> Self { + let output_path = output_path.unwrap_or_else(|| std::path::Path::new(".").to_path_buf()); + let (api_template, model_templates, supporting_templates) = templates::default_templates(); + Self { + api, + output_path, + package_info, + api_template, + model_templates, + supporting_templates, + suppress_errors: false, + circ_ref_checker: RefCell::new(CircularRefChecker::default()), + } + } +} + +#[derive(Debug)] +pub struct PackageInfo { + pub name: String, + pub version: String, + pub libname: String, + pub edition: String, +} + +#[derive(Clone, Content)] +struct ApiInfoTpl<'a> { + apis: &'a Vec>, +} +#[derive(Clone, Content)] +struct Apis<'a> { + #[ramhorns(rename = "operations")] + apis: Vec>, +} +#[derive(Clone, Content)] +#[ramhorns(rename_all = "camelCase")] +struct SupportingTpl<'a> { + api_info: ApiInfoTpl<'a>, + operations: OperationsTpl<'a>, + models: ModelTpl<'a>, + package_name: &'a str, + package_version: &'a str, + package_libname: &'a str, + package_edition: &'a str, +} +#[derive(Clone, Content)] +#[ramhorns(rename_all = "camelCase")] +struct ModelsTpl<'a> { + models: ModelTpl<'a>, +} +#[derive(Clone, Content)] +struct ModelTpl<'a> { + model: &'a Vec, +} + +#[derive(Content, Debug, Clone)] +struct OperationsTpl<'a> { + operation: &'a Vec, +} + +#[derive(Content, Clone, Debug)] +#[ramhorns(rename_all = "camelCase")] +pub(super) struct OperationsApiTpl<'a> { + classname: &'a str, + class_filename: &'a str, + + operations: OperationsTpl<'a>, +} +pub(super) struct OperationsApi { + classname: String, + class_filename: String, + + operations: Vec, +} + +impl OpenApiV3 { + /// Run the OpenApi V3 Code Generator. + pub fn run(&self, models: bool, ops: bool) -> Result<(), std::io::Error> { + let models = if models { self.models()? } else { vec![] }; + let operations = if ops { self.operations()? } else { vec![] }; + let apis = self.apis(&operations)?; + let apis = apis + .iter() + .map(|o| OperationsApiTpl { + classname: o.classname(), + class_filename: o.class_filename(), + operations: OperationsTpl { + operation: &o.operations, + }, + }) + .collect::>(); + + self.ensure_templates()?; + + self.render_supporting(&models, &operations, &apis)?; + self.render_models(&models)?; + self.render_apis(&apis)?; + + Ok(()) + } + fn ensure_templates(&self) -> Result<(), std::io::Error> { + Self::ensure_path(&self.output_path, true)?; + let templates = self + .supporting_templates + .iter() + .map(Deref::deref) + .chain(self.api_template.iter().map(Deref::deref)) + .chain(self.model_templates.iter().map(Deref::deref)) + .collect::>(); + self.ensure_template(&templates) + } + fn ensure_template_path( + &self, + path: &std::path::Path, + clean: bool, + ) -> Result<(), std::io::Error> { + let path = self.output_path.join(path); + Self::ensure_path(&path, clean) + } + fn ensure_path(path: &std::path::Path, clean: bool) -> Result<(), std::io::Error> { + if clean && path.exists() { + if path.is_dir() { + std::fs::remove_dir_all(path)?; + } else { + std::fs::remove_file(path)?; + } + } + std::fs::create_dir_all(path) + } + fn ensure_template(&self, templates: &[&GenTemplateFile]) -> Result<(), std::io::Error> { + templates + .iter() + .try_for_each(|template| self.ensure_template_path(template.target_prefix(), true))?; + templates + .iter() + .try_for_each(|template| self.ensure_template_path(template.target_prefix(), false)) + } + fn render_supporting( + &self, + models: &Vec, + operations: &Vec, + apis: &Vec, + ) -> Result<(), std::io::Error> { + self.supporting_templates + .iter() + .try_for_each(|e| self.render_supporting_template(e, models, operations, apis)) + } + fn render_apis(&self, apis: &Vec) -> Result<(), std::io::Error> { + self.api_template + .iter() + .try_for_each(|e| self.render_template_apis(e, apis)) + } + fn render_models(&self, models: &Vec) -> Result<(), std::io::Error> { + for property in models { + let model = &vec![property.clone()]; + for template in &self.model_templates { + let tpl = self.tpl(template)?; + + let path = self.output_path.join(&template.model_path(property)); + + tpl.render_to_file( + path, + &ModelsTpl { + models: ModelTpl { model }, + }, + )?; + } + } + + Ok(()) + } + + fn tpl(&self, template: &GenTemplateFile) -> Result { + let Some(mustache) = template.input().buffer() else { + return Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "Template from path not supported yet", + )); + }; + let tpl = Template::new(mustache).map_err(|error| { + std::io::Error::new(std::io::ErrorKind::InvalidInput, error.to_string()) + })?; + + Ok(tpl) + } + + fn render_supporting_template( + &self, + template: &SuppTemplateFile, + models: &Vec, + operations: &Vec, + apis: &Vec, + ) -> Result<(), std::io::Error> { + let tpl = self.tpl(template)?; + + let path = self + .output_path + .join(template.target_prefix()) + .join(template.target_postfix()); + tpl.render_to_file( + path, + &SupportingTpl { + api_info: ApiInfoTpl { apis }, + operations: OperationsTpl { + operation: operations, + }, + models: ModelTpl { model: models }, + package_name: self.package_info.name.as_str(), + package_version: self.package_info.version.as_str(), + package_libname: self.package_info.libname.as_str(), + package_edition: self.package_info.edition.as_str(), + }, + )?; + + Ok(()) + } + #[allow(unused)] + fn render_template_models( + &self, + template: &ModelTemplateFile, + models: &Vec, + ) -> Result<(), std::io::Error> { + let tpl = self.tpl(template)?; + + for model in models { + let path = self.output_path.join(&template.model_path(model)); + let model = &vec![model.clone()]; + tpl.render_to_file( + path, + &ModelsTpl { + models: ModelTpl { model }, + }, + )?; + } + + Ok(()) + } + fn render_template_apis( + &self, + template: &ApiTemplateFile, + apis: &Vec, + ) -> Result<(), std::io::Error> { + let tpl = self.tpl(template)?; + + for api in apis { + let path = self.output_path.join(template.api_path(api)); + if let Some(parent) = path.parent() { + // we already cleaned the top-level, don't clean it again as we might have other templates + // with the form $output/$target-folder/$api-classname/$any + Self::ensure_path(parent, false)?; + } + tpl.render_to_file(path, api)?; + } + + Ok(()) + } + + fn models(&self) -> Result, std::io::Error> { + let model = self + .api + .components + .as_ref() + .unwrap() + .schemas + .iter() + //.filter(|(name, _)| name.starts_with("ReplicaSpec")) + .map(|(name, ref_or)| { + let model = self.resolve_reference_or(ref_or, None, None, Some(name)); + debug!("Model: {} => {}", name, model); + model + }) + .flat_map(|m| m.discovered_models().into_iter().chain(vec![m])) + .filter(|m| m.is_model() && !m.data_type().is_empty()) + .map(Self::post_process) + // todo: when discovering models we should use a cache to avoid re-processing models + // then we won't need to do this dedup. + .sorted_by(|a, b| a.schema().cmp(b.schema())) + .dedup_by(|a, b| a.schema() == b.schema()) + .inspect(|model| debug!("Model => {}", model)) + .collect::>(); + Ok(model) + } + fn operations(&self) -> Result, std::io::Error> { + let operation = self + .api + .operations() + .map(|(path, method, operation)| Operation::new(self, path, method, operation)) + .collect::>(); + + Ok(operation) + } + fn apis(&self, operations: &Vec) -> Result, std::io::Error> { + let mut tags = std::collections::HashMap::::new(); + for op in operations { + for tag in op.tags() { + match tags.get_mut(tag) { + Some(api) => { + api.add_op(op); + } + None => { + tags.insert(tag.clone(), op.into()); + } + } + } + } + + // let apis = tags + // .clone() + // .into_values() + // .map(|o| o.classname().to_string()) + // .collect::>(); + // debug!("apis: {:?}", apis); + + Ok(tags + .into_values() + .sorted_by(|l, r| l.classname().cmp(r.classname())) + .collect::>()) + } +} + +impl OpenApiV3 { + fn missing_schema_ref(&self, reference: &str) { + if !self.suppress_errors { + println!("Schema reference({}) not found", reference); + } + } + fn contains_schema(&self, type_: &str) -> bool { + let contains = match &self.api.components { + None => false, + Some(components) => components.schemas.contains_key(type_), + }; + trace!("Contains {} => {}", type_, contains); + contains + } + fn set_resolving(&self, type_name: &str) { + let mut checker = self.circ_ref_checker.borrow_mut(); + checker.add(type_name); + } + fn resolving(&self, property: &Property) -> bool { + let checker = self.circ_ref_checker.borrow(); + checker.exists(property.type_ref()) + } + fn clear_resolving(&self, type_name: &str) { + let mut checker = self.circ_ref_checker.borrow_mut(); + checker.remove(type_name); + } + fn resolve_schema_name(&self, var_name: Option<&str>, reference: &str) -> Property { + let type_name = match reference.strip_prefix("#/components/schemas/") { + Some(type_name) => type_name, + None => todo!("schema not found..."), + }; + trace!("Resolving: {:?}/{}", var_name, type_name); + let schemas = self.api.components.as_ref().map(|c| &c.schemas); + match schemas.and_then(|s| s.get(type_name)) { + None => { + panic!("Schema {} Not found!", type_name); + } + Some(ref_or) => self.resolve_reference_or(ref_or, None, var_name, Some(type_name)), + } + } + fn resolve_schema( + &self, + schema: &openapiv3::Schema, + parent: Option<&Property>, + name: Option<&str>, + type_: Option<&str>, + ) -> Property { + trace!("ResolvingSchema: {:?}/{:?}", name, type_); + if let Some(type_) = &type_ { + self.set_resolving(type_); + } + let property = Property::from_schema(self, parent, schema, name, type_); + if let Some(type_) = &type_ { + self.clear_resolving(type_); + } + property + } + + fn resolve_reference_or( + &self, + reference: &openapiv3::ReferenceOr, + parent: Option<&Property>, + name: Option<&str>, // parameter name, only known for object vars + type_: Option<&str>, // type, only known when walking the component schema list + ) -> Property { + match reference { + openapiv3::ReferenceOr::Reference { reference } => { + self.resolve_schema_name(name, reference) + } + openapiv3::ReferenceOr::Item(schema) => { + self.resolve_schema(schema, parent, name, type_) + } + } + } + fn resolve_reference_or_resp( + &self, + content: &str, + reference: &openapiv3::ReferenceOr, + ) -> Property { + debug!("Response: {reference:?}"); + match reference { + openapiv3::ReferenceOr::Reference { reference } => { + self.resolve_schema_name(None, reference) + } + openapiv3::ReferenceOr::Item(item) => match item.content.get(content) { + Some(media) => match &media.schema { + Some(schema) => self.resolve_reference_or(schema, None, None, None), + None => Property::default(), + }, + None => Property::default().with_data_property(&property::PropertyDataType::Empty), + }, + } + } + + fn post_process(property: Property) -> Property { + property.post_process() + } +} + +impl OperationsApiTpl<'_> { + /// Get a reference to the api classname. + pub fn classname(&self) -> &str { + self.classname + } + /// Get a reference to the api class filename. + pub fn class_filename(&self) -> &str { + self.class_filename + } +} +impl OperationsApi { + /// Get a reference to the api classname. + pub fn classname(&self) -> &str { + &self.classname + } + /// Get a reference to the api class filename. + pub fn class_filename(&self) -> &str { + &self.class_filename + } + /// Add the given operation. + pub(super) fn add_op(&mut self, operation: &Operation) { + self.operations.push(operation.clone()); + } +} + +impl From<&Operation> for OperationsApi { + fn from(src: &Operation) -> OperationsApi { + OperationsApi { + class_filename: src.class_filename().into(), + classname: src.classname().into(), + operations: vec![src.clone()], + } + } +} + +/// Circular Reference Checker +/// If a model's member variable references a model currently being resolved +/// (either parent, or another elder) then a reference check must be used +/// to break out of an infinit loop. +/// In this case we don't really need to re-resolve the entire model +/// because the model itself will resolve itself. +#[derive(Clone, Debug, Default)] +struct CircularRefChecker { + /// List of type_names in the resolve chain. + type_names: HashSet, + /// Current type being resolved. + current: String, +} +impl CircularRefChecker { + fn add(&mut self, type_name: &str) { + if self.type_names.insert(type_name.to_string()) { + // trace!("Added cache: {type_name}"); + self.current = type_name.to_string(); + } + } + fn exists(&self, type_name: &str) -> bool { + self.current.as_str() != type_name && self.type_names.contains(type_name) + } + fn remove(&mut self, type_name: &str) { + if self.type_names.remove(type_name) { + // trace!("Removed cache: {type_name}"); + } + } +} diff --git a/codegen/src/v3/operation.rs b/codegen/src/v3/operation.rs new file mode 100644 index 000000000..07709daff --- /dev/null +++ b/codegen/src/v3/operation.rs @@ -0,0 +1,241 @@ +use std::collections::HashMap; + +use super::{OpenApiV3, Parameter, Property}; + +use heck::{ToLowerCamelCase, ToSnakeCase, ToUpperCamelCase}; +use ramhorns_derive::Content; + +#[derive(Default, Content, Clone, Debug)] +#[ramhorns(rename_all = "camelCase")] +pub(crate) struct Operation { + classname: String, + class_filename: String, + + response_headers: Vec, + + return_type_is_primitive: bool, + return_simple_type: bool, + subresource_operation: bool, + is_multipart: bool, + is_response_binary: bool, + is_response_file: bool, + is_response_optional: bool, + has_reference: bool, + is_restful_index: bool, + is_restful_show: bool, + is_restful_create: bool, + is_restful_update: bool, + is_restful_destroy: bool, + is_restful: bool, + is_deprecated: Option, + is_callback_request: bool, + unique_items: bool, + has_default_response: bool, + // if 4xx, 5xx responses have at least one error object defined + has_error_response_object: bool, + + path: String, + operation_id: Option, + return_type: String, + return_format: String, + http_method: String, + return_base_type: String, + return_container: String, + summary: Option, + unescaped_notes: String, + basename: String, + default_response: String, + + consumes: Vec>, + has_consumes: bool, + produces: Vec>, + has_produces: bool, + prioritized_content_types: Vec>, + + body_param: Parameter, + + all_params: Vec, + has_params: bool, + path_params: Vec, + has_path_params: bool, + query_params: Vec, + has_query_params: bool, + header_params: Vec, + has_header_params: bool, + implicit_headers_params: Vec, + has_implicit_headers_params: bool, + form_params: Vec, + has_form_params: bool, + required_params: Vec, + has_required_params: bool, + optional_params: Vec, + has_optional_params: bool, + auth_methods: Vec, + has_auth_methods: bool, + + tags: Vec, + responses: Vec<()>, + callbacks: Vec<()>, + + examples: Vec>, + request_body_examples: Vec>, + + vendor_extensions: HashMap, + + operation_id_original: Option, + operation_id_camel_case: Option, + support_multiple_responses: bool, +} + +fn query_param(api: &OpenApiV3, value: &openapiv3::Parameter) -> Option { + match value { + openapiv3::Parameter::Query { parameter_data, .. } => { + let parameter = Parameter::new(api, parameter_data); + Some(parameter) + } + _ => None, + } +} +fn path_param(api: &OpenApiV3, value: &openapiv3::Parameter) -> Option { + match value { + openapiv3::Parameter::Path { parameter_data, .. } => { + let parameter = Parameter::new(api, parameter_data); + Some(parameter) + } + _ => None, + } +} +#[allow(unused)] +fn header_param(api: &OpenApiV3, value: &openapiv3::Parameter) -> Option { + match value { + openapiv3::Parameter::Header { parameter_data, .. } => { + let parameter = Parameter::new(api, parameter_data); + Some(parameter) + } + _ => None, + } +} + +impl Operation { + /// Create an Operation based on the deserialized openapi operation. + pub(crate) fn new( + root: &OpenApiV3, + path: &str, + method: &str, + operation: &openapiv3::Operation, + ) -> Self { + debug!( + "Operation::{id:?} => {method}::{path}::{tags:?}", + id = operation.operation_id, + tags = operation.tags + ); + let mut vendor_extensions = operation + .extensions + .iter() + .map(|(k, v)| (k.clone(), v.to_string())) + .collect::>(); + + vendor_extensions.insert("x-httpMethodLower".into(), method.to_ascii_lowercase()); + vendor_extensions.insert("x-httpMethodUpper".into(), method.to_ascii_uppercase()); + + let query_params = operation + .parameters + .iter() + .flat_map(|p| { + match p { + // todo: need to handle this + openapiv3::ReferenceOr::Reference { .. } => todo!(), + openapiv3::ReferenceOr::Item(item) => query_param(root, item), + } + }) + .collect::>(); + let path_params = operation + .parameters + .iter() + .flat_map(|p| { + match p { + // todo: need to handle this + openapiv3::ReferenceOr::Reference { .. } => todo!(), + openapiv3::ReferenceOr::Item(item) => path_param(root, item), + } + }) + .collect::>(); + + let mut ext_path = path.to_string(); + for param in &path_params { + if param.data_format() == "url" { + //info!("path: {path}"); + //info!("path_params: {param:?}"); + ext_path = path.replace(param.name(), &format!("{}:.*", param.base_name())); + vendor_extensions.insert("x-actix-query-string".into(), "true".into()); + } + } + vendor_extensions.insert("x-actixPath".into(), ext_path); + + let all_params = query_params + .iter() + .chain(&path_params) + .cloned() + .collect::>(); + // todo: support multiple responses + let return_model = match operation + .responses + .responses + .get(&openapiv3::StatusCode::Code(200)) + .or(operation + .responses + .responses + .get(&openapiv3::StatusCode::Code(204))) + { + Some(ref_or) => root.resolve_reference_or_resp("application/json", ref_or), + None => todo!(), + }; + // todo: should we post process after all operations are processed? + let return_model = return_model.post_process_data_type(); + let (class, class_file) = match operation.tags.first() { + Some(class) => (class.clone(), format!("{class}_api").to_snake_case()), + // How should this be handled? Shuld it be required? What if there's more than 1 tag? + None => (String::new(), String::new()), + }; + Self { + classname: class, + class_filename: class_file, + summary: operation.summary.clone(), + tags: operation.tags.clone(), + is_deprecated: Some(operation.deprecated), + operation_id_camel_case: operation + .operation_id + .as_ref() + .map(|o| o.to_lower_camel_case()), + operation_id: operation.operation_id.clone(), + operation_id_original: operation.operation_id.clone(), + has_params: !all_params.is_empty(), + all_params, + has_path_params: !path_params.is_empty(), + path_params, + has_query_params: !query_params.is_empty(), + query_params, + header_params: vec![], + has_header_params: false, + path: path.to_string(), + http_method: method.to_upper_camel_case(), + support_multiple_responses: false, + return_type: return_model.data_type(), + has_auth_methods: operation.security.is_some(), + vendor_extensions, + ..Default::default() + } + } + /// Get a reference to the operation tags list. + pub fn tags(&self) -> &Vec { + &self.tags + } + /// Get a reference to the operation class name. + pub fn classname(&self) -> &str { + &self.classname + } + /// Get a reference to the operation class filename. + pub fn class_filename(&self) -> &str { + &self.class_filename + } +} diff --git a/codegen/src/v3/parameter.rs b/codegen/src/v3/parameter.rs new file mode 100644 index 000000000..a7a80651e --- /dev/null +++ b/codegen/src/v3/parameter.rs @@ -0,0 +1,101 @@ +use heck::ToSnakeCase; +use ramhorns_derive::Content; +use std::collections::HashMap; + +use super::{property::Property, OpenApiV3}; + +#[derive(Default, Content, Clone, Debug)] +#[ramhorns(rename_all = "camelCase")] +pub(crate) struct Parameter { + param_name: String, + base_name: String, + example: Option, + examples: Vec, + required: bool, + deprecated: Option, + is_nullable: bool, + is_string: bool, + is_array: bool, + is_uuid: bool, + is_primitive_type: bool, + is_container: bool, + data_type: String, + data_format: String, + vendor_extensions: HashMap, + items: Option>, +} + +impl Parameter { + /// Create a new Parameter based on the deserialized parameter data. + pub(super) fn new(api: &OpenApiV3, param: &openapiv3::ParameterData) -> Self { + let schema_back; + let schema = match ¶m.format { + openapiv3::ParameterSchemaOrContent::Schema(ref_s) => match ref_s { + openapiv3::ReferenceOr::Reference { reference } => { + match api.api.components.as_ref().and_then(|c| { + c.schemas + .get(&reference.replace("#/components/schemas/", "")) + }) { + None => { + api.missing_schema_ref(reference); + schema_back = openapiv3::Schema { + schema_data: Default::default(), + schema_kind: openapiv3::SchemaKind::Any( + openapiv3::AnySchema::default(), + ), + }; + &schema_back + } + Some(ref_or) => match ref_or { + openapiv3::ReferenceOr::Reference { .. } => { + panic!("double reference not supported"); + } + openapiv3::ReferenceOr::Item(schema) => schema, + }, + } + } + openapiv3::ReferenceOr::Item(schema) => schema, + }, + openapiv3::ParameterSchemaOrContent::Content(_) => { + todo!() + } + }; + let property = Property::from_schema(api, None, schema, Some(¶m.name), None); + let property = super::OpenApiV3::post_process(property); + Self { + // todo: should have snake case param + param_name: param.name.to_snake_case(), + base_name: param.name.clone(), + example: param.example.as_ref().map(|v| v.to_string()), + examples: vec![], + required: param.required, + deprecated: param.deprecated, + is_nullable: schema.schema_data.nullable, + is_string: property.is_string(), + is_array: property.is_array(), + is_uuid: property.is_uuid(), + is_primitive_type: property.is_primitive_type(), + is_container: property.is_container(), + items: property.items().clone(), + data_type: property.data_type(), + data_format: property.data_format(), + vendor_extensions: param + .extensions + .iter() + .map(|(k, v)| (k.clone(), v.to_string())) + .collect(), + } + } + /// Get a reference to the parameter data type format. + pub fn data_format(&self) -> &str { + &self.data_format + } + /// Get a reference to the parameter base name (no case modifications). + pub fn base_name(&self) -> &str { + &self.base_name + } + /// Get a reference to the parameter name. + pub fn name(&self) -> &str { + &self.param_name + } +} diff --git a/codegen/src/v3/property.rs b/codegen/src/v3/property.rs new file mode 100644 index 000000000..20709a4ca --- /dev/null +++ b/codegen/src/v3/property.rs @@ -0,0 +1,968 @@ +use std::{collections::HashMap, fmt::Display, ops::Deref, rc::Rc}; + +use heck::{ToSnakeCase, ToUpperCamelCase}; +use ramhorns_derive::Content; + +/// The various openapi v3 property data types. +#[derive(Clone, Debug)] +pub(crate) enum PropertyDataType { + Unknown, + Resolved(String, Option), + Any, + RawString, + String(openapiv3::StringType), + Enum(String, String), + Boolean, + Integer(openapiv3::IntegerType), + Number(openapiv3::NumberType), + Model(String), + DiscModel(String, String), + Map(Box, Box), + Array(Box), + Empty, +} + +impl Display for PropertyDataType { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +impl PropertyDataType { + fn as_str(&self) -> &str { + match self { + PropertyDataType::Unknown => "Unkown", + PropertyDataType::Resolved(inner, _) => inner.as_ref(), + PropertyDataType::Array(inner) => inner.as_str(), + PropertyDataType::Any => "Any", + PropertyDataType::Map(_, _) => "Map", + PropertyDataType::RawString => "String", + PropertyDataType::String(_) => "String", + PropertyDataType::Enum(_, _) => "Enum", + PropertyDataType::Boolean => "bool", + PropertyDataType::Integer(_) => "integer", + PropertyDataType::Number(_) => "number", + PropertyDataType::Model(inner) => inner.as_str(), + PropertyDataType::DiscModel(_, _) => "Disc", + PropertyDataType::Empty => "Empty", + } + } + fn format(&self) -> Option<&String> { + match self { + PropertyDataType::Resolved(_, format) => format.as_ref(), + _ => None, + } + } + fn resolve(&mut self, data_type: &str) { + self.set_if_unresolved(Self::Resolved(data_type.into(), None)); + } + fn resolve_format>(&mut self, data_type: &str, format: T) { + self.set_if_unresolved(Self::Resolved(data_type.into(), Some(format.into()))); + } + fn resolve_format_opt(&mut self, data_type: &str, format: Option) { + self.set_if_unresolved(Self::Resolved(data_type.into(), format)); + } + fn set_string(&mut self, data_type: &openapiv3::StringType) { + self.set_if_unresolved(Self::String(data_type.clone())); + } + fn set_array(&mut self, data_type: &Self) { + self.set_if_unresolved(Self::Array(Box::new(data_type.clone()))); + } + fn set_boolean(&mut self) { + self.set_if_unresolved(Self::Boolean); + } + fn set_integer(&mut self, data_type: &openapiv3::IntegerType) { + self.set_if_unresolved(Self::Integer(data_type.clone())); + } + fn set_number(&mut self, data_type: &openapiv3::NumberType) { + self.set_if_unresolved(Self::Number(data_type.clone())); + } + fn set_model(&mut self, data_type: &str) { + self.set_if_unresolved(Self::Model(data_type.to_string())); + } + fn set_disc_model(&mut self, parent: String, name: &str) { + self.set_if_unresolved(Self::DiscModel(parent, name.to_string())); + } + fn set_map(&mut self, key: &Self, value: &Self) { + self.set_if_unresolved(Self::Map(Box::new(key.clone()), Box::new(value.clone()))); + } + fn set_enum(&mut self, name: &str, data_type: &str) { + self.set_if_unresolved(Self::Enum(name.to_string(), data_type.to_string())); + } + fn set_any(&mut self) { + self.set_if_unresolved(Self::Any); + } + fn set_if_unresolved(&mut self, to: Self) { + if !matches!(self, Self::Resolved(_, _)) { + *self = to; + } + } +} + +impl Default for PropertyDataType { + fn default() -> Self { + Self::Unknown + } +} + +impl ramhorns::Content for PropertyDataType { + #[inline] + fn is_truthy(&self) -> bool { + !self.as_str().is_empty() + } + + #[inline] + fn capacity_hint(&self, _tpl: &ramhorns::Template) -> usize { + self.as_str().len() + } + + #[inline] + fn render_escaped( + &self, + encoder: &mut E, + ) -> Result<(), E::Error> { + encoder.write_escaped(self.as_str()) + } + + #[inline] + fn render_unescaped( + &self, + encoder: &mut E, + ) -> Result<(), E::Error> { + encoder.write_unescaped(self.as_str()) + } +} + +/// A list of properties. +pub(crate) type Properties = Vec; + +/// An OpenApiV3 property of a Schema Object. +/// https://spec.openapis.org/oas/v3.0.3#properties +/// Including fixed fields, composition, etc. +/// These fields are used for both managing the template generation as well as input for +/// the templates themselves. +#[derive(Default, Content, Clone, Debug)] +#[ramhorns(rename_all = "camelCase")] +pub(crate) struct Property { + // The schema name as written in the OpenAPI document. + name: String, + + // The language-specific name of the "class" that implements this schema. + // The name of the class is derived from the OpenAPI schema name with formatting rules applied. + // The classname is derived from the OpenAPI schema name, with sanitization and escaping rules + // applied. + pub classname: String, + schema_name: String, + class_filename: String, + + base_name: String, + enum_name: Option, + // The value of the 'title' attribute in the OpenAPI document. + title: Option, + description: Option, + example: Option, + class_var_name: String, + model_json: String, + data_type: PropertyDataType, + data_format: String, + /// The type_ coming from component schema. + type_: String, + unescaped_description: String, + + /// Booleans for is_$-like type checking. + is_string: bool, + is_integer: bool, + is_long: bool, + is_number: bool, + is_numeric: bool, + is_float: bool, + is_double: bool, + is_date: bool, + is_date_time: bool, + is_password: bool, + is_decimal: bool, + is_binary: bool, + is_byte: bool, + is_short: bool, + is_unbounded_integer: bool, + is_primitive_type: bool, + is_boolean: bool, + is_uuid: bool, + is_any_type: bool, + is_enum: bool, + is_array: bool, + is_container: bool, + is_map: bool, + is_null: bool, + is_var: bool, + + /// Indicates whether additional properties has defined this as an Any type. + additional_properties_is_any_type: bool, + + /// If Self is an object, these are all its child properties. + vars: Properties, + /// And this? Inludes the parent properties? What does this mean? + all_vars: Properties, + + /// These could be "special" ramhorn methods rather than fields to avoid copy. + /// Only the required properties. + required_vars: Properties, + /// Only the optional properties. + optional_vars: Properties, + // Only the read-only properties. + read_only_vars: Properties, + // The read/write properties. + read_write_vars: Properties, + /// The Self's parent properties. + parent_vars: Properties, + + /// If this is an enum, all the allowed values. + allowable_values: HashMap>, + + /// If this is an array, the inner property of each index. + items: Option>, + + /// Indicates whether Self has child variables or not. + has_vars: bool, + /// Indicates whether there are enpty vars? What does this mean? + empty_vars: bool, + has_enums: bool, + /// Validation rules? Like patterns? + has_validation: bool, + /// Indicates the OAS schema specifies "nullable: true". + is_nullable: bool, + /// Indicates the type has at least one required property. + has_required: bool, + /// Indicates the type has at least one optional property. + has_optional: bool, + /// Indicates wether we have children vars? Or are these for inline schemas/properties? + has_children: bool, + + is_deprecated: bool, + has_only_read_only: bool, + required: bool, + max_properties: Option, + min_properties: Option, + unique_items: bool, + max_items: Option, + min_items: Option, + max_length: Option, + min_length: Option, + exclusive_minimum: bool, + exclusive_maximum: bool, + minimum: Option, + maximum: Option, + pattern: Option, + + /// If we are a schema defined model? + is_model: bool, + /// If we are a component model defined in the root component schemas: #/components/schemas. + is_component_model: bool, + + one_of: Properties, + all_of: Properties, + + /// Inline models discovered through the schema of this very model. + discovered_props: Rc, + + /// The parent property of this property, if this property is defined "inline" as an Item or a class member or item. + parent: Option>, +} + +impl Display for Property { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "{}/{}/{}.rs", + self.data_type(), + self.classname, + self.class_filename + ) + } +} + +impl Property { + /// Mutate the inner properties with the OpenAPI `openapiv3::SchemaData`. + pub fn with_data(mut self, data: &openapiv3::SchemaData) -> Self { + self.is_null = data.nullable; + self.is_nullable = data.nullable; + self.is_deprecated = data.deprecated; + self.title = data.title.clone(); + self.description = data + .description + .as_ref() + .map(|s| s.escape_default().to_string().replace("\\n", " ")); + self.example = data.example.as_ref().map(ToString::to_string); + self + } + /// Set wether the property is a model or not. + pub fn with_model(mut self, model: bool) -> Self { + self.is_model = model; + self + } + /// Set wether the property is a component model or not. + pub fn with_component_model(mut self, root_model: bool) -> Self { + if root_model { + self.is_component_model = true; + } + self + } + /// Get a reference to the property type. + pub fn type_ref(&self) -> &str { + &self.type_ + } + /// Get the property data type. + pub fn data_type(&self) -> String { + self.data_type.to_string() + } + /// Get the property data format. + pub fn data_format(&self) -> String { + self.data_type.format().map(Into::into).unwrap_or_default() + } + /// Get the class filename, if the property is a model. + pub fn filename(&self) -> &str { + self.class_filename.as_str() + } + /// Set the property data type. + pub fn with_data_property(mut self, type_: &PropertyDataType) -> Self { + self.data_type = type_.clone(); + self + } + /// Set the model type. + pub fn with_model_type(mut self, type_: &str) -> Self { + match self.parent() { + Some(parent) if type_.is_empty() => { + let parent_type = parent.type_.clone(); + self.data_type.set_disc_model(parent_type, &self.name); + } + _ => { + self.data_type.set_model(type_); + } + } + self + } + /// Set the data type Any, and if there's additional properties. + fn with_data_type_any(mut self, is_add_props: bool) -> Self { + self.data_type.set_any(); + self.is_any_type = true; + self.additional_properties_is_any_type = is_add_props; + self + } + /// Set the property type. + pub fn with_type(mut self, type_: &str) -> Self { + self.type_ = type_.to_string(); + self + } + /// The property is an OpenAPI AllOf, composed of a single property. + /// (This is because multiple properties is not supported yet) + pub fn with_one_all_of(self, single: Property) -> Self { + self.with_name(&single.name) + .with_type(&single.type_) + .with_data_property(&single.data_type) + .with_model(true) + .with_parent(&Some(&single)) + .with_all_of(vec![single]) + } + fn with_all_of(mut self, all_of: Vec) -> Self { + self.all_of = all_of; + self + } + /// Get a reference to the list of properties discovered through this property. + fn discovered_props(&self) -> &Vec { + &self.discovered_props + } + /// Similar as `discovered_props` but filters for models and applied recursively. + pub fn discovered_models(&self) -> Vec { + self.discovered_props() + .iter() + .flat_map(|m| { + let mut v = m.discovered_models(); + v.push(m.clone()); + v + }) + .filter(|p| !p.is_component_model && p.is_model && !p.is_all_of() && !p.is_enum) + .collect::>() + } +} +impl From<&openapiv3::SchemaData> for Property { + fn from(data: &openapiv3::SchemaData) -> Self { + Self::default().with_data(data) + } +} + +impl Property { + /// Create a `Property` from an OpenAPI schema, with some other information. + pub fn from_schema( + root: &super::OpenApiV3, + parent: Option<&Property>, + schema: &openapiv3::Schema, + name: Option<&str>, + type_: Option<&str>, + ) -> Self { + let name = name.unwrap_or_default(); + let type_ = type_.unwrap_or_default(); + trace!("PropertyFromSchema: {}/{}", name, type_); + let prop = Property::from(&schema.schema_data) + .with_name(name) + .with_parent(&parent) + .with_type(type_) + .with_component_model(root.contains_schema(type_)); + + prop.with_kind(root, schema, &schema.schema_kind, parent, name, type_) + } + + fn with_kind( + mut self, + root: &super::OpenApiV3, + schema: &openapiv3::Schema, + schema_kind: &openapiv3::SchemaKind, + parent: Option<&Self>, + name: &str, + type_: &str, + ) -> Self { + match schema_kind { + openapiv3::SchemaKind::Type(t) => match t { + openapiv3::Type::String(t) => self.with_string(root, t), + openapiv3::Type::Number(t) => self.with_number(root, t), + openapiv3::Type::Integer(t) => self.with_integer(root, t), + openapiv3::Type::Object(t) => self.with_model_type(type_).with_obj(root, t), + openapiv3::Type::Array(t) => self.with_array(root, t), + openapiv3::Type::Boolean(_) => { + self.data_type.set_boolean(); + self.is_boolean = true; + self.is_primitive_type = true; + self + } + }, + openapiv3::SchemaKind::OneOf { .. } => { + panic!("OneOf: {:#?} not implemented", schema); + } + openapiv3::SchemaKind::AllOf { all_of } if all_of.len() != 1 => { + unimplemented!() + } + openapiv3::SchemaKind::AllOf { all_of } => { + let first = all_of.first().unwrap(); + let first_model = root + .resolve_reference_or(first, parent, Some(name), None) + .with_data(&schema.schema_data); + Self::from(&schema.schema_data).with_one_all_of(first_model) + } + openapiv3::SchemaKind::AnyOf { .. } => { + unimplemented!() + } + openapiv3::SchemaKind::Not { .. } => { + unimplemented!() + } + // In some cases, we get Any rather than a specific kind :( + // For more info: https://github.com/glademiller/openapiv3/pull/79 + // todo: this needs a lot of tweaking... + openapiv3::SchemaKind::Any(any_schema) => match &any_schema.typ { + Some(typ) => match typ.as_str() { + "bool" => { + let kind = openapiv3::SchemaKind::Type(openapiv3::Type::Boolean( + openapiv3::BooleanType { + enumeration: vec![], + }, + )); + self.with_kind(root, schema, &kind, parent, name, type_) + } + "object" => self.with_model_type(type_).with_anyobj(root, any_schema), + not_handled => { + // See above, we must handle all types in the match :( + error!("BUG - must handle {not_handled} data type as AnySchema"); + self.with_data_type_any(false) + } + }, + // not sure how to handle this? default to Any for now. + None => self.with_data_type_any(false), + }, + } + } + + fn assign_classnames(&mut self) { + if self.classname.is_empty() && self.is_model && !self.is_var { + let schema_name = self.data_type.as_str(); + self.class_filename = schema_name.to_snake_case(); + self.classname = schema_name.to_upper_camel_case(); + } + self.assign_enumnames(); + } + fn assign_varnames(&mut self) { + if !self.name.is_empty() { + self.name = self.name.to_snake_case(); + } + } + fn assign_enumnames(&mut self) { + if self.is_enum { + self.enum_name = Some(self.data_type()); + } + } + fn string_format_str(format: openapiv3::StringFormat) -> &'static str { + match format { + openapiv3::StringFormat::Date => "date", + openapiv3::StringFormat::DateTime => "date-time", + openapiv3::StringFormat::Password => "password", + openapiv3::StringFormat::Byte => "byte", + openapiv3::StringFormat::Binary => "binary", + } + } + // This can be provided for a way of custumizing the types. + fn post_process_dt(data_type: &mut PropertyDataType, is_decl: bool) { + match data_type.clone() { + PropertyDataType::Unknown => {} + PropertyDataType::Resolved(_, _) => {} + PropertyDataType::Any => data_type.resolve("serde_json::Value"), + PropertyDataType::RawString => data_type.resolve("String"), + PropertyDataType::String(str) => { + match str.format { + openapiv3::VariantOrUnknownOrEmpty::Item(format) => { + // todo: handle these formats + data_type.resolve_format("String", Self::string_format_str(format)); + } + openapiv3::VariantOrUnknownOrEmpty::Unknown(format) => match format.as_str() { + "uuid" => data_type.resolve("uuid::Uuid"), + _ => data_type.resolve_format("String", format), + }, + openapiv3::VariantOrUnknownOrEmpty::Empty => { + data_type.resolve("String"); + } + } + } + PropertyDataType::Enum(name, type_) if !is_decl => { + let enum_ = if type_.is_empty() { name } else { type_ }.to_upper_camel_case(); + data_type.resolve(&format!("crate::models::{enum_}")) + } + PropertyDataType::Enum(name, type_) => { + let enum_ = if type_.is_empty() { name } else { type_ }.to_upper_camel_case(); + data_type.resolve(&enum_) + } + PropertyDataType::Boolean => data_type.resolve("bool"), + PropertyDataType::Integer(type_) => { + let (signed, bits, format) = match type_.format { + openapiv3::VariantOrUnknownOrEmpty::Item(item) => match item { + openapiv3::IntegerFormat::Int32 => (true, 32, Some("int32".into())), + openapiv3::IntegerFormat::Int64 => (true, 64, Some("int64".into())), + }, + openapiv3::VariantOrUnknownOrEmpty::Unknown(format) => match format.as_str() { + "uint32" => (false, 32, Some(format)), + "uint64" => (false, 64, Some(format)), + "int16" => (true, 16, Some(format)), + "uint16" => (false, 16, Some(format)), + "int8" => (true, 8, Some(format)), + "uint8" => (false, 8, Some(format)), + _ => (true, 0, Some(format)), + }, + _ => (true, 0, None), + }; + let signed = type_.minimum.map(|m| m < 0).unwrap_or(signed); + + // no format specified + let bits = if bits == 0 { + "size".to_string() + } else { + bits.to_string() + }; + + // todo: check min and max + data_type.resolve_format_opt( + &format!("{}{}", if signed { "i" } else { "u" }, bits), + format, + ) + } + PropertyDataType::Number(_) => data_type.resolve_format("u64", "u64"), + PropertyDataType::Model(model) if !is_decl => { + data_type.resolve(&format!("crate::models::{model}")) + } + PropertyDataType::Model(model) => data_type.resolve(&model), + PropertyDataType::DiscModel(parent, this) => { + let this = this.to_upper_camel_case(); + let parent = parent.to_upper_camel_case(); + if is_decl { + data_type.resolve(&format!("{parent}{this}")); + } else { + data_type.resolve(&format!("crate::models::{parent}{this}")); + } + } + PropertyDataType::Map(key, mut value) => { + Self::post_process_dt(&mut value, false); + data_type.resolve(&format!( + "::std::collections::HashMap<{}, {}>", + key.as_ref(), + value.as_str() + )) + } + PropertyDataType::Array(mut inner) => { + Self::post_process_dt(&mut inner, is_decl); + data_type.resolve(&format!("Vec<{}>", inner.as_str())) + } + PropertyDataType::Empty => data_type.resolve("()"), + } + } + /// This is a specific template hack, basically pretends this is not an enum + /// preventing it from being declared in the same module as the property where it was defined. + pub(crate) fn uninline_enums(&mut self) { + if self.is_var && self.is_component_model && self.is_enum { + // this is a very specific template hack? + self.is_enum = false; + } + } + /// Processes the data type for usage. + /// Properties which are not discovered at the top (eg: discovered via reference schema) get + /// a code import prefix added to them. + pub fn post_process(mut self) -> Property { + self.post_process_refmut(); + self + } + /// Process the data type for a non-declaration usage. + /// The property **will** get the code import prefix added. + pub fn post_process_data_type(mut self) -> Property { + Self::post_process_dt(&mut self.data_type, false); + self + } + fn post_process_refmut(&mut self) { + // 1. setup data type, eg: add crate::models:: prefix for import. + // This is not required if the type is declared in the same module which currently is only + // true for enums. + let mut is_decl = !self.is_var && !self.is_container; + if self.is_var && !self.is_component_model && self.is_enum { + is_decl = true; + } + Self::post_process_dt(&mut self.data_type, is_decl); + + // 2. fixup classname/type of non-enums defined within a type using Item + self.assign_classnames(); + // 3. setup var names to be snake case + self.assign_varnames(); + + // 4. Uninline enums to avoid inline code generation. + // todo: template itself should do this!? + self.uninline_enums(); + + // 5. apply the same logic for variables within this object. + for var in &mut self.vars { + var.post_process_refmut(); + } + for var in &mut self.required_vars { + var.post_process_refmut(); + } + for var in &mut self.optional_vars { + var.post_process_refmut(); + } + for var in &mut self.all_vars { + var.post_process_refmut(); + } + for var in &mut self.all_of { + var.post_process_refmut(); + } + for var in &mut self.one_of { + var.post_process_refmut(); + } + if let Some(item) = &mut self.items { + item.post_process_refmut(); + } + } + + fn parent(&self) -> Option<&Self> { + match &self.parent { + None => None, + Some(parent) => Some(parent.deref()), + } + } + /// Get a reference to the inner type of the collection. + pub fn items(&self) -> &Option> { + &self.items + } + /// Extend property with a new name. + pub fn with_name(mut self, name: &str) -> Self { + self.name = name.to_string(); + self.base_name = name.to_string(); + self + } + /// Extend property with a new is_var boolean. + fn with_is_var(mut self, is_var: bool) -> Self { + self.is_var = is_var; + self + } + /// Get a reference to the schema's data type. + /// # Warning: will panic if there is no data type (bug). + pub fn schema(&self) -> &str { + if self.data_type.as_str().is_empty() { + panic!("Schema data type should not be empty! Schema: {:#?}", self); + } + self.data_type.as_str() + } + /// Extend property with a new is_var boolean. + pub fn with_required(mut self, required: bool) -> Self { + self.required = required; + self + } + /// Extend property with a new parent property. + pub fn with_parent(mut self, parent: &Option<&Self>) -> Self { + self.parent = parent.map(|p| Rc::new(p.clone())); + self + } + /// Check if the property is a model. + pub fn is_model(&self) -> bool { + self.is_model + } + /// Check if the property is a string. + pub fn is_string(&self) -> bool { + self.is_string + } + /// Check if the property is an array. + pub fn is_array(&self) -> bool { + self.is_array + } + /// Check if the property is a string uuid. + pub fn is_uuid(&self) -> bool { + self.is_uuid + } + /// Check if the property is a container. + pub fn is_container(&self) -> bool { + self.is_container + } + /// Check if the property is a primitive type. + pub fn is_primitive_type(&self) -> bool { + self.is_primitive_type + } + /// Check if the property is an AllOf. + pub fn is_all_of(&self) -> bool { + !self.all_of.is_empty() + } + fn with_array(mut self, _root: &super::OpenApiV3, by: &openapiv3::ArrayType) -> Self { + self.items = by + .items + .clone() + .map(|i| _root.resolve_reference_or(&i.unbox(), Some(&self), None, None)) + .map(|i| i.with_is_var(true)) + .map(Box::new); + self.min_items = by.min_items; + self.max_items = by.max_items; + self.unique_items = by.unique_items; + self.is_array = true; + match &self.items { + Some(items) => { + self.data_type.set_array(&items.data_type); + } + None => { + panic!("BUG: an array without an inner type: {:?}", self); + } + } + self.is_container = true; + self + } + fn with_anyobj(mut self, root: &super::OpenApiV3, by: &openapiv3::AnySchema) -> Self { + self.min_properties = by.min_properties; + self.max_properties = by.max_properties; + + self.is_model = true; + + let vars = by + .properties + .iter() + .map(|(k, v)| root.resolve_reference_or(&v.clone().unbox(), Some(&self), Some(k), None)) + .map(|m| { + let required = by.required.contains(&m.name); + m.with_required(required) + }) + .collect::>(); + + let vars = vars + .into_iter() + .map(|p| p.with_is_var(true)) + .collect::>(); + + self.required_vars = vars + .iter() + .filter(|m| m.required) + .cloned() + .collect::>(); + self.optional_vars = vars + .iter() + .filter(|m| !m.required) + .cloned() + .collect::>(); + self.vars = vars; + + let mut vars_ = self.vars.iter().filter(|p| !p.required).collect::>(); + if vars_.len() != self.vars.len() { + panic!("Not Supported - all vars of oneOf must be optional"); + } + + let one_of = &by.one_of; + one_of + .iter() + .flat_map(|p| p.as_item()) + .map(|s| match &s.schema_kind { + openapiv3::SchemaKind::Any(schema) => schema, + _ => todo!(), + }) + .filter(|o| o.required.len() == 1) + .for_each(|o| vars_.retain(|v| v.name != o.required[0])); + + self.one_of = vec![self.clone()]; + self + } + fn with_obj(mut self, root: &super::OpenApiV3, by: &openapiv3::ObjectType) -> Self { + self.min_properties = by.min_properties; + self.max_properties = by.max_properties; + + if let Some(props) = &by.additional_properties { + match props { + openapiv3::AdditionalProperties::Any(any) => { + if *any { + return self.with_data_type_any(*any); + } + } + openapiv3::AdditionalProperties::Schema(ref_or) => match ref_or.deref() { + openapiv3::ReferenceOr::Reference { reference } => { + let inner = root.resolve_schema_name(None, reference); + self.data_type + .set_map(&PropertyDataType::RawString, &inner.data_type); + self.discovered_props = Rc::new(vec![inner]); + return self; + } + openapiv3::ReferenceOr::Item(item) => { + let property = Self::from_schema(root, None, item, None, None); + self.data_type + .set_map(&PropertyDataType::RawString, &property.data_type); + return self; + } + }, + } + } + + if !root.resolving(&self) { + let vars = by + .properties + .iter() + .map(|(k, v)| { + root.resolve_reference_or(&v.clone().unbox(), Some(&self), Some(k), None) + }) + .map(|m| { + let required = by.required.contains(&m.name); + m.with_required(required) + }) + .collect::>(); + + if vars.is_empty() { + return self.with_data_type_any(false); + } + self.is_model = true; + + self.discovered_props = Rc::new(vars.clone()); + let vars = vars + .into_iter() + .map(|p| p.with_is_var(true)) + .collect::>(); + + self.required_vars = vars + .iter() + .filter(|m| m.required) + .cloned() + .collect::>(); + self.optional_vars = vars + .iter() + .filter(|m| !m.required) + .cloned() + .collect::>(); + self.vars = vars; + } else { + // it's a circular reference, we must be a model + self.is_model = true; + } + + // if let Some(one_of) = &by.one_of { + // let mut vars_ = self.vars.iter().filter(|p| !p.required).collect::>(); + // if vars_.len() != self.vars.len() { + // panic!("Not Supported - all vars of oneOf must be optional"); + // } + // one_of + // .iter() + // .flat_map(|p| p.as_item()) + // .filter(|o| o.required.len() == 1) + // .for_each(|o| vars_.retain(|v| v.name != o.required[0])); + // if vars_.is_empty() { + // self.one_of = vec![self.clone()]; + // } else { + // panic!("OneOf with incorrect combination of required fields"); + // } + // } + self + } + fn with_integer(mut self, _root: &super::OpenApiV3, by: &openapiv3::IntegerType) -> Self { + self.exclusive_maximum = by.exclusive_maximum; + self.exclusive_minimum = by.exclusive_minimum; + self.minimum = by.minimum.map(|v| v.to_string()); + self.maximum = by.maximum.map(|v| v.to_string()); + self.is_integer = true; + self.is_primitive_type = true; + self.data_type.set_integer(by); + self + } + fn with_number(mut self, _root: &super::OpenApiV3, by: &openapiv3::NumberType) -> Self { + self.exclusive_maximum = by.exclusive_maximum; + self.exclusive_minimum = by.exclusive_minimum; + self.minimum = by.minimum.map(|v| v.to_string()); + self.maximum = by.maximum.map(|v| v.to_string()); + self.data_type.set_number(by); + self.is_primitive_type = true; + self + } + fn with_string(mut self, _root: &super::OpenApiV3, by: &openapiv3::StringType) -> Self { + self.pattern = by.pattern.clone(); + self.has_enums = !by.enumeration.is_empty(); + self.is_enum = self.has_enums; + + self.min_length = by.min_length; + self.data_type.set_string(by); + + match &by.format { + openapiv3::VariantOrUnknownOrEmpty::Item(item) => match item { + openapiv3::StringFormat::Date => self.is_date = true, + openapiv3::StringFormat::DateTime => self.is_date_time = true, + openapiv3::StringFormat::Password => self.is_date = true, + openapiv3::StringFormat::Byte => self.is_byte = true, + openapiv3::StringFormat::Binary => self.is_binary = true, + }, + openapiv3::VariantOrUnknownOrEmpty::Unknown(format) => match format.as_str() { + "uuid" => self.is_uuid = true, + "date" => self.is_date = true, + "date-time" => self.is_date_time = true, + _ => { + self.is_string = true; + } + }, + openapiv3::VariantOrUnknownOrEmpty::Empty => { + self.is_string = true; + } + } + + if self.is_enum { + let enum_vars = by + .enumeration + .iter() + .flatten() + .map(|v| EnumValue { + name: v.to_upper_camel_case(), + value: v.to_string(), + }) + .collect::>(); + + self.is_model = true; + self.allowable_values.insert("enumVars".into(), enum_vars); + self.data_type.set_enum(&self.name, &self.type_); + } else { + self.is_primitive_type = true; + } + + self + } +} + +#[derive(Default, Content, Clone, Debug)] +#[ramhorns(rename_all = "camelCase")] +pub(crate) struct EnumValue { + name: String, + value: String, +} diff --git a/codegen/src/v3/templates/default/Cargo.mustache b/codegen/src/v3/templates/default/Cargo.mustache new file mode 100644 index 000000000..32f59ccad --- /dev/null +++ b/codegen/src/v3/templates/default/Cargo.mustache @@ -0,0 +1,61 @@ +[package] +name = "{{{packageName}}}" +version = "{{{packageVersion}}}" +edition = "{{{packageEdition}}}" + +[lib] +name = "{{{packageLibname}}}" +path = "src/lib.rs" + +[features] +default = [ "tower-client-rls", "tower-trace" ] +actix-server = [ "actix" ] +actix-client = [ "actix", "actix-web-opentelemetry", "awc" ] +actix = [ "actix-web", "rustls" ] +tower-client-rls = [ "tower-client", "rustls_feat" ] +tower-client-tls = [ "tower-client", "hyper_tls_feat" ] +tower-client = [ "tower-hyper" ] +tower-hyper = [ "hyper", "tower", "tower-http", "http-body", "futures", "pin-project", "tokio" ] +hyper_tls_feat = [ "hyper-tls", "tokio-native-tls" ] +rustls_feat = [ "rustls", "webpki", "hyper-rustls" ] +tower-trace = [ "opentelemetry-jaeger", "tracing-opentelemetry", "opentelemetry", "opentelemetry-http", "tracing", "tracing-subscriber" ] + +[dependencies] +serde = "^1.0" +serde_derive = "^1.0" +serde_json = "^1.0" +url = { version = "^2.4", features = ["serde"] } +async-trait = "0.1.73" +dyn-clonable = "0.9.0" +uuid = { version = "1.4.1", features = ["serde", "v4"] } +serde_urlencoded = "0.7" + +# actix dependencies +actix-web = { version = "4.4.0", features = ["rustls-0_21"], optional = true } +actix-web-opentelemetry = { version = "0.17.0", optional = true } +awc = { version = "3.2.0", optional = true } + +# tower and hyper dependencies +hyper = { version = "0.14.27", features = [ "client", "http1", "http2", "tcp", "stream" ], optional = true } +tower = { version = "0.4.13", features = [ "timeout", "util", "limit" ], optional = true } +tower-http = { version = "0.4.4", features = [ "trace", "map-response-body", "auth" ], optional = true } +tokio = { version = "1.32.0", features = ["full"], optional = true } +http-body = { version = "0.4.5", optional = true } +futures = { version = "0.3.28", optional = true } +pin-project = { version = "1.1.3", optional = true } + +# SSL +rustls = { version = "0.21.12", optional = true, features = [ "dangerous_configuration" ] } +rustls-pemfile = "1.0.3" +webpki = { version = "0.22.2", optional = true } +hyper-rustls = { version = "0.24.1", optional = true } +hyper-tls = { version = "0.5.0", optional = true } +tokio-native-tls = { version = "0.3.1", optional = true } + +# tracing and telemetry +opentelemetry-jaeger = { version = "0.21.0", features = ["rt-tokio-current-thread"], optional = true } +tracing-opentelemetry = { version = "0.23.0", optional = true } +opentelemetry = { version = "0.22.0", optional = true } +opentelemetry-http = { version = "0.11.1", optional = true } +tracing = { version = "0.1.37", optional = true } +tracing-subscriber = { version = "0.3.18", optional = true } \ No newline at end of file diff --git a/codegen/src/v3/templates/default/README.md b/codegen/src/v3/templates/default/README.md new file mode 100644 index 000000000..e375a30f7 --- /dev/null +++ b/codegen/src/v3/templates/default/README.md @@ -0,0 +1,50 @@ +# Rust API client for {{{packageName}}} + +{{#appDescriptionWithNewLines}} +{{{appDescriptionWithNewLines}}} +{{/appDescriptionWithNewLines}} + +## Overview + +- API version: {{{appVersion}}} +- Package version: {{{packageVersion}}} +{{^hideGenerationTimestamp}} +- Build date: {{{generatedDate}}} +{{/hideGenerationTimestamp}} +- Build package: {{{generatorClass}}} +{{#infoUrl}} +For more information, please visit [{{{infoUrl}}}]({{{infoUrl}}}) +{{/infoUrl}} + +## Installation + +Put the package under your project folder and add the following to `Cargo.toml` under `[dependencies]`: + +``` + openapi = { path = "./generated" } +``` + +## Documentation for API Endpoints + +All URIs are relative to *{{{basePath}}}* + +Class | Method | HTTP request | Description +------------ | ------------- | ------------- | ------------- +{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}*{{{classname}}}* | [**{{{operationId}}}**]({{{apiDocPath}}}{{classname}}.md#{{{operationIdLowerCase}}}) | **{{{httpMethod}}}** {{{path}}} | {{#summary}}{{{summary}}}{{/summary}} +{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}} + +## Documentation For Models + +{{#models}}{{#model}} - [{{{classname}}}]({{{modelDocPath}}}{{{classname}}}.md) +{{/model}}{{/models}} + +To get access to the crate's generated documentation, use: + +``` +cargo doc --open +``` + +## Author + +{{#apiInfo}}{{#apis}}{{#-last}}{{{infoEmail}}} +{{/-last}}{{/apis}}{{/apiInfo}} \ No newline at end of file diff --git a/codegen/src/v3/templates/default/actix/client/api_clients.mustache b/codegen/src/v3/templates/default/actix/client/api_clients.mustache new file mode 100644 index 000000000..f386bdbe3 --- /dev/null +++ b/codegen/src/v3/templates/default/actix/client/api_clients.mustache @@ -0,0 +1,201 @@ +#![allow(clippy::vec_init_then_push)] + +use crate::clients::actix::{ + configuration, Error, ResponseContent, ResponseContentUnexpected, +}; +use actix_web_opentelemetry::ClientExt; +use std::rc::Rc; + +#[derive(Clone)] +pub struct {{{classname}}}Client { + configuration: Rc, +} + +impl {{{classname}}}Client { + pub fn new(configuration: Rc) -> Self { + Self { + configuration, + } + } +} + +#[async_trait::async_trait(?Send)] +#[dyn_clonable::clonable] +pub trait {{{classname}}}: Clone { + {{#operations}} + {{#operation}} + {{#description}} + /// {{{.}}} + {{/description}} + {{#notes}} + /// {{{.}}} + {{/notes}} + async fn {{{operationId}}}(&self, {{#allParams}}{{{paramName}}}: {{^required}}Option<{{/required}}{{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{#isString}}{{#isArray}}Vec<{{/isArray}}&str{{#isArray}}>{{/isArray}}{{/isString}}{{#isUuid}}{{#isArray}}Vec<{{/isArray}}&uuid::Uuid{{#isArray}}>{{/isArray}}{{/isUuid}}{{^isString}}{{^isUuid}}{{^isPrimitiveType}}{{^isContainer}}crate::models::{{/isContainer}}{{/isPrimitiveType}}{{{dataType}}}{{/isUuid}}{{/isString}}{{^required}}>{{/required}}{{#required}}{{#isNullable}}>{{/isNullable}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) -> Result<{{#supportMultipleResponses}}ResponseContent<{{{operationIdCamelCase}}}Success>{{/supportMultipleResponses}}{{^supportMultipleResponses}}{{^returnType}}(){{/returnType}}{{#returnType}}{{{returnType}}}{{/returnType}}{{/supportMultipleResponses}}, Error>; + {{/operation}} + {{/operations}} +} + +#[async_trait::async_trait(?Send)] +impl {{{classname}}} for {{{classname}}}Client { + {{#operations}} + {{#operation}} + {{#vendorExtensions.x-group-parameters}} + async fn {{{operationId}}}(&self{{#allParams}}{{#-first}}, params: {{{operationIdCamelCase}}}Params{{/-first}}{{/allParams}}) -> Result<{{#supportMultipleResponses}}ResponseContent<{{{operationIdCamelCase}}}Success>{{/supportMultipleResponses}}{{^supportMultipleResponses}}{{^returnType}}(){{/returnType}}{{#returnType}}{{{returnType}}}{{/returnType}}{{/supportMultipleResponses}}, Error> { + // unbox the parameters + {{#allParams}} + let {{paramName}} = params.{{paramName}}; + {{/allParams}} + + {{/vendorExtensions.x-group-parameters}} + {{^vendorExtensions.x-group-parameters}} + async fn {{{operationId}}}(&self, {{#allParams}}{{{paramName}}}: {{^required}}Option<{{/required}}{{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{#isString}}{{#isArray}}Vec<{{/isArray}}&str{{#isArray}}>{{/isArray}}{{/isString}}{{#isUuid}}{{#isArray}}Vec<{{/isArray}}&uuid::Uuid{{#isArray}}>{{/isArray}}{{/isUuid}}{{^isString}}{{^isUuid}}{{^isPrimitiveType}}{{^isContainer}}crate::models::{{/isContainer}}{{/isPrimitiveType}}{{{dataType}}}{{/isUuid}}{{/isString}}{{^required}}>{{/required}}{{#required}}{{#isNullable}}>{{/isNullable}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) -> Result<{{#supportMultipleResponses}}ResponseContent<{{{operationIdCamelCase}}}Success>{{/supportMultipleResponses}}{{^supportMultipleResponses}}{{^returnType}}(){{/returnType}}{{#returnType}}{{{returnType}}}{{/returnType}}{{/supportMultipleResponses}}, Error> { + {{/vendorExtensions.x-group-parameters}} + + let configuration = &self.configuration; + let local_var_client = &configuration.client; + + let local_var_uri_str = format!("{}{{{path}}}", configuration.base_path{{#pathParams}}, {{{baseName}}}={{#isString}}crate::apis::urlencode({{/isString}}{{{paramName}}}{{^required}}.unwrap_or_default(){{/required}}{{#required}}{{#isNullable}}.unwrap_or_default(){{/isNullable}}{{/required}}{{#isArray}}.join(",").as_ref(){{/isArray}}{{#isString}}){{/isString}}{{^isString}}.to_string(){{/isString}}{{/pathParams}}); + let mut local_var_req_builder = local_var_client.request(awc::http::Method::{{#vendorExtensions}}{{x-httpMethodUpper}}{{/vendorExtensions}}, local_var_uri_str.as_str()); + + {{#hasQueryParams}} + let mut query_params = vec![]; + {{#queryParams}} + {{#required}} + query_params.push(("{{{baseName}}}", {{{paramName}}}{{#isArray}}.into_iter().map(|p| p.to_string()).collect::>().join(","){{/isArray}}.to_string())); + {{/required}} + {{^required}} + if let Some(ref local_var_str) = {{{paramName}}} { + query_params.push(("{{{baseName}}}", local_var_str{{#isArray}}.into_iter().map(|p| p.to_string()).collect::>().join(","){{/isArray}}.to_string())); + } + {{/required}} + {{/queryParams}} + local_var_req_builder = local_var_req_builder.query(&query_params)?; + {{/hasQueryParams}} + {{#hasAuthMethods}} + {{#authMethods}} + {{#isApiKey}} + {{#isKeyInQuery}} + if let Some(ref local_var_apikey) = configuration.api_key { + let local_var_key = local_var_apikey.key.clone(); + let local_var_value = match local_var_apikey.prefix { + Some(ref local_var_prefix) => format!("{local_var_prefix} {local_var_key}"), + None => local_var_key, + }; + {{^hasQueryParams}}let mut query_params = vec![];{{/hasQueryParams}} + query_params.push(("{{{keyParamName}}}", local_var_value)); + local_var_req_builder = local_var_req_builder.query(&query_params)?; + } + {{/isKeyInQuery}} + {{/isApiKey}} + {{/authMethods}} + {{/hasAuthMethods}} + if let Some(ref local_var_user_agent) = configuration.user_agent { + local_var_req_builder = local_var_req_builder.insert_header((awc::http::header::USER_AGENT, local_var_user_agent.clone())); + } + {{#hasHeaderParams}} + {{#headerParams}} + {{#required}} + {{^isNullable}} + local_var_req_builder = local_var_req_builder.insert_header(("{{{baseName}}}", {{{paramName}}}{{#isArray}}.join(","){{/isArray}}.to_string())); + {{/isNullable}} + {{#isNullable}} + match {{{paramName}}} { + Some(local_var_param_value) => { local_var_req_builder = local_var_req_builder.insert_header(("{{{baseName}}}", local_var_param_value{{#isArray}}.join(","){{/isArray}}.to_string())); }, + None => { local_var_req_builder = local_var_req_builder.insert_header(("{{{baseName}}}", "")); }, + } + {{/isNullable}} + {{/required}} + {{^required}} + if let Some(local_var_param_value) = {{{paramName}}} { + local_var_req_builder = local_var_req_builder.insert_header(("{{{baseName}}}", local_var_param_value{{#isArray}}.join(","){{/isArray}}.to_string())); + } + {{/required}} + {{/headerParams}} + {{/hasHeaderParams}} + {{#hasAuthMethods}} + {{#authMethods}} + {{#isApiKey}} + {{#isKeyInHeader}} + if let Some(ref local_var_apikey) = configuration.api_key { + let local_var_key = local_var_apikey.key.clone(); + let local_var_value = match local_var_apikey.prefix { + Some(ref local_var_prefix) => format!("{local_var_prefix} {local_var_key}"), + None => local_var_key, + }; + local_var_req_builder = local_var_req_builder.insert_header(("{{{keyParamName}}}", local_var_value)); + } + {{/isKeyInHeader}} + {{/isApiKey}} + {{#isBasic}} + {{#isBasicBasic}} + if let Some(ref local_var_auth_conf) = configuration.basic_auth { + local_var_req_builder = local_var_req_builder.basic_auth(local_var_auth_conf.0.to_owned(), local_var_auth_conf.1.to_owned()); + } + {{/isBasicBasic}} + {{#isBasicBearer}} + if let Some(ref local_var_token) = configuration.bearer_access_token { + local_var_req_builder = local_var_req_builder.bearer_auth(local_var_token.to_owned()); + } + {{/isBasicBearer}} + {{/isBasic}} + {{#isOAuth}} + if let Some(ref local_var_token) = configuration.oauth_access_token { + local_var_req_builder = local_var_req_builder.bearer_auth(local_var_token.to_owned()); + } + {{/isOAuth}} + {{/authMethods}} + {{/hasAuthMethods}} + {{#isMultipart}} + $NOT_SUPPORTED$ + {{/isMultipart}} + {{#hasBodyParam}} + {{#bodyParam}} + let mut local_var_resp = if configuration.trace_requests { + local_var_req_builder.send_json(&{{{paramName}}}).await + } else { + local_var_req_builder.trace_request().send_json(&{{{paramName}}}).await + }?; + {{/bodyParam}} + {{/hasBodyParam}} + {{^hasBodyParam}} + let mut local_var_resp = if configuration.trace_requests { + local_var_req_builder.trace_request().send().await + } else { + local_var_req_builder.send().await + }?; + {{/hasBodyParam}} + + let local_var_status = local_var_resp.status(); + + if local_var_status.is_success() { + {{^supportMultipleResponses}} + {{^returnType}} + Ok(()) + {{/returnType}} + {{#returnType}} + let local_var_content = local_var_resp.json::<{{#supportMultipleResponses}}ResponseContent<{{{operationIdCamelCase}}}Success>{{/supportMultipleResponses}}{{^supportMultipleResponses}}{{^returnType}}(){{/returnType}}{{#returnType}}{{{returnType}}}{{/returnType}}{{/supportMultipleResponses}}>().await?; + Ok(local_var_content) + {{/returnType}} + {{/supportMultipleResponses}} + {{#supportMultipleResponses}} + let local_var_content = local_var_resp.json::<{{#supportMultipleResponses}}ResponseContent<{{{operationIdCamelCase}}}Success>{{/supportMultipleResponses}}{{^supportMultipleResponses}}{{^returnType}}(){{/returnType}}{{#returnType}}{{{returnType}}}{{/returnType}}{{/supportMultipleResponses}}>().await?; + let local_var_entity: Option<{{{operationIdCamelCase}}}Success> = serde_json::from_str(&local_var_content).ok(); + let local_var_result = ResponseContent { status: local_var_status, entity: local_var_entity }; + Ok(local_var_result) + {{/supportMultipleResponses}} + } else { + match local_var_resp.json::().await { + Ok(error) => Err(Error::ResponseError(ResponseContent { + status: local_var_status, + error, + })), + Err(_) => Err(Error::ResponseUnexpected(ResponseContentUnexpected { + status: local_var_status, + text: local_var_resp.json().await?, + })), + } + } + } + {{/operation}} + {{/operations}} +} \ No newline at end of file diff --git a/codegen/src/v3/templates/default/actix/client/client.mustache b/codegen/src/v3/templates/default/actix/client/client.mustache new file mode 100644 index 000000000..9681f9262 --- /dev/null +++ b/codegen/src/v3/templates/default/actix/client/client.mustache @@ -0,0 +1,134 @@ +pub mod configuration; + +pub use configuration::Configuration; +use std::{error, fmt, rc::Rc}; + +#[derive(Clone)] +pub struct ApiClient { +{{#apiInfo}} +{{#apis}} +{{#operations}} + {{{classFilename}}}: Box, +{{/operations}} +{{/apis}} +{{/apiInfo}} +} + +impl ApiClient { + pub fn new(configuration: Configuration) -> ApiClient { + let rc = Rc::new(configuration); + + ApiClient { +{{#apiInfo}} +{{#apis}} +{{#operations}} + {{^-last}} + {{{classFilename}}}: Box::new(crate::apis::{{{classFilename}}}::actix::client::{{{classname}}}Client::new(rc.clone())), + {{/-last}} + {{#-last}} + {{{classFilename}}}: Box::new(crate::apis::{{{classFilename}}}::actix::client::{{{classname}}}Client::new(rc)), + {{/-last}} +{{/operations}} +{{/apis}} +{{/apiInfo}} + } + } + +{{#apiInfo}} +{{#apis}} +{{#operations}} + pub fn {{{classFilename}}}(&self) -> &dyn crate::apis::{{{classFilename}}}::actix::client::{{{classname}}} { + self.{{{classFilename}}}.as_ref() + } +{{/operations}} +{{/apis}} +{{/apiInfo}} +} + +#[derive(Debug, Clone)] +pub struct ResponseContent { + pub status: awc::http::StatusCode, + pub error: T, +} + +#[derive(Debug, Clone)] +pub struct ResponseContentUnexpected { + pub status: awc::http::StatusCode, + pub text: String, +} + +#[derive(Debug)] +pub enum Error { + Request(awc::error::SendRequestError), + Serde(serde_json::Error), + SerdeEncoded(serde_urlencoded::ser::Error), + PayloadError(awc::error::JsonPayloadError), + Io(std::io::Error), + ResponseError(ResponseContent), + ResponseUnexpected(ResponseContentUnexpected), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let (module, e) = match self { + Error::Request(e) => ("request", e.to_string()), + Error::Serde(e) => ("serde", e.to_string()), + Error::SerdeEncoded(e) => ("serde", e.to_string()), + Error::PayloadError(e) => ("payload", e.to_string()), + Error::Io(e) => ("IO", e.to_string()), + Error::ResponseError(e) => ( + "response", + format!("status code '{}', content: '{:?}'", e.status, e.error), + ), + Error::ResponseUnexpected(e) => ( + "response", + format!("status code '{}', text '{}'", e.status, e.text), + ), + }; + write!(f, "error in {module}: {e}") + } +} + +impl error::Error for Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + Some(match self { + Error::Request(e) => e, + Error::Serde(e) => e, + Error::SerdeEncoded(e) => e, + Error::PayloadError(e) => e, + Error::Io(e) => e, + Error::ResponseError(_) => return None, + Error::ResponseUnexpected(_) => return None, + }) + } +} + +impl From for Error { + fn from(e: awc::error::SendRequestError) -> Self { + Error::Request(e) + } +} + +impl From for Error { + fn from(e: awc::error::JsonPayloadError) -> Self { + Error::PayloadError(e) + } +} + +impl From for Error { + fn from(e: serde_json::Error) -> Self { + Error::Serde(e) + } +} + +impl From for Error { + fn from(e: serde_urlencoded::ser::Error) -> Self { + Error::SerdeEncoded(e) + } +} + +impl From for Error { + fn from(e: std::io::Error) -> Self { + Error::Io(e) + } +} diff --git a/codegen/src/v3/templates/default/actix/client/configuration.mustache b/codegen/src/v3/templates/default/actix/client/configuration.mustache new file mode 100644 index 000000000..fda28e7e8 --- /dev/null +++ b/codegen/src/v3/templates/default/actix/client/configuration.mustache @@ -0,0 +1,105 @@ +#[derive(Clone)] +pub struct Configuration { + pub base_path: String, + pub user_agent: Option, + pub client: awc::Client, + pub basic_auth: Option, + pub oauth_access_token: Option, + pub bearer_access_token: Option, + pub api_key: Option, + pub trace_requests: bool, + // TODO: take an oauth2 token source, similar to the go one +} + +pub type BasicAuth = (String, Option); + +#[derive(Debug, Clone)] +pub struct ApiKey { + pub prefix: Option, + pub key: String, +} + +/// Configuration creation Error +#[derive(Debug)] +pub enum Error { + Certificate, +} + +impl Configuration { + /// New Configuration + pub fn new( + uri: &str, + timeout: std::time::Duration, + bearer_access_token: Option, + certificate: Option<&[u8]>, + trace_requests: bool, + ) -> Result { + let (client, url) = match certificate { + Some(bytes) => { + let cert_file = &mut std::io::BufReader::new(bytes); + + let mut config = rustls::ClientConfig::new(); + config + .root_store + .add_pem_file(cert_file) + .map_err(|_| Error::Certificate)?; + let connector = awc::Connector::new().rustls(std::sync::Arc::new(config)); + let client = awc::Client::builder() + .timeout(timeout) + .connector(connector) + .finish(); + + (client, format!("https://{uri}")) + } + None => { + let client = awc::Client::builder().timeout(timeout).finish(); + (client, format!("http://{uri}")) + } + }; + + Ok(Configuration { + base_path: url, + user_agent: None, + client, + basic_auth: None, + oauth_access_token: None, + bearer_access_token, + api_key: None, + trace_requests, + }) + } + + /// New Configuration with a provided client + pub fn new_with_client( + url: &str, + client: awc::Client, + bearer_access_token: Option, + trace_requests: bool, + ) -> Self { + Self { + base_path: url.to_string(), + user_agent: None, + client, + basic_auth: None, + oauth_access_token: None, + bearer_access_token, + api_key: None, + trace_requests, + } + } +} + +impl Default for Configuration { + fn default() -> Self { + Configuration { + base_path: "http://localhost/v0".to_owned(), + user_agent: Some("OpenAPI-Generator/v0/rust".to_owned()), + client: awc::Client::new(), + basic_auth: None, + oauth_access_token: None, + bearer_access_token: None, + api_key: None, + trace_requests: false, + } + } +} diff --git a/codegen/src/v3/templates/default/actix/mod.mustache b/codegen/src/v3/templates/default/actix/mod.mustache new file mode 100644 index 000000000..97528c6de --- /dev/null +++ b/codegen/src/v3/templates/default/actix/mod.mustache @@ -0,0 +1,4 @@ +#[cfg(feature = "actix-client")] +pub mod client; +#[cfg(feature = "actix-server")] +pub mod server; \ No newline at end of file diff --git a/codegen/src/v3/templates/default/actix/server/api.mustache b/codegen/src/v3/templates/default/actix/server/api.mustache new file mode 100644 index 000000000..714c23f45 --- /dev/null +++ b/codegen/src/v3/templates/default/actix/server/api.mustache @@ -0,0 +1,21 @@ +#![allow(missing_docs, trivial_casts, unused_variables, unused_mut, unused_imports, unused_extern_crates, non_camel_case_types)] + +use crate::apis::actix_server::{Body, Path, Query, RestError}; +use actix_web::web::Json; + +#[async_trait::async_trait] +pub trait {{{classname}}} { +{{#operations}} +{{#operation}} +{{#description}} + /// {{{.}}} +{{/description}} +{{#notes}} + /// {{{.}}} +{{/notes}} + async fn {{{operationId}}}({{#vendorExtensions.x-actix-query-string}}query: &str{{#hasParams}}, {{/hasParams}}{{/vendorExtensions.x-actix-query-string}}{{#hasPathParams}}Path({{#pathParams.1}}({{/pathParams.1}}{{#pathParams}}{{{paramName}}}{{^-last}}, {{/-last}}{{/pathParams}}{{#pathParams.1}}){{/pathParams.1}}): Path<{{#pathParams.1}}({{/pathParams.1}}{{#pathParams}}{{^required}}Option<{{/required}}{{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{#isString}}{{#isArray}}Vec<{{/isArray}}String{{#isArray}}>{{/isArray}}{{/isString}}{{#isUuid}}{{#isArray}}Vec<{{/isArray}}uuid::Uuid{{#isArray}}>{{/isArray}}{{/isUuid}}{{^isString}}{{^isUuid}}{{^isPrimitiveType}}{{^isContainer}}crate::models::{{/isContainer}}{{/isPrimitiveType}}{{{dataType}}}{{/isUuid}}{{/isString}}{{^required}}>{{/required}}{{#required}}{{#isNullable}}>{{/isNullable}}{{/required}}{{^-last}}, {{/-last}}{{/pathParams}}{{#pathParams.1}}){{/pathParams.1}}>{{/hasPathParams}}{{#hasQueryParams}}{{#hasPathParams}}, {{/hasPathParams}}Query({{#queryParams.1}}({{/queryParams.1}}{{#queryParams}}{{{paramName}}}{{^-last}}, {{/-last}}{{/queryParams}}{{#queryParams.1}}){{/queryParams.1}}): Query<{{#queryParams.1}}({{/queryParams.1}}{{#queryParams}}{{^required}}Option<{{/required}}{{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{#isString}}{{{dataType}}}{{/isString}}{{#isUuid}}uuid::Uuid{{/isUuid}}{{^isString}}{{^isUuid}}{{^isPrimitiveType}}{{^isContainer}}crate::models::{{/isContainer}}{{/isPrimitiveType}}{{{dataType}}}{{/isUuid}}{{/isString}}{{^required}}>{{/required}}{{#required}}{{#isNullable}}>{{/isNullable}}{{/required}}{{^-last}}, {{/-last}}{{/queryParams}}{{#queryParams.1}}){{/queryParams.1}}>{{/hasQueryParams}}{{#hasBodyParam}}{{#hasQueryParams}}, {{/hasQueryParams}}{{^hasQueryParams}}{{#hasPathParams}}, {{/hasPathParams}}{{/hasQueryParams}}{{#bodyParam}}Body({{{paramName}}}): Body<{{^required}}Option<{{/required}}{{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{#isString}}{{#isArray}}Vec<{{/isArray}}String{{#isArray}}>{{/isArray}}{{/isString}}{{#isUuid}}{{#isArray}}Vec<{{/isArray}}uuid::Uuid{{#isArray}}>{{/isArray}}{{/isUuid}}{{^isString}}{{^isUuid}}{{^isPrimitiveType}}{{^isContainer}}crate::models::{{/isContainer}}{{/isPrimitiveType}}{{{dataType}}}{{/isUuid}}{{/isString}}{{^required}}>{{/required}}{{#required}}{{#isNullable}}>{{/isNullable}}{{/required}}>{{/bodyParam}}{{/hasBodyParam}}) -> Result<{{#returnType}}{{/returnType}}{{#supportMultipleResponses}}ResponseContent<{{{operationIdCamelCase}}}Success>{{/supportMultipleResponses}}{{^supportMultipleResponses}}{{^returnType}}(){{/returnType}}{{#returnType}}{{{returnType}}}{{/returnType}}{{/supportMultipleResponses}}, RestError>; +{{/operation}} +{{/operations}} +} + +pub mod handlers; \ No newline at end of file diff --git a/codegen/src/v3/templates/default/actix/server/api_mod.mustache b/codegen/src/v3/templates/default/actix/server/api_mod.mustache new file mode 100644 index 000000000..61f7c438f --- /dev/null +++ b/codegen/src/v3/templates/default/actix/server/api_mod.mustache @@ -0,0 +1,251 @@ +use actix_web::http::StatusCode; +use actix_web::{web::ServiceConfig, FromRequest, HttpResponse, ResponseError}; +use serde::Serialize; +use std::{ + fmt::{self, Debug, Display, Formatter}, + ops, +}; + +{{#apiInfo}} +{{#apis}} +{{#operations}} +{{#operation}} +{{#-last}} +pub use crate::apis::{{{classFilename}}}::actix::server::{{{classname}}}; +{{/-last}} +{{/operation}} +{{/operations}} +{{/apis}} +{{/apiInfo}} + +/// Rest Error wrapper with a status code and a JSON error +/// Note: Only a single error type for each handler is supported at the moment +pub struct RestError { + status_code: StatusCode, + error_response: T, +} + +impl RestError { + pub fn new(status_code: StatusCode, error_response: T) -> Self { + Self { + status_code, + error_response + } + } +} + +impl Debug for RestError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("RestError") + .field("status_code", &self.status_code) + .field("error_response", &self.error_response) + .finish() + } +} + +impl Display for RestError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{self:?}") + } +} + +impl ResponseError for RestError { + fn status_code(&self) -> StatusCode { + self.status_code + } + + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code).json(&self.error_response) + } +} + +/// 204 Response with no content +#[derive(Default)] +pub(crate) struct NoContent; + +impl From> for NoContent { + fn from(_: actix_web::web::Json<()>) -> Self { + NoContent {} + } +} +impl From<()> for NoContent { + fn from(_: ()) -> Self { + NoContent {} + } +} +impl actix_web::Responder for NoContent { + {{^actixWeb4Beta}}type Body = actix_web::body::BoxBody;{{/actixWeb4Beta}} + + fn respond_to(self, _: &actix_web::HttpRequest) -> actix_web::HttpResponse { + actix_web::HttpResponse::NoContent().finish() + } +} + +/// Wrapper type used as tag to easily distinguish the 3 different parameter types: +/// 1. Path 2. Query 3. Body +/// Example usage: +/// fn delete_resource(Path((p1, p2)): Path<(String, u64)>) { ... } +pub struct Path(pub T); + +impl Path { + /// Deconstruct to an inner value + pub fn into_inner(self) -> T { + self.0 + } +} + +impl AsRef for Path { + fn as_ref(&self) -> &T { + &self.0 + } +} + +impl ops::Deref for Path { + type Target = T; + + fn deref(&self) -> &T { + &self.0 + } +} + +impl ops::DerefMut for Path { + fn deref_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +/// Wrapper type used as tag to easily distinguish the 3 different parameter types: +/// 1. Path 2. Query 3. Body +/// Example usage: +/// fn delete_resource(Path((p1, p2)): Path<(String, u64)>) { ... } +pub struct Query(pub T); + +impl Query { + /// Deconstruct to an inner value + pub fn into_inner(self) -> T { + self.0 + } +} + +impl AsRef for Query { + fn as_ref(&self) -> &T { + &self.0 + } +} + +impl ops::Deref for Query { + type Target = T; + + fn deref(&self) -> &T { + &self.0 + } +} + +impl ops::DerefMut for Query { + fn deref_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +/// Wrapper type used as tag to easily distinguish the 3 different parameter types: +/// 1. Path 2. Query 3. Body +/// Example usage: +/// fn delete_resource(Path((p1, p2)): Path<(String, u64)>) { ... } +pub struct Body(pub T); + +impl Body { + /// Deconstruct to an inner value + pub fn into_inner(self) -> T { + self.0 + } +} + +impl AsRef for Body { + fn as_ref(&self) -> &T { + &self.0 + } +} + +impl ops::Deref for Body { + type Target = T; + + fn deref(&self) -> &T { + &self.0 + } +} + +impl ops::DerefMut for Body { + fn deref_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +/// Configure all actix server handlers +pub fn configure(cfg: &mut ServiceConfig) { +{{#apiInfo}} +{{#apis}} +{{#operations}} +{{#operation}} +{{#-last}} + crate::apis::{{{classFilename}}}::actix::server::handlers::configure::(cfg); +{{/-last}} +{{/operation}} +{{/operations}} +{{/apis}} +{{/apiInfo}} +} + +/// Used with Query to deserialize into Vec. +#[allow(dead_code)] +pub(crate) fn deserialize_stringified_list<'de, D, I>( + deserializer: D, +) -> std::result::Result, D::Error> +where + D: serde::de::Deserializer<'de>, + I: serde::de::DeserializeOwned, +{ + struct StringVecVisitor(std::marker::PhantomData); + + impl<'de, I> serde::de::Visitor<'de> for StringVecVisitor + where + I: serde::de::DeserializeOwned, + { + type Value = Vec; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string containing a list") + } + + fn visit_str(self, v: &str) -> std::result::Result + where + E: serde::de::Error, + { + let mut list = Vec::new(); + if !v.is_empty() { + for item in v.split(',') { + let item = I::deserialize(serde::de::IntoDeserializer::into_deserializer(item))?; + list.push(item); + } + } + Ok(list) + } + } + + deserializer.deserialize_any(StringVecVisitor(std::marker::PhantomData::)) +} + +/// Used with Query to deserialize into Option>. +#[allow(dead_code)] +pub(crate) fn deserialize_option_stringified_list<'de, D, I>( + deserializer: D, +) -> std::result::Result>, D::Error> +where + D: serde::de::Deserializer<'de>, + I: serde::de::DeserializeOwned, +{ + let list = deserialize_stringified_list(deserializer)?; + match list.is_empty() { + true => Ok(None), + false => Ok(Some(list)), + } +} \ No newline at end of file diff --git a/codegen/src/v3/templates/default/actix/server/handlers.mustache b/codegen/src/v3/templates/default/actix/server/handlers.mustache new file mode 100644 index 000000000..03b44b79d --- /dev/null +++ b/codegen/src/v3/templates/default/actix/server/handlers.mustache @@ -0,0 +1,60 @@ +#![allow(missing_docs, trivial_casts, unused_variables, unused_mut, unused_imports, unused_extern_crates, non_camel_case_types)] + +use crate::{ + actix::server::{deserialize_option_stringified_list, deserialize_stringified_list}, + apis::{ + actix_server::{Body, NoContent, RestError}, + {{{classFilename}}}::actix::server, + }, +}; +use actix_web::{ + web::{Json, Path, Query, ServiceConfig}, + FromRequest, HttpRequest, +}; + + +/// Configure handlers for the {{{classname}}} resource +pub fn configure(cfg: &mut ServiceConfig) { + cfg +{{#operations}} +{{#operation}} + .service( + actix_web::web::resource("{{#vendorExtensions.x-actixPath}}{{{vendorExtensions.x-actixPath}}}{{/vendorExtensions.x-actixPath}}{{^vendorExtensions.x-actixPath}}{{{path}}}{{/vendorExtensions.x-actixPath}}") + .name("{{{operationId}}}") + .guard(actix_web::guard::{{{httpMethod}}}()) + .route(actix_web::web::{{#vendorExtensions}}{{x-httpMethodLower}}{{/vendorExtensions}}().to({{{operationId}}}::)) + ){{#-last}};{{/-last}} +{{/operation}} +{{/operations}} +} + +{{#operations}} +{{#operation}} +{{#hasQueryParams}} +#[derive(serde::Deserialize)] +struct {{{operationId}}}QueryParams { +{{#queryParams}} + {{#description}}/// {{{description}}}{{/description}} + #[serde(rename = "{{{baseName}}}"{{^required}}, default, skip_serializing_if = "Option::is_none"{{#isContainer}}{{#items}}{{#isString}}, deserialize_with = "deserialize_option_stringified_list"{{/isString}}{{/items}}{{/isContainer}}{{/required}}{{#required}}{{#isContainer}}{{#items}}{{#isString}}, deserialize_with = "deserialize_stringified_list"{{/isString}}{{/items}}{{/isContainer}}{{/required}})] + pub {{{paramName}}}: {{^required}}Option<{{/required}}{{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{#isString}}{{{dataType}}}{{/isString}}{{#isUuid}}uuid::Uuid{{/isUuid}}{{^isString}}{{^isUuid}}{{^isPrimitiveType}}{{^isContainer}}crate::models::{{/isContainer}}{{/isPrimitiveType}}{{{dataType}}}{{/isUuid}}{{/isString}}{{^required}}>{{/required}}{{#required}}{{#isNullable}}>{{/isNullable}}{{/required}}, +{{/queryParams}} +} +{{/hasQueryParams}} +{{/operation}} +{{/operations}} + +{{#operations}} +{{#operation}} +{{#description}} +/// {{{.}}} +{{/description}} +{{#notes}} +/// {{{.}}} +{{/notes}} +async fn {{{operationId}}}({{#vendorExtensions.x-actix-query-string}}request: HttpRequest{{#hasParams}}, {{/hasParams}}{{^hasParams}}{{#hasAuthMethods}}, {{/hasAuthMethods}}{{/hasParams}}{{/vendorExtensions.x-actix-query-string}}{{#hasAuthMethods}}_token: A{{#hasParams}}, {{/hasParams}}{{/hasAuthMethods}}{{#hasPathParams}}path: Path<{{#pathParams.1}}({{/pathParams.1}}{{#pathParams}}{{^required}}Option<{{/required}}{{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{#isString}}{{#isArray}}Vec<{{/isArray}}String{{#isArray}}>{{/isArray}}{{/isString}}{{#isUuid}}{{#isArray}}Vec<{{/isArray}}uuid::Uuid{{#isArray}}>{{/isArray}}{{/isUuid}}{{^isString}}{{^isUuid}}{{^isPrimitiveType}}{{^isContainer}}crate::models::{{/isContainer}}{{/isPrimitiveType}}{{{dataType}}}{{/isUuid}}{{/isString}}{{^required}}>{{/required}}{{#required}}{{#isNullable}}>{{/isNullable}}{{/required}}{{^-last}}, {{/-last}}{{/pathParams}}{{#pathParams.1}}){{/pathParams.1}}>{{/hasPathParams}}{{#hasQueryParams}}{{#hasPathParams}}, {{/hasPathParams}}query: Query<{{{operationId}}}QueryParams>{{/hasQueryParams}}{{#hasBodyParam}}{{#hasQueryParams}}, {{/hasQueryParams}}{{^hasQueryParams}}{{#hasPathParams}}, {{/hasPathParams}}{{/hasQueryParams}}{{#bodyParam}}Json({{{paramName}}}): Json<{{^required}}Option<{{/required}}{{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{#isString}}{{#isArray}}Vec<{{/isArray}}{{{dataType}}}{{#isArray}}>{{/isArray}}{{/isString}}{{#isUuid}}{{#isArray}}Vec<{{/isArray}}String{{#isArray}}>{{/isArray}}{{/isUuid}}{{^isString}}{{^isUuid}}{{^isPrimitiveType}}{{^isContainer}}crate::models::{{/isContainer}}{{/isPrimitiveType}}{{{dataType}}}{{/isUuid}}{{/isString}}{{^required}}>{{/required}}{{#required}}{{#isNullable}}>{{/isNullable}}{{/required}}>{{/bodyParam}}{{/hasBodyParam}}) -> Result<{{#supportMultipleResponses}}Json>{{/supportMultipleResponses}}{{^supportMultipleResponses}}{{^returnType}}NoContent{{/returnType}}{{#returnType}}Json<{{{returnType}}}>{{/returnType}}{{/supportMultipleResponses}}, RestError> { + {{#hasQueryParams}}let query = query.into_inner(); + {{/hasQueryParams}}T::{{{operationId}}}({{#vendorExtensions.x-actix-query-string}}request.query_string(){{#hasParams}}, {{/hasParams}}{{/vendorExtensions.x-actix-query-string}}{{#hasPathParams}}crate::apis::actix_server::Path(path.into_inner()){{/hasPathParams}}{{#hasQueryParams}}{{#hasPathParams}}, {{/hasPathParams}}crate::apis::actix_server::Query({{#queryParams.1}}({{/queryParams.1}}{{#queryParams}}query.{{{paramName}}}{{^-last}}, {{/-last}}{{/queryParams}}{{#queryParams.1}}){{/queryParams.1}}){{/hasQueryParams}}{{#hasBodyParam}}{{#hasQueryParams}}, {{/hasQueryParams}}{{^hasQueryParams}}{{#hasPathParams}}, {{/hasPathParams}}{{/hasQueryParams}}{{#bodyParam}}Body({{{paramName}}}){{/bodyParam}}{{/hasBodyParam}}).await.map(Json){{^supportMultipleResponses}}{{^returnType}}.map(Into::into){{/returnType}}{{/supportMultipleResponses}} +} + +{{/operation}} +{{/operations}} diff --git a/codegen/src/v3/templates/default/api_doc.mustache b/codegen/src/v3/templates/default/api_doc.mustache new file mode 100644 index 000000000..250db3fc5 --- /dev/null +++ b/codegen/src/v3/templates/default/api_doc.mustache @@ -0,0 +1,47 @@ +# {{#hasInvokerPackage}}{{{invokerPackage}}}\{{/hasInvokerPackage}}{{{classname}}}{{#description}} + +{{{description}}}{{/description}} + +All URIs are relative to *{{{basePath}}}* + +Method | HTTP request | Description +------------- | ------------- | ------------- +{{#operations}}{{#operation}}[**{{{operationId}}}**]({{{classname}}}.md#{{{operationId}}}) | **{{{httpMethod}}}** {{{path}}} | {{#summary}}{{{summary}}}{{/summary}} +{{/operation}}{{/operations}} + +{{#operations}} +{{#operation}} + +## {{{operationId}}} + +> {{#returnType}}{{{returnType}}} {{/returnType}}{{{operationId}}}({{#allParams}}{{{paramName}}}{{^-last}}, {{/-last}}{{/allParams}}) +{{{summary}}}{{#notes}} + +{{{notes}}}{{/notes}} + +### Parameters + +{{^allParams}}This endpoint does not need any parameter.{{/allParams}}{{#allParams}}{{#-last}} +Name | Type | Description | Required | Notes +------------- | ------------- | ------------- | ------------- | -------------{{/-last}}{{/allParams}} +{{#allParams}} +**{{{paramName}}}** | {{^required}}Option<{{/required}}{{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{#isPrimitiveType}}**{{{dataType}}}**{{/isPrimitiveType}}{{^isPrimitiveType}}[**{{{dataType}}}**]({{{baseType}}}.md){{/isPrimitiveType}}{{^required}}>{{/required}}{{#required}}{{#isNullable}}>{{/isNullable}}{{/required}} | {{{description}}} | {{#required}}[required]{{/required}} |{{#defaultValue}}[default to {{{defaultValue}}}]{{/defaultValue}} +{{/allParams}} + +### Return type + +{{#returnType}}{{#returnTypeIsPrimitive}}**{{{returnType}}}**{{/returnTypeIsPrimitive}}{{^returnTypeIsPrimitive}}[**{{{returnType}}}**]({{{returnBaseType}}}.md){{/returnTypeIsPrimitive}}{{/returnType}}{{^returnType}} (empty response body){{/returnType}} + +### Authorization + +{{^authMethods}}No authorization required{{/authMethods}}{{#authMethods}}[{{{name}}}](../README.md#{{{name}}}){{^-last}}, {{/-last}}{{/authMethods}} + +### HTTP request headers + +- **Content-Type**: {{#consumes}}{{{mediaType}}}{{^-last}}, {{/-last}}{{/consumes}}{{^consumes}}Not defined{{/consumes}} +- **Accept**: {{#produces}}{{{mediaType}}}{{^-last}}, {{/-last}}{{/produces}}{{^produces}}Not defined{{/produces}} + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +{{/operation}} +{{/operations}} \ No newline at end of file diff --git a/codegen/src/v3/templates/default/api_mod.mustache b/codegen/src/v3/templates/default/api_mod.mustache new file mode 100644 index 000000000..d19d3462a --- /dev/null +++ b/codegen/src/v3/templates/default/api_mod.mustache @@ -0,0 +1,55 @@ +{{#apiInfo}} +{{#apis}} +pub mod {{{classFilename}}}; +{{/apis}} +{{/apiInfo}} + +/// Actix server. +#[cfg(feature = "actix-server")] +pub mod actix_server; + +#[cfg(feature = "tower-hyper")] +pub use hyper::http::StatusCode; + +#[cfg(not(feature = "tower-hyper"))] +#[cfg(feature = "actix")] +pub use actix_web::http::StatusCode; + +/// Url. +pub use url::Url; +/// Uuid. +pub use uuid::Uuid; + +/// Encode string to use in a URL. +pub fn urlencode>(s: T) -> String { + ::url::form_urlencoded::byte_serialize(s.as_ref().as_bytes()).collect() +} + +/// Helper to convert from Vec into Vec. +pub trait IntoVec: Sized { + /// Performs the conversion. + fn into_vec(self) -> Vec; +} + +impl, T> IntoVec for Vec { + fn into_vec(self) -> Vec { + self.into_iter().map(Into::into).collect() + } +} + +/// Helper to convert from Vec or Option> into Option>. +pub trait IntoOptVec: Sized { + /// Performs the conversion. + fn into_opt_vec(self) -> Option>; +} + +impl, T> IntoOptVec for Vec { + fn into_opt_vec(self) -> Option> { + Some(self.into_iter().map(Into::into).collect()) + } +} +impl, T> IntoOptVec for Option> { + fn into_opt_vec(self) -> Option> { + self.map(|s| s.into_iter().map(Into::into).collect()) + } +} diff --git a/codegen/src/v3/templates/default/gitignore.mustache b/codegen/src/v3/templates/default/gitignore.mustache new file mode 100644 index 000000000..eccd7b4ab --- /dev/null +++ b/codegen/src/v3/templates/default/gitignore.mustache @@ -0,0 +1,2 @@ +/target/ +**/*.rs.bk diff --git a/codegen/src/v3/templates/default/lib.mustache b/codegen/src/v3/templates/default/lib.mustache new file mode 100644 index 000000000..2ad74474f --- /dev/null +++ b/codegen/src/v3/templates/default/lib.mustache @@ -0,0 +1,24 @@ +#[macro_use] +extern crate serde_derive; + +extern crate serde; +extern crate serde_json; +extern crate url; + +pub mod apis; +pub mod models; +pub mod clients; + +#[cfg(feature = "tower-hyper")] +pub mod tower { + pub use crate::clients::tower as client; +} + +#[cfg(feature = "actix")] +pub mod actix { + #[cfg(feature = "actix-client")] + pub use crate::clients::actix as client; + + #[cfg(feature = "actix-server")] + pub use crate::apis::actix_server as server; +} \ No newline at end of file diff --git a/codegen/src/v3/templates/default/mod.mustache b/codegen/src/v3/templates/default/mod.mustache new file mode 100644 index 000000000..a7e8d7d21 --- /dev/null +++ b/codegen/src/v3/templates/default/mod.mustache @@ -0,0 +1,4 @@ +#[cfg(feature = "actix")] +pub mod actix; +#[cfg(feature = "tower-hyper")] +pub mod tower; diff --git a/codegen/src/v3/templates/default/mod_clients.mustache b/codegen/src/v3/templates/default/mod_clients.mustache new file mode 100644 index 000000000..3ee07f2de --- /dev/null +++ b/codegen/src/v3/templates/default/mod_clients.mustache @@ -0,0 +1,9 @@ +#[cfg(feature = "actix-client")] +pub mod actix; +#[cfg(feature = "tower-client")] +pub mod tower; + +// Enable once we move to rust-2021 +// #[cfg(all(feature = "tower-client-rls", feature = "tower-client-tls"))] +// compile_error!("feature \"tower-client-rls\" and feature \"tower-client-tls\" cannot be enabled +// at the same time"); \ No newline at end of file diff --git a/codegen/src/v3/templates/default/model.mustache b/codegen/src/v3/templates/default/model.mustache new file mode 100644 index 000000000..fd615e34d --- /dev/null +++ b/codegen/src/v3/templates/default/model.mustache @@ -0,0 +1,129 @@ +#![allow(clippy::too_many_arguments, clippy::new_without_default, non_camel_case_types, unused_imports)] + +use crate::apis::{IntoOptVec, IntoVec}; + +{{#models}} +{{#model}} +{{#description}}/// {{{classname}}} : {{{description}}}{{/description}} + +{{!-- for enum schemas --}} +{{#isEnum}} +/// {{{description}}} +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +pub enum {{{classname}}} { +{{#allowableValues}} +{{#enumVars}} + #[serde(rename = "{{{value}}}")] + {{{name}}}, +{{/enumVars}}{{/allowableValues}} +} + +impl ToString for {{{classname}}} { + fn to_string(&self) -> String { + match self { + {{#allowableValues}} + {{#enumVars}} + Self::{{{name}}} => String::from("{{{value}}}"), + {{/enumVars}} + {{/allowableValues}} + } + } +} + +impl Default for {{{classname}}} { + fn default() -> Self { + Self::{{#allowableValues}}{{#enumVars}}{{#-first}}{{{name}}}{{/-first}}{{/enumVars}}{{/allowableValues}} + } +} + +{{/isEnum}} +{{!-- for schemas that have a discriminator --}} +{{#discriminator}} +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(tag = "{{{vendorExtensions.x-tag-name}}}")] +pub enum {{{classname}}} { +{{#vendorExtensions}} + {{#x-mapped-models}} + #[serde(rename="{{mappingName}}")] + {{{modelName}}} { + {{#vars}} + {{#description}}/// {{{description}}}{{/description}} + #[serde(rename = "{{{baseName}}}"{{^required}}, skip_serializing_if = "Option::is_none"{{/required}})] + {{{name}}}: {{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{^required}}Option<{{/required}}{{#isEnum}}{{{enumName}}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#required}}{{#isNullable}}>{{/isNullable}}{{/required}}{{^required}}>{{/required}}, + {{/vars}} + }, + {{/x-mapped-models}} +{{/vendorExtensions}} +} + +{{/discriminator}} +{{!-- for non-enum schemas --}} +{{^isEnum}} +{{^discriminator}} +{{#description}}/// {{{description}}}{{/description}} +{{^oneOf}} +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +pub struct {{{classname}}} { +{{#vars}} + {{#description}}/// {{{description}}}{{/description}} + #[serde(default, rename = "{{{baseName}}}"{{^required}}, skip_serializing_if = "Option::is_none"{{/required}})] + pub {{{name}}}: {{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{^required}}Option<{{/required}}{{#isEnum}}{{#isArray}}{{#uniqueItems}}std::collections::HashSet<{{/uniqueItems}}{{^uniqueItems}}Vec<{{/uniqueItems}}{{/isArray}}{{{enumName}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#required}}{{#isNullable}}>{{/isNullable}}{{/required}}{{^required}}>{{/required}}, +{{/vars}} +} + +impl {{{classname}}} { + /// {{{classname}}} using only the required fields + pub fn new({{#requiredVars}}{{{name}}}: {{#isArray}}impl IntoVec<{{#items}}{{dataType}}{{/items}}>{{/isArray}}{{^isArray}}impl Into<{{#isNullable}}Option<{{/isNullable}}{{#isEnum}}{{{enumName}}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#isNullable}}{{/isNullable}}>{{/isArray}}{{^-last}}, {{/-last}}{{/requiredVars}}) -> {{{classname}}} { + {{{classname}}} { + {{#vars}}{{{name}}}{{#required}}{{#isArray}}: {{{name}}}.into_vec(){{/isArray}}{{^isArray}}: {{{name}}}.into(){{/isArray}}{{/required}}{{^required}}{{#isArray}}: None{{/isArray}}{{#isMap}}: None{{/isMap}}{{^isContainer}}: None{{/isContainer}}{{/required}}{{#required}}{{/required}}, + {{/vars}} + } + } + /// {{{classname}}} using all fields + pub fn new_all({{#vars}}{{{name}}}: {{#isArray}}impl Into{{^required}}Opt{{/required}}Vec<{{#items}}{{dataType}}{{/items}}>{{/isArray}}{{^isArray}}impl Into<{{^required}}Option<{{/required}}{{#isNullable}}Option<{{/isNullable}}{{#isEnum}}{{{enumName}}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#isNullable}}>{{/isNullable}}{{^required}}>{{/required}}>{{/isArray}}{{^-last}}, {{/-last}}{{/vars}}) -> {{{classname}}} { + {{{classname}}} { + {{#vars}}{{{name}}}: {{{name}}}{{#isArray}}.into_{{^required}}opt_{{/required}}vec(){{/isArray}}{{^isArray}}.into(){{/isArray}}, + {{/vars}} + } + } +} +{{/oneOf}} +{{#oneOf}} +{{#-first}} +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub enum {{{classname}}} { +{{#vars}} + {{#description}}/// {{{description}}}{{/description}} + #[serde(rename = "{{{baseName}}}")] + {{{name}}}({{#isNullable}}Option<{{/isNullable}}{{#isEnum}}{{#isArray}}{{#uniqueItems}}std::collections::HashSet<{{/uniqueItems}}{{^uniqueItems}}Vec<{{/uniqueItems}}{{/isArray}}{{{enumName}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#isNullable}}>{{/isNullable}}), +{{/vars}} +} +{{/-first}} +{{/oneOf}} + +{{/discriminator}} +{{/isEnum}} +{{!-- for properties that are of enum type --}} +{{#vars}} +{{#isEnum}} +{{#description}}/// {{{description}}}{{/description}} +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +pub enum {{{enumName}}} { +{{#allowableValues}} +{{#enumVars}} + #[serde(rename = "{{{value}}}")] + {{{name}}}, +{{/enumVars}} +{{/allowableValues}} +} + +impl Default for {{{enumName}}} { + fn default() -> Self { + Self::{{#allowableValues}}{{#enumVars}}{{#-first}}{{{name}}}{{/-first}}{{/enumVars}}{{/allowableValues}} + } +} + +{{/isEnum}} +{{/vars}} +{{/model}} +{{/models}} \ No newline at end of file diff --git a/codegen/src/v3/templates/default/model_doc.mustache b/codegen/src/v3/templates/default/model_doc.mustache new file mode 100644 index 000000000..c0cc2980f --- /dev/null +++ b/codegen/src/v3/templates/default/model_doc.mustache @@ -0,0 +1,12 @@ +{{#models}}{{#model}}# {{{classname}}} + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +{{#vars}}**{{{name}}}** | {{^required}}Option<{{/required}}{{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{#isPrimitiveType}}**{{{dataType}}}**{{/isPrimitiveType}}{{^isPrimitiveType}}[**{{{dataType}}}**]({{{complexType}}}.md){{/isPrimitiveType}}{{^required}}>{{/required}}{{#required}}{{#isNullable}}>{{/isNullable}}{{/required}} | {{{description}}} | {{^required}}[optional]{{/required}}{{#isReadOnly}}[readonly]{{/isReadOnly}}{{#defaultValue}}[default to {{{defaultValue}}}]{{/defaultValue}} +{{/vars}} + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + +{{/model}}{{/models}} \ No newline at end of file diff --git a/codegen/src/v3/templates/default/model_mod.mustache b/codegen/src/v3/templates/default/model_mod.mustache new file mode 100644 index 000000000..46c382005 --- /dev/null +++ b/codegen/src/v3/templates/default/model_mod.mustache @@ -0,0 +1,6 @@ +{{#models}} +{{#model}} +pub mod {{{classFilename}}}; +pub use self::{{{classFilename}}}::{{{classname}}}; +{{/model}} +{{/models}} \ No newline at end of file diff --git a/codegen/src/v3/templates/default/openapi.mustache b/codegen/src/v3/templates/default/openapi.mustache new file mode 100644 index 000000000..51ebafb01 --- /dev/null +++ b/codegen/src/v3/templates/default/openapi.mustache @@ -0,0 +1 @@ +{{{openapi-yaml}}} \ No newline at end of file diff --git a/codegen/src/v3/templates/default/tower-hyper/client/api_clients.mustache b/codegen/src/v3/templates/default/tower-hyper/client/api_clients.mustache new file mode 100644 index 000000000..0cd755be9 --- /dev/null +++ b/codegen/src/v3/templates/default/tower-hyper/client/api_clients.mustache @@ -0,0 +1,291 @@ +#![allow(clippy::to_string_in_format_args)] + +use crate::clients::tower::{ + configuration, Error, RequestError, ResponseContent, ResponseContentUnexpected, ResponseError, +}; + +use hyper::service::Service; +use std::sync::Arc; +use tower::ServiceExt; + +pub struct {{{classname}}}Client { + configuration: Arc, +} + +impl {{{classname}}}Client { + pub fn new(configuration: Arc) -> Self { + Self { + configuration, + } + } +} +impl Clone for {{{classname}}}Client { + fn clone(&self) -> Self { + Self { + configuration: self.configuration.clone() + } + } +} + +#[async_trait::async_trait] +#[dyn_clonable::clonable] +pub trait {{{classname}}}: Clone + Send + Sync { + {{#operations}} + {{#operation}} + {{#description}} + /// {{{.}}} + {{/description}} + {{#notes}} + /// {{{.}}} + {{/notes}} + async fn {{{operationId}}}(&self, {{#allParams}}{{{paramName}}}: {{^required}}Option<{{/required}}{{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{#isString}}{{#isArray}}Vec<{{/isArray}}&str{{#isArray}}>{{/isArray}}{{/isString}}{{#isUuid}}{{#isArray}}Vec<{{/isArray}}&uuid::Uuid{{#isArray}}>{{/isArray}}{{/isUuid}}{{^isString}}{{^isUuid}}{{^isPrimitiveType}}{{^isContainer}}crate::models::{{/isContainer}}{{/isPrimitiveType}}{{{dataType}}}{{/isUuid}}{{/isString}}{{^required}}>{{/required}}{{#required}}{{#isNullable}}>{{/isNullable}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) -> Result<{{#supportMultipleResponses}}ResponseContent<{{{operationIdCamelCase}}}Success>{{/supportMultipleResponses}}{{^supportMultipleResponses}}ResponseContent<{{^returnType}}(){{/returnType}}{{#returnType}}{{{returnType}}}{{/returnType}}>{{/supportMultipleResponses}}, Error>; + {{/operation}} + {{/operations}} +} + +/// Same as `{{{classname}}}` but it returns the result body directly. +pub mod direct { + #[async_trait::async_trait] + #[dyn_clonable::clonable] + pub trait {{{classname}}}: Clone + Send + Sync { + {{#operations}} + {{#operation}} + {{#description}} + /// {{{.}}} + {{/description}} + {{#notes}} + /// {{{.}}} + {{/notes}} + async fn {{{operationId}}}(&self, {{#allParams}}{{{paramName}}}: {{^required}}Option<{{/required}}{{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{#isString}}{{#isArray}}Vec<{{/isArray}}&str{{#isArray}}>{{/isArray}}{{/isString}}{{#isUuid}}{{#isArray}}Vec<{{/isArray}}&uuid::Uuid{{#isArray}}>{{/isArray}}{{/isUuid}}{{^isString}}{{^isUuid}}{{^isPrimitiveType}}{{^isContainer}}crate::models::{{/isContainer}}{{/isPrimitiveType}}{{{dataType}}}{{/isUuid}}{{/isString}}{{^required}}>{{/required}}{{#required}}{{#isNullable}}>{{/isNullable}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) -> Result<{{#supportMultipleResponses}}ResponseContent<{{{operationIdCamelCase}}}Success>{{/supportMultipleResponses}}{{^supportMultipleResponses}}{{^returnType}}(){{/returnType}}{{#returnType}}{{{returnType}}}{{/returnType}}{{/supportMultipleResponses}}, super::Error>; + {{/operation}} + {{/operations}} + } +} + +#[async_trait::async_trait] +impl direct::{{{classname}}} for {{{classname}}}Client { + {{#operations}} + {{#operation}} + {{#vendorExtensions.x-group-parameters}} + async fn {{{operationId}}}(&self{{#allParams}}{{#-first}}, params: {{{operationIdCamelCase}}}Params{{/-first}}{{/allParams}}) -> Result<{{#supportMultipleResponses}}ResponseContent<{{{operationIdCamelCase}}}Success>{{/supportMultipleResponses}}{{^supportMultipleResponses}}ResponseContent<{{^returnType}}(){{/returnType}}{{#returnType}}{{{returnType}}}{{/returnType}}>{{/supportMultipleResponses}}, Error> { + {{{classname}}}::{{{operationId}}}(self, {{#allParams}}{{#-first}}, params{{/-first}}{{/allParams}}).map(|r| r.into_body()) + {{/vendorExtensions.x-group-parameters}} + {{^vendorExtensions.x-group-parameters}} + async fn {{{operationId}}}(&self, {{#allParams}}{{{paramName}}}: {{^required}}Option<{{/required}}{{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{#isString}}{{#isArray}}Vec<{{/isArray}}&str{{#isArray}}>{{/isArray}}{{/isString}}{{#isUuid}}{{#isArray}}Vec<{{/isArray}}&uuid::Uuid{{#isArray}}>{{/isArray}}{{/isUuid}}{{^isString}}{{^isUuid}}{{^isPrimitiveType}}{{^isContainer}}crate::models::{{/isContainer}}{{/isPrimitiveType}}{{{dataType}}}{{/isUuid}}{{/isString}}{{^required}}>{{/required}}{{#required}}{{#isNullable}}>{{/isNullable}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) -> Result<{{#supportMultipleResponses}}ResponseContent<{{{operationIdCamelCase}}}Success>{{/supportMultipleResponses}}{{^supportMultipleResponses}}{{^returnType}}(){{/returnType}}{{#returnType}}{{{returnType}}}{{/returnType}}{{/supportMultipleResponses}}, Error> { + {{/vendorExtensions.x-group-parameters}} + {{{classname}}}::{{{operationId}}}(self, {{#allParams}}{{{paramName}}}{{^-last}}, {{/-last}}{{/allParams}}).await.map(|r| r.into_body()) + } + {{/operation}} + {{/operations}} +} + +#[async_trait::async_trait] +impl {{{classname}}} for {{{classname}}}Client { + {{#operations}} + {{#operation}} + {{#vendorExtensions.x-group-parameters}} + async fn {{{operationId}}}(&self{{#allParams}}{{#-first}}, params: {{{operationIdCamelCase}}}Params{{/-first}}{{/allParams}}) -> Result<{{#supportMultipleResponses}}ResponseContent<{{{operationIdCamelCase}}}Success>{{/supportMultipleResponses}}{{^supportMultipleResponses}}ResponseContent<{{^returnType}}(){{/returnType}}{{#returnType}}{{{returnType}}}{{/returnType}}>{{/supportMultipleResponses}}, Error> { + // unbox the parameters + {{#allParams}} + let {{paramName}} = params.{{paramName}}; + {{/allParams}} + + {{/vendorExtensions.x-group-parameters}} + {{^vendorExtensions.x-group-parameters}} + async fn {{{operationId}}}(&self, {{#allParams}}{{{paramName}}}: {{^required}}Option<{{/required}}{{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{#isString}}{{#isArray}}Vec<{{/isArray}}&str{{#isArray}}>{{/isArray}}{{/isString}}{{#isUuid}}{{#isArray}}Vec<{{/isArray}}&uuid::Uuid{{#isArray}}>{{/isArray}}{{/isUuid}}{{^isString}}{{^isUuid}}{{^isPrimitiveType}}{{^isContainer}}crate::models::{{/isContainer}}{{/isPrimitiveType}}{{{dataType}}}{{/isUuid}}{{/isString}}{{^required}}>{{/required}}{{#required}}{{#isNullable}}>{{/isNullable}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) -> Result<{{#supportMultipleResponses}}ResponseContent<{{{operationIdCamelCase}}}Success>{{/supportMultipleResponses}}{{^supportMultipleResponses}}ResponseContent<{{^returnType}}(){{/returnType}}{{#returnType}}{{{returnType}}}{{/returnType}}>{{/supportMultipleResponses}}, Error> { + {{/vendorExtensions.x-group-parameters}} + let configuration = &self.configuration; + + let local_var_uri_str = format!("{}{{{path}}}", configuration.base_path{{#pathParams}}, {{{baseName}}}={{#isString}}crate::apis::urlencode({{/isString}}{{{paramName}}}{{^required}}.unwrap_or_default(){{/required}}{{#required}}{{#isNullable}}.unwrap_or_default(){{/isNullable}}{{/required}}{{#isArray}}.join(",").as_ref(){{/isArray}}{{#isString}}){{/isString}}{{^isString}}.to_string(){{/isString}}{{/pathParams}}); + let mut local_var_req_builder = hyper::Request::builder().method(hyper::Method::{{#vendorExtensions}}{{x-httpMethodUpper}}{{/vendorExtensions}}); + + {{#hasQueryParams}} + let query_params: Option = None; + {{#queryParams}} + {{#required}} + let query_params = match query_params { + None => Some(format!("{{{baseName}}}={}", {{{paramName}}}{{#isArray}}{{#items}}{{^isString}}.into_iter().map(|p| p.to_string()).collect::>(){{/isString}}.join(","){{/items}}{{/isArray}}{{^isArray}}.to_string(){{/isArray}})), + Some(previous) => Some(format!("{previous}&{{{baseName}}}={}", {{{paramName}}}{{#isArray}}{{#items}}{{^isString}}.into_iter().map(|p| p.to_string()).collect::>(){{/isString}}.join(","){{/items}}{{/isArray}}{{^isArray}}.to_string(){{/isArray}})) + }; + {{/required}} + {{^required}} + let query_params = if let Some(local_var_str) = {{{paramName}}} { + match query_params { + None => Some(format!("{{{baseName}}}={}", local_var_str{{#isArray}}{{#items}}{{^isString}}.into_iter().map(|p| p.to_string()).collect::>(){{/isString}}.join(","){{/items}}{{/isArray}}{{^isArray}}.to_string(){{/isArray}})), + Some(previous) => Some(format!("{previous}&{{{baseName}}}={}", local_var_str{{#isArray}}{{#items}}{{^isString}}.into_iter().map(|p| p.to_string()).collect::>(){{/isString}}.join(","){{/items}}{{/isArray}}{{^isArray}}.to_string(){{/isArray}})) + } + } else { + query_params + }; + {{/required}} + {{/queryParams}} + let local_var_uri_str = match query_params { + None => local_var_uri_str, + Some(params) => format!("{local_var_uri_str}?{params}") + }; + {{/hasQueryParams}} + {{#hasAuthMethods}} + {{#authMethods}} + {{#isApiKey}} + {{#isKeyInQuery}} + let local_var_uri_str = if let Some(ref local_var_apikey) = configuration.api_key { + let local_var_key = local_var_apikey.key.clone(); + let local_var_value = match local_var_apikey.prefix { + Some(ref local_var_prefix) => format!("{local_var_prefix} {local_var_key}"), + None => local_var_key, + }; + {{#hasQueryParams}} + let local_var_uri_str = match query_params { + None => format!("{local_var_uri_str}?{{{keyParamName}}}={local_var_value}"), + Some(_) => format!("{local_var_uri_str}&{{{keyParamName}}}={local_var_value}"), + }; + {{/hasQueryParams}} + {{^hasQueryParams}} + let local_var_uri_str = format!("{local_var_uri_str}?{{{keyParamName}}}={local_var_value}"); + {{/hasQueryParams}} + local_var_uri_str + } else { + local_var_uri_str + } + {{/isKeyInQuery}} + {{/isApiKey}} + {{/authMethods}} + {{/hasAuthMethods}} + if let Some(ref local_var_user_agent) = configuration.user_agent { + local_var_req_builder = local_var_req_builder.header(hyper::header::USER_AGENT, local_var_user_agent.clone()); + } + {{#hasHeaderParams}} + {{#headerParams}} + {{#required}} + {{^isNullable}} + local_var_req_builder = local_var_req_builder.header("{{{baseName}}}", {{{paramName}}}{{#isArray}}.join(","){{/isArray}}{{^isArray}}.to_string(){{/isArray}}); + {{/isNullable}} + {{#isNullable}} + match {{{paramName}}} { + Some(local_var_param_value) => { local_var_req_builder = local_var_req_builder.insert_header(("{{{baseName}}}", local_var_param_value{{#isArray}}.join(","){{/isArray}}{{^isArray}}.to_string(){{/isArray}})); }, + None => { local_var_req_builder = local_var_req_builder.insert_header(("{{{baseName}}}", "")); }, + } + {{/isNullable}} + {{/required}} + {{^required}} + if let Some(local_var_param_value) = {{{paramName}}} { + local_var_req_builder = local_var_req_builder.header("{{{baseName}}}", local_var_param_value{{#isArray}}.join(","){{/isArray}}{{^isArray}}.to_string(){{/isArray}}); + } + {{/required}} + {{/headerParams}} + {{/hasHeaderParams}} + {{#hasAuthMethods}} + {{#authMethods}} + {{#isApiKey}} + {{#isKeyInHeader}} + if let Some(ref local_var_apikey) = configuration.api_key { + let local_var_key = local_var_apikey.key.clone(); + let local_var_value = match local_var_apikey.prefix { + Some(ref local_var_prefix) => format!("{local_var_prefix} {local_var_key}"), + None => local_var_key, + }; + local_var_req_builder = local_var_req_builder.header("{{{keyParamName}}}", local_var_value); + }; + {{/isKeyInHeader}} + {{/isApiKey}} + {{#isBasic}} + {{#isBasicBasic}} + if let Some(ref local_var_auth_conf) = configuration.basic_auth { + local_var_req_builder = local_var_req_builder.header(hyper::header::AUTHORIZATION, format!("Basic {local_var_auth_conf}")); + }; + {{/isBasicBasic}} + {{#isBasicBearer}} + if let Some(ref local_var_token) = configuration.bearer_access_token { + local_var_req_builder = local_var_req_builder.header(hyper::header::AUTHORIZATION, format!("Bearer {local_var_token}")); + }; + {{/isBasicBearer}} + {{/isBasic}} + {{#isOAuth}} + if let Some(ref local_var_token) = configuration.oauth_access_token { + local_var_req_builder = local_var_req_builder.header(hyper::header::AUTHORIZATION, format!("Bearer {local_var_token}")); + }; + {{/isOAuth}} + {{/authMethods}} + {{/hasAuthMethods}} + {{#isMultipart}} + $NOT_SUPPORTED$ + {{/isMultipart}} + {{#hasBodyParam}} + {{#bodyParam}} + let body = hyper::Body::from( + serde_json::to_vec(&{{{paramName}}}).map_err(RequestError::Serde)?, + ); + {{/bodyParam}} + {{/hasBodyParam}} + {{^hasBodyParam}} + let body = hyper::Body::empty(); + {{/hasBodyParam}} + let request = local_var_req_builder.uri(local_var_uri_str).header("content-type", "application/json").body(body).map_err(RequestError::BuildRequest)?; + + let local_var_resp = { + let mut client_service = configuration.client_service.lock().await.clone(); + client_service + .ready() + .await + .map_err(RequestError::NotReady)? + .call(request) + .await + .map_err(RequestError::Request)? + }; + let local_var_status = local_var_resp.status(); + + if local_var_status.is_success() { + {{^supportMultipleResponses}} + {{^returnType}} + Ok(ResponseContent { status: local_var_status, body: () }) + {{/returnType}} + {{#returnType}} + let body = hyper::body::to_bytes(local_var_resp.into_body()).await.map_err(|e| ResponseError::PayloadError { + status: local_var_status, + error: e, + })?; + let local_var_content: {{{returnType}}} = + serde_json::from_slice(&body).map_err(|e| { + ResponseError::Unexpected(ResponseContentUnexpected { + status: local_var_status, + text: e.to_string(), + }) + })?; + Ok(ResponseContent { status: local_var_status, body: local_var_content }) + {{/returnType}} + {{/supportMultipleResponses}} + {{#supportMultipleResponses}} + let body = hyper::body::to_bytes(local_var_resp.into_body()).await.map_err(|e| ResponseError::PayloadError { + status: local_var_status, + error: e, + })?; + let local_var_content: ResponseContent<{{{operationIdCamelCase}}}Success> = serde_json::from_slice(&body)?; + let local_var_entity: Option<{{{operationIdCamelCase}}}Success> = serde_json::from_str(&local_var_content).ok(); + let local_var_resp = ResponseContent { status: local_var_status, entity: local_var_entity }; + Ok(local_var_resp) + {{/supportMultipleResponses}} + } else { + match hyper::body::to_bytes(local_var_resp.into_body()).await { + Ok(body) => match serde_json::from_slice::(&body) { + Ok(error) => Err(Error::Response(ResponseError::Expected(ResponseContent { + status: local_var_status, + body: error, + }))), + Err(_) => Err(Error::Response(ResponseError::Unexpected( + ResponseContentUnexpected { + status: local_var_status, + text: String::from_utf8_lossy(&body).to_string(), + }, + ))), + }, + Err(error) => Err(Error::Response(ResponseError::PayloadError { + status: local_var_status, + error, + })), + } + + } + } + {{/operation}} + {{/operations}} +} \ No newline at end of file diff --git a/codegen/src/v3/templates/default/tower-hyper/client/body.mustache b/codegen/src/v3/templates/default/tower-hyper/client/body.mustache new file mode 100644 index 000000000..0a262a19a --- /dev/null +++ b/codegen/src/v3/templates/default/tower-hyper/client/body.mustache @@ -0,0 +1,46 @@ +//! Kube-builder convertion from `tower_http::trace::ResponseBody` to `hyper::Body` +//! + +use std::{ + pin::Pin, + task::{Context, Poll}, +}; + +use futures::stream::Stream; +use http_body::Body; +use pin_project::pin_project; + +// Wrap `http_body::Body` to implement `Stream`. +#[pin_project] +pub struct IntoStream { + #[pin] + body: B, +} + +impl IntoStream { + pub(crate) fn new(body: B) -> Self { + Self { body } + } +} + +impl Stream for IntoStream +where + B: Body, +{ + type Item = Result; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.project().body.poll_data(cx) + } +} + +pub trait BodyStreamExt: Body { + fn into_stream(self) -> IntoStream + where + Self: Sized, + { + IntoStream::new(self) + } +} + +impl BodyStreamExt for T where T: Body {} diff --git a/codegen/src/v3/templates/default/tower-hyper/client/client.mustache b/codegen/src/v3/templates/default/tower-hyper/client/client.mustache new file mode 100644 index 000000000..f2dcfa2ae --- /dev/null +++ b/codegen/src/v3/templates/default/tower-hyper/client/client.mustache @@ -0,0 +1,318 @@ +mod body; +pub mod configuration; + +use configuration::BoxedError; +pub use configuration::Configuration; +pub use hyper::{self, StatusCode, Uri}; +pub use url::Url; + +use std::{error, fmt, ops::Deref, sync::Arc}; + +#[derive(Clone)] +pub struct ApiClient { +{{#apiInfo}} +{{#apis}} +{{#operations}} + {{{classFilename}}}: Box, +{{/operations}} +{{/apis}} +{{/apiInfo}} +} + +/// Same as `ApiClient` but returns the body directly. +pub mod direct { + #[derive(Clone)] + pub struct ApiClient { + {{#apiInfo}} + {{#apis}} + {{#operations}} + {{{classFilename}}}: Box, + {{/operations}} + {{/apis}} + {{/apiInfo}} + } + + impl ApiClient { + pub fn new(configuration: super::Configuration) -> ApiClient { + let rc = super::Arc::new(configuration); + + ApiClient { + {{#apiInfo}} + {{#apis}} + {{#operations}} + {{^-last}} + {{{classFilename}}}: Box::new(crate::apis::{{{classFilename}}}::tower::client::{{{classname}}}Client::new(rc.clone())), + {{/-last}} + {{#-last}} + {{{classFilename}}}: Box::new(crate::apis::{{{classFilename}}}::tower::client::{{{classname}}}Client::new(rc)), + {{/-last}} + {{/operations}} + {{/apis}} + {{/apiInfo}} + } + } + + {{#apiInfo}} + {{#apis}} + {{#operations}} + pub fn {{{classFilename}}}(&self) -> &dyn crate::apis::{{{classFilename}}}::tower::client::direct::{{{classname}}} { + self.{{{classFilename}}}.as_ref() + } + {{/operations}} + {{/apis}} + {{/apiInfo}} + } +} + +impl ApiClient { + pub fn new(configuration: Configuration) -> ApiClient { + let rc = Arc::new(configuration); + + ApiClient { +{{#apiInfo}} +{{#apis}} +{{#operations}} + {{^-last}} + {{{classFilename}}}: Box::new(crate::apis::{{{classFilename}}}::tower::client::{{{classname}}}Client::new(rc.clone())), + {{/-last}} + {{#-last}} + {{{classFilename}}}: Box::new(crate::apis::{{{classFilename}}}::tower::client::{{{classname}}}Client::new(rc)), + {{/-last}} +{{/operations}} +{{/apis}} +{{/apiInfo}} + } + } + +{{#apiInfo}} +{{#apis}} +{{#operations}} + pub fn {{{classFilename}}}(&self) -> &dyn crate::apis::{{{classFilename}}}::tower::client::{{{classname}}} { + self.{{{classFilename}}}.as_ref() + } +{{/operations}} +{{/apis}} +{{/apiInfo}} +} + +/// Http Response with status and body. +#[derive(Debug, Clone)] +pub struct ResponseContent { + pub(crate) status: hyper::StatusCode, + pub(crate) body: T, +} +impl ResponseContent { + /// Get the status code. + pub fn status(&self) -> hyper::StatusCode { + self.status + } + /// Get a reference to the body. + pub fn body(&self) -> &T { + &self.body + } + /// Convert self into the body. + pub fn into_body(self) -> T { + self.body + } + /// Convert ResponseContent into ResponseContent>. + pub fn with_vec_body(self) -> ResponseContent> { + ResponseContent { + status: self.status, + body: vec![self.body] + } + } +} + +/// Http Response with status and body as text (could not be coerced into the expected type). +#[derive(Debug, Clone)] +pub struct ResponseContentUnexpected { + pub(crate) status: hyper::StatusCode, + pub(crate) text: String, +} +impl ResponseContentUnexpected { + /// Get the status code. + pub fn status(&self) -> hyper::StatusCode { + self.status + } + /// Get a reference to the text. + pub fn text(&self) -> &str { + self.text.as_ref() + } +} + +/// Error type for all Requests with the various variants. +#[derive(Debug)] +pub enum Error { + Request(RequestError), + Response(ResponseError), +} +impl Error { + /// Get the request error, if that is the case. + pub fn request(&self) -> Option<&RequestError> { + match self { + Error::Request(request) => Some(request), + Error::Response(_) => None, + } + } + /// Get the response error, if that is the case. + pub fn response(&self) -> Option<&ResponseError> { + match self { + Error::Request(_) => None, + Error::Response(response) => Some(response), + } + } + /// Get the expected error, if received. + pub fn expected(&self) -> Option<&ResponseContent> { + match self { + Error::Request(_) => None, + Error::Response(response) => response.expected(), + } + } + /// Get the inner body error, if expected. + pub fn error_body(&self) -> Option<&T> { + match self { + Error::Request(_) => None, + Error::Response(response) => response.error_body(), + } + } + /// Get the response status code, if any received. + pub fn status(&self) -> Option { + self.response().map(|response| response.status()) + } +} +impl From for Error { + fn from(src: RequestError) -> Self { + Self::Request(src) + } +} +impl From> for Error { + fn from(src: ResponseError) -> Self { + Self::Response(src) + } +} +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::Request(r) => r.fmt(f), + Error::Response(r) => r.fmt(f), + } + } +} +impl error::Error for Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match self { + Error::Request(r) => r.source(), + Error::Response(r) => r.source(), + } + } +} + +/// Failed to issue the request. +#[derive(Debug)] +pub enum RequestError { + /// Failed to build the http request. + BuildRequest(hyper::http::Error), + /// Service Request call returned an error. + Request(BoxedError), + /// Service was not ready to process the request. + NotReady(BoxedError), + /// Failed to serialize request payload. + Serde(serde_json::Error), + /// Failed to encode the url path. + SerdeEncoded(serde_urlencoded::ser::Error), +} +impl fmt::Display for RequestError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let (module, e) = match self { + RequestError::BuildRequest(e) => ("build_request", e.to_string()), + RequestError::Request(e) => ("request", e.to_string()), + RequestError::NotReady(e) => ("not_ready", e.to_string()), + RequestError::Serde(e) => ("serde", e.to_string()), + RequestError::SerdeEncoded(e) => ("serde_encoding", e.to_string()), + }; + write!(f, "error in {module}: {e}") + } +} +impl error::Error for RequestError { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + Some(match self { + RequestError::BuildRequest(e) => e, + RequestError::Request(e) => e.deref(), + RequestError::NotReady(e) => e.deref(), + RequestError::Serde(e) => e, + RequestError::SerdeEncoded(e) => e, + }) + } +} + +/// Error type for all Requests with the various variants. +#[derive(Debug)] +pub enum ResponseError { + /// The OpenAPI call returned the "expected" OpenAPI JSON content. + Expected(ResponseContent), + /// Failed to convert the response payload to bytes. + PayloadError { + status: hyper::StatusCode, + error: hyper::Error, + }, + /// The OpenAPI call returned an "unexpected" JSON content. + Unexpected(ResponseContentUnexpected), +} +impl fmt::Display for ResponseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let (module, e) = match self { + ResponseError::Expected(e) => ( + "response", + format!("status code '{}', content: '{:?}'", e.status, e.body), + ), + ResponseError::PayloadError { status, error } => ( + "response", + format!("status code '{status}', error: '{error:?}'"), + ), + ResponseError::Unexpected(e) => ( + "response", + format!("status code '{}', text '{}'", e.status, e.text), + ), + }; + write!(f, "error in {module}: {e}") + } +} +impl error::Error for ResponseError { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match self { + ResponseError::Expected(_) => None, + ResponseError::PayloadError { error, .. } => Some(error), + ResponseError::Unexpected(_) => None, + } + } +} +impl ResponseError { + /// Get the inner status. + pub fn status(&self) -> StatusCode { + match self { + ResponseError::Expected(expected) => expected.status, + ResponseError::PayloadError { status, .. } => *status, + ResponseError::Unexpected(unexpected) => unexpected.status, + } + } + /// Get the expected error, if received. + pub fn expected(&self) -> Option<&ResponseContent> { + match self { + ResponseError::Expected(expected) => Some(expected), + _ => None, + } + } + /// Get the inner body error, if expected. + pub fn error_body(&self) -> Option<&T> { + match self { + ResponseError::Expected(expected) => Some(&expected.body), + _ => None, + } + } +} + +impl std::fmt::Debug for ApiClient { + fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> fmt::Result { + fmt::Result::Ok(()) + } +} diff --git a/codegen/src/v3/templates/default/tower-hyper/client/configuration.mustache b/codegen/src/v3/templates/default/tower-hyper/client/configuration.mustache new file mode 100644 index 000000000..c100b7e1c --- /dev/null +++ b/codegen/src/v3/templates/default/tower-hyper/client/configuration.mustache @@ -0,0 +1,485 @@ +#![allow(clippy::type_complexity)] +use super::body::BodyStreamExt; + +pub use hyper::{body, service::Service, Body, Request, Response, Uri}; + +use std::{sync::Arc, time::Duration}; +use tokio::sync::Mutex; +use tower::{util::BoxCloneService, Layer, ServiceExt}; + +#[cfg(feature = "tower-trace")] +use opentelemetry::global; +#[cfg(feature = "tower-trace")] +use opentelemetry_http::HeaderInjector; + +#[cfg(all(feature = "tower-client-rls", not(feature = "tower-client-tls")))] +use rustls::{ + client::{ServerCertVerified, ServerCertVerifier}, + Certificate, Error as TLSError, +}; + +use tower_http::map_response_body::MapResponseBodyLayer; +#[cfg(feature = "tower-trace")] +use tower_http::{classify::ServerErrorsFailureClass, trace::TraceLayer}; + +#[cfg(feature = "tower-trace")] +use tracing::Span; +#[cfg(feature = "tower-trace")] +use tracing_opentelemetry::OpenTelemetrySpanExt; + +/// Tower Service Error +pub type BoxedError = Box; + +/// `ConfigurationBuilder` that can be used to build a `Configuration`. +#[derive(Clone)] +pub struct ConfigurationBuilder { + /// Timeout for each HTTP Request. + timeout: Option, + /// Bearer Access Token for bearer-configured routes. + bearer_token: Option, + /// OpenTel and Tracing layer. + #[cfg(feature = "tower-trace")] + tracing_layer: bool, + certificate: Option>, + concurrency_limit: Option, +} + +impl Default for ConfigurationBuilder { + fn default() -> Self { + Self { + timeout: Some(std::time::Duration::from_secs(5)), + bearer_token: None, + #[cfg(feature = "tower-trace")] + tracing_layer: true, + certificate: None, + concurrency_limit: None, + } + } +} + +impl ConfigurationBuilder { + /// Return a new `Self`. + pub fn new() -> Self { + Self::default() + } + /// Enable/Disable a request timeout layer with the given request timeout. + pub fn with_timeout>>(mut self, timeout: O) -> Self { + self.timeout = timeout.into(); + self + } + /// Enable/Disable the given request bearer token. + pub fn with_bearer_token(mut self, bearer_token: Option) -> Self { + self.bearer_token = bearer_token; + self + } + /// Add a request concurrency limit. + pub fn with_concurrency_limit(mut self, limit: Option) -> Self { + self.concurrency_limit = limit; + self + } + /// Add a PEM-format certificate file. + pub fn with_certificate(mut self, certificate: &[u8]) -> Self { + self.certificate = Some(certificate.to_vec()); + self + } + /// Enable/Disable the telemetry and tracing layer. + #[cfg(feature = "tower-trace")] + pub fn with_tracing(mut self, tracing_layer: bool) -> Self { + self.tracing_layer = tracing_layer; + self + } + /// Build a `Configuration` from the Self parameters. + pub fn build(self, uri: hyper::Uri) -> Result { + Configuration::new( + uri.to_string().parse().map_err(Error::UriToUrl)?, + self.timeout.unwrap(), + self.bearer_token, + self.certificate.as_ref().map(|c| &c[..]), + self.tracing_layer, + self.concurrency_limit, + ) + } + /// Build a `Configuration` from the Self parameters. + pub fn build_url(self, url: url::Url) -> Result { + Configuration::new( + url, + self.timeout.unwrap_or_else(|| Duration::from_secs(5)), + self.bearer_token, + self.certificate.as_ref().map(|c| &c[..]), + self.tracing_layer, + self.concurrency_limit, + ) + } + /// Build a `Configuration` from the Self parameters. + pub fn build_with_svc( + self, + uri: hyper::Uri, + client_service: S, + ) -> Result + where + S: Service, Response = Response> + Sync + Send + Clone + 'static, + S::Future: Send + 'static, + S::Error: Into + std::fmt::Debug, + { + #[cfg(feature = "tower-trace")] + let tracing_layer = self.tracing_layer; + #[cfg(not(feature = "tower-trace"))] + let tracing_layer = false; + Configuration::new_with_client( + uri, + client_service, + self.timeout, + self.bearer_token, + tracing_layer, + self.concurrency_limit, + ) + } +} + +/// Configuration used by the `ApiClient`. +#[derive(Clone)] +pub struct Configuration { + pub base_path: hyper::Uri, + pub user_agent: Option, + pub client_service: Arc, Response, BoxedError>>>, + pub basic_auth: Option, + pub oauth_access_token: Option, + pub bearer_access_token: Option, + pub api_key: Option, +} + +/// Basic authentication. +pub type BasicAuth = (String, Option); + +/// ApiKey used for ApiKey authentication. +#[derive(Debug, Clone)] +pub struct ApiKey { + pub prefix: Option, + pub key: String, +} + +/// Configuration creation Error. +#[derive(Debug)] +pub enum Error { + Certificate, + TlsConnector, + NoTracingFeature, + UrlToUri(hyper::http::uri::InvalidUri), + UriToUrl(url::ParseError), + AddingVersionPath(hyper::http::uri::InvalidUri), +} + +impl Configuration { + /// Return a new `ConfigurationBuilder`. + pub fn builder() -> ConfigurationBuilder { + ConfigurationBuilder::new() + } + + /// New `Self` with a provided client. + pub fn new_with_client( + mut url: hyper::Uri, + client_service: S, + timeout: Option, + bearer_access_token: Option, + trace_requests: bool, + concurrency_limit: Option, + ) -> Result + where + S: Service, Response = Response> + Sync + Send + Clone + 'static, + S::Future: Send + 'static, + S::Error: Into + std::fmt::Debug, + { + #[cfg(feature = "tower-trace")] + let tracing_layer = tower::ServiceBuilder::new() + .layer( + TraceLayer::new_for_http() + .make_span_with(|request: &Request| { + tracing::info_span!( + "HTTP", + http.method = %request.method(), + http.url = %request.uri(), + http.status_code = tracing::field::Empty, + otel.name = %format!("{} {}", request.method(), request.uri()), + otel.kind = "client", + otel.status_code = tracing::field::Empty, + ) + }) + // to silence the default trace + .on_request(|request: &Request, _span: &Span| { + tracing::trace!("started {} {}", request.method(), request.uri().path()) + }) + .on_response( + |response: &Response, _latency: std::time::Duration, span: &Span| { + let status = response.status(); + span.record("http.status_code", status.as_u16()); + if status.is_client_error() || status.is_server_error() { + span.record("otel.status_code", "ERROR"); + } + }, + ) + .on_body_chunk(()) + .on_failure( + |ec: ServerErrorsFailureClass, + _latency: std::time::Duration, + span: &Span| { + span.record("otel.status_code", "ERROR"); + match ec { + ServerErrorsFailureClass::StatusCode(status) => { + span.record("http.status_code", status.as_u16()); + tracing::debug!(status=%status, "failed to issue request") + } + ServerErrorsFailureClass::Error(err) => { + tracing::debug!(error=%err, "failed to issue request") + } + } + }, + ), + ) + // injects the telemetry context in the http headers + .layer(OpenTelContext::new()) + .into_inner(); + + url = format!("{}/v0", url.to_string().trim_end_matches('/')) + .parse() + .map_err(Error::AddingVersionPath)?; + + let backend_service = tower::ServiceBuilder::new() + .option_layer(timeout.map(tower::timeout::TimeoutLayer::new)) + // .option_layer( + // bearer_access_token.map(|b| tower_http::auth::AddAuthorizationLayer::bearer(&b)), + // ) + .service(client_service); + + let service_builder = tower::ServiceBuilder::new() + .option_layer(concurrency_limit.map(tower::limit::ConcurrencyLimitLayer::new)); + + match trace_requests { + false => Ok(Self::new_with_client_inner( + url, + service_builder.service(backend_service), + bearer_access_token, + )), + true => { + #[cfg(feature = "tower-trace")] + let result = Ok(Self::new_with_client_inner( + url, + service_builder + .layer(tracing_layer) + .service(backend_service), + bearer_access_token, + )); + #[cfg(not(feature = "tower-trace"))] + let result = Err(Error::NoTracingFeature {}); + result + } + } + } + + /// New `Self`. + pub fn new( + mut url: url::Url, + timeout: std::time::Duration, + bearer_access_token: Option, + certificate: Option<&[u8]>, + trace_requests: bool, + concurrency_limit: Option, + ) -> Result { + #[cfg(all(not(feature = "tower-client-tls"), feature = "tower-client-rls"))] + let client = { + match certificate { + None => { + let mut http = hyper::client::HttpConnector::new(); + + let tls = match url.scheme() == "https" { + true => { + http.enforce_http(false); + rustls::ClientConfig::builder() + .with_safe_defaults() + .with_custom_certificate_verifier(std::sync::Arc::new( + DisableServerCertVerifier {}, + )) + .with_no_client_auth() + } + false => rustls::ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(rustls::RootCertStore::empty()) + .with_no_client_auth(), + }; + + let connector = + hyper_rustls::HttpsConnector::from((http, std::sync::Arc::new(tls))); + hyper::Client::builder().build(connector) + } + Some(bytes) => { + let mut cert_file = std::io::BufReader::new(bytes); + let mut root_store = rustls::RootCertStore::empty(); + root_store.add_parsable_certificates( + &rustls_pemfile::certs(&mut cert_file).map_err(|_| Error::Certificate)?, + ); + let config = rustls::ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(root_store) + .with_no_client_auth(); + + let mut http = hyper::client::HttpConnector::new(); + http.enforce_http(false); + let connector = + hyper_rustls::HttpsConnector::from((http, std::sync::Arc::new(config))); + url.set_scheme("https").ok(); + hyper::Client::builder().build(connector) + } + } + }; + #[cfg(feature = "tower-client-tls")] + let client = { + match certificate { + None => { + let mut http = hyper_tls::HttpsConnector::new(); + if url.scheme() == "https" { + http.https_only(true); + } + + let tls = hyper_tls::native_tls::TlsConnector::builder() + .danger_accept_invalid_certs(true) + .build() + .map_err(|_| Error::TlsConnector)?; + let tls = tokio_native_tls::TlsConnector::from(tls); + + let connector = hyper_tls::HttpsConnector::from((http, tls)); + hyper::Client::builder().build(connector) + } + Some(bytes) => { + let certificate = hyper_tls::native_tls::Certificate::from_pem(bytes) + .map_err(|_| Error::Certificate)?; + + let tls = hyper_tls::native_tls::TlsConnector::builder() + .add_root_certificate(certificate) + .danger_accept_invalid_hostnames(true) + .disable_built_in_roots(true) + .build() + .map_err(|_| Error::TlsConnector)?; + let tls = tokio_native_tls::TlsConnector::from(tls); + + let mut http = hyper_tls::HttpsConnector::new(); + http.https_only(true); + let connector = hyper_tls::HttpsConnector::from((http, tls)); + url.set_scheme("https").ok(); + hyper::Client::builder().build(connector) + } + } + }; + + let uri = url.to_string().parse().map_err(Error::UrlToUri)?; + Self::new_with_client( + uri, + client, + Some(timeout), + bearer_access_token, + trace_requests, + concurrency_limit, + ) + } + + /// New `Self` with a provided client. + pub fn new_with_client_inner( + url: hyper::Uri, + client_service: S, + bearer_access_token: Option, + ) -> Self + where + S: Service, Response = Response> + Sync + Send + Clone + 'static, + S::Future: Send + 'static, + S::Error: Into + std::fmt::Debug, + B: http_body::Body + Send + 'static, + B::Error: std::error::Error + Send + Sync + 'static, + { + // Transform response body to `hyper::Body` and use type erased error to avoid type + // parameters. + let client_service = MapResponseBodyLayer::new(|b: B| Body::wrap_stream(b.into_stream())) + .layer(client_service) + .map_err(|e| e.into()); + let client_service = Arc::new(Mutex::new(BoxCloneService::new(client_service))); + Self { + base_path: url, + user_agent: None, + client_service, + basic_auth: None, + oauth_access_token: None, + bearer_access_token, + api_key: None, + } + } +} + +/// Add OpenTelemetry Span to the Http Headers. +#[cfg(feature = "tower-trace")] +pub struct OpenTelContext {} +#[cfg(feature = "tower-trace")] +impl OpenTelContext { + fn new() -> Self { + Self {} + } +} +#[cfg(feature = "tower-trace")] +impl Layer for OpenTelContext { + type Service = OpenTelContextService; + + fn layer(&self, service: S) -> Self::Service { + OpenTelContextService::new(service) + } +} + +/// OpenTelemetry Service that injects the current span into the Http Headers. +#[cfg(feature = "tower-trace")] +#[derive(Clone)] +pub struct OpenTelContextService { + service: S, +} +#[cfg(feature = "tower-trace")] +impl OpenTelContextService { + fn new(service: S) -> Self { + Self { service } + } +} + +#[cfg(feature = "tower-trace")] +impl Service> for OpenTelContextService +where + S: Service>, +{ + type Response = S::Response; + type Error = S::Error; + type Future = S::Future; + + fn poll_ready( + &mut self, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.service.poll_ready(cx) + } + + fn call(&mut self, mut request: hyper::Request) -> Self::Future { + let cx = tracing::Span::current().context(); + global::get_text_map_propagator(|propagator| { + propagator.inject_context(&cx, &mut HeaderInjector(request.headers_mut())) + }); + self.service.call(request) + } +} + +#[cfg(all(feature = "tower-client-rls", not(feature = "tower-client-tls")))] +struct DisableServerCertVerifier {} +#[cfg(all(feature = "tower-client-rls", not(feature = "tower-client-tls")))] +impl ServerCertVerifier for DisableServerCertVerifier { + fn verify_server_cert( + &self, + _end_entity: &Certificate, + _intermediates: &[Certificate], + _server_name: &rustls::ServerName, + _scts: &mut dyn Iterator, + _ocsp_response: &[u8], + _now: std::time::SystemTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } +} diff --git a/codegen/src/v3/templates/default/tower-hyper/mod.mustache b/codegen/src/v3/templates/default/tower-hyper/mod.mustache new file mode 100644 index 000000000..a2b385a4b --- /dev/null +++ b/codegen/src/v3/templates/default/tower-hyper/mod.mustache @@ -0,0 +1,2 @@ +#[cfg(feature = "tower-client")] +pub mod client; diff --git a/codegen/src/v3/templates/mod.rs b/codegen/src/v3/templates/mod.rs new file mode 100644 index 000000000..c8f115cf3 --- /dev/null +++ b/codegen/src/v3/templates/mod.rs @@ -0,0 +1,294 @@ +use std::ops::Deref; + +use crate::v3::{OperationsApiTpl, Property}; +use heck::{ToPascalCase, ToSnakeCase}; + +/// Support cases for the class names. +#[derive(Debug, Clone)] +pub(super) enum ClassCase { + PascalCase, + SnakeCase, +} +impl ClassCase { + fn format(&self, name: &str) -> String { + match self { + ClassCase::PascalCase => name.to_pascal_case(), + ClassCase::SnakeCase => name.to_snake_case(), + } + } +} + +#[derive(Debug, Clone)] +pub(super) enum ApiTargetFileCfg { + /// $prefix/$class_fname/$postfix + ClassFileName, + /// $prefix/$class/$postfix + ClassName, +} + +/// Template file specification for support files. +#[derive(Debug, Clone)] +pub(super) struct SuppTemplateFile { + gen: GenTemplateFile, +} +impl SuppTemplateFile { + /// Create a new `Self` by specifying the template path, the target prefix and file extension. + pub(super) fn new(template: &'static str, target_prefix: &str, target_postfix: &str) -> Self { + let gen = GenTemplateFile { + template: TemplateFile::Buffer(template), + target_prefix: std::path::PathBuf::from(target_prefix), + target_postfix: std::path::PathBuf::from(target_postfix), + casing: ClassCase::PascalCase, + }; + Self { gen } + } +} +impl Deref for SuppTemplateFile { + type Target = GenTemplateFile; + fn deref(&self) -> &Self::Target { + &self.gen + } +} + +/// Template file specification for model files. +#[derive(Debug, Clone)] +pub(super) struct ModelTemplateFile { + gen: GenTemplateFile, + extension: String, +} +impl ModelTemplateFile { + /// Create a new `Self` by specifying the template path, the target prefix and file extension. + pub(super) fn new(template: &'static str, target_prefix: &str, ext: &str) -> Self { + let gen = GenTemplateFile { + template: TemplateFile::Buffer(template), + target_prefix: std::path::PathBuf::from(target_prefix), + target_postfix: std::path::PathBuf::from("."), + casing: ClassCase::SnakeCase, + }; + Self { + gen, + extension: ext.trim_start_matches('.').into(), + } + } + /// Override the class file case. + pub(super) fn with_file_case(mut self, casing: ClassCase) -> Self { + self.gen = self.gen.with_class_case(casing); + self + } + /// Generate the path for the model file. + pub(super) fn model_path(&self, property: &Property) -> std::path::PathBuf { + let model_fname = self.casing.format(property.filename()); + self.gen + .target_prefix + .join(model_fname) + .with_extension(&self.extension) + } +} +impl Deref for ModelTemplateFile { + type Target = GenTemplateFile; + fn deref(&self) -> &Self::Target { + &self.gen + } +} + +/// Template file specification for api files. +#[derive(Debug, Clone)] +pub(super) struct ApiTemplateFile { + gen: GenTemplateFile, + kind: ApiTargetFileCfg, +} +impl ApiTemplateFile { + /// Create a new `Self` by specifying the template path, the target prefix and postfix. + pub(super) fn new(template: &'static str, target_prefix: &str, target_postfix: &str) -> Self { + let gen = GenTemplateFile { + template: TemplateFile::Buffer(template), + target_prefix: std::path::PathBuf::from(target_prefix), + target_postfix: std::path::PathBuf::from(target_postfix), + casing: ClassCase::SnakeCase, + }; + Self { + gen, + kind: ApiTargetFileCfg::ClassFileName, + } + } + /// Override the class file case. + pub(super) fn with_class_case(mut self, casing: ClassCase) -> Self { + self.gen = self.gen.with_class_case(casing); + self + } + /// Override the class type. + pub(super) fn with_class(mut self, kind: ApiTargetFileCfg) -> Self { + self.kind = kind; + self + } + /// Generate the path for the api file. + pub(super) fn api_path(&self, api: &OperationsApiTpl) -> std::path::PathBuf { + let prefix = self.target_prefix(); + let postfix = self.gen.target_postfix().display().to_string(); + let class = self.casing.format(match &self.kind { + ApiTargetFileCfg::ClassFileName => api.class_filename(), + ApiTargetFileCfg::ClassName => api.classname(), + }); + match postfix.starts_with('.') { + true => prefix + .join(class) + .with_extension(postfix.trim_start_matches('.')), + false => prefix.join(class).join(postfix), + } + } +} +impl Deref for ApiTemplateFile { + type Target = GenTemplateFile; + fn deref(&self) -> &Self::Target { + &self.gen + } +} + +#[derive(Debug, Clone)] +pub(crate) enum TemplateFile { + #[allow(unused)] + Path(std::path::PathBuf), + Buffer(&'static str), +} +impl TemplateFile { + /// Get the template file path. + #[allow(dead_code)] + pub(super) fn path(&self) -> Option<&std::path::PathBuf> { + match self { + TemplateFile::Path(path) => Some(path), + TemplateFile::Buffer(_) => None, + } + } + /// Get the template raw buffer. + pub(super) fn buffer(&self) -> Option<&'static str> { + match self { + TemplateFile::Path(_) => None, + TemplateFile::Buffer(buffer) => Some(buffer), + } + } +} + +/// A generic template file specification. +#[derive(Debug, Clone)] +pub(super) struct GenTemplateFile { + template: TemplateFile, + target_prefix: std::path::PathBuf, + target_postfix: std::path::PathBuf, + casing: ClassCase, +} +impl GenTemplateFile { + /// Override the class file case. + pub(super) fn with_class_case(mut self, casing: ClassCase) -> Self { + self.casing = casing; + self + } + /// Get the template input file. + pub(super) fn input(&self) -> &TemplateFile { + &self.template + } + /// Get the target path prefix. + pub(super) fn target_prefix(&self) -> &std::path::PathBuf { + &self.target_prefix + } + /// Get the target path postfix. + pub(super) fn target_postfix(&self) -> &std::path::PathBuf { + &self.target_postfix + } +} + +pub(super) fn default_templates() -> ( + Vec, + Vec, + Vec, +) { + let api_templates = vec![ + // Actix + ApiTemplateFile::new( + include_str!("default/actix/client/api_clients.mustache"), + "apis", + "actix/client/mod.rs", + ), + ApiTemplateFile::new( + include_str!("default/actix/server/handlers.mustache"), + "apis", + "actix/server/handlers.rs", + ), + ApiTemplateFile::new( + include_str!("default/actix/mod.mustache"), + "apis", + "actix/mod.rs", + ), + ApiTemplateFile::new( + include_str!("default/actix/server/api.mustache"), + "apis", + "actix/server/mod.rs", + ), + // Tower-hyper + ApiTemplateFile::new( + include_str!("default/tower-hyper/mod.mustache"), + "apis", + "tower/mod.rs", + ), + ApiTemplateFile::new( + include_str!("default/tower-hyper/client/api_clients.mustache",), + "apis", + "tower/client/mod.rs", + ), + // Common + ApiTemplateFile::new(include_str!("default/mod.mustache"), "apis", "mod.rs"), + ApiTemplateFile::new(include_str!("default/api_doc.mustache"), "docs/apis", ".md") + .with_class_case(ClassCase::PascalCase) + .with_class(ApiTargetFileCfg::ClassName), + ]; + let model_templates = vec![ + ModelTemplateFile::new(include_str!("default/model.mustache"), "models", ".rs"), + ModelTemplateFile::new( + include_str!("default/model_doc.mustache"), + "docs/models", + ".md", + ) + .with_file_case(ClassCase::PascalCase), + ]; + let supporting_templates = vec![ + SuppTemplateFile::new( + include_str!("default/model_mod.mustache"), + "models", + "mod.rs", + ), + SuppTemplateFile::new( + include_str!("default/tower-hyper/client/configuration.mustache",), + "clients/tower", + "configuration.rs", + ), + SuppTemplateFile::new( + include_str!("default/tower-hyper/client/client.mustache"), + "clients/tower", + "mod.rs", + ), + SuppTemplateFile::new( + include_str!("default/tower-hyper/client/body.mustache"), + "clients/tower", + "body.rs", + ), + SuppTemplateFile::new(include_str!("default/api_mod.mustache"), "apis", "mod.rs"), + SuppTemplateFile::new( + include_str!("default/mod_clients.mustache"), + "clients", + "mod.rs", + ), + SuppTemplateFile::new(include_str!("default/lib.mustache"), "", "mod.rs"), + SuppTemplateFile::new( + include_str!("default/actix/server/api_mod.mustache"), + "apis", + "actix_server.rs", + ), + SuppTemplateFile::new(include_str!("default/Cargo.mustache"), "", "Cargo.toml"), + SuppTemplateFile::new(include_str!("default/gitignore.mustache"), "", ".gitignore"), + SuppTemplateFile::new( + include_str!("default/openapi.mustache"), + "apis", + "openapi.yaml", + ), + ]; + (api_templates, model_templates, supporting_templates) +} diff --git a/core/src/v2/mod.rs b/core/src/v2/mod.rs index f036a082b..28ceae400 100644 --- a/core/src/v2/mod.rs +++ b/core/src/v2/mod.rs @@ -24,9 +24,9 @@ pub use self::{ pub use paperclip_macros::*; #[cfg(feature = "codegen")] -use self::resolver::Resolver; +pub(crate) use self::resolver::Resolver; #[cfg(feature = "codegen")] -use crate::error::ValidationError; +pub(crate) use crate::error::ValidationError; #[cfg(feature = "codegen")] impl ResolvableApi { diff --git a/core/src/v3/mod.rs b/core/src/v3/mod.rs index 5a4976441..94d0369ee 100644 --- a/core/src/v3/mod.rs +++ b/core/src/v3/mod.rs @@ -2,27 +2,8 @@ //! Conversion traits and helps functions that help converting openapi v2 types to openapi v3. //! For the OpenAPI v3 types the crate `openapiv3` is used. -mod contact; -mod external_documentation; -mod header; -mod info; -mod license; -mod openapi; -mod operation; -mod parameter; -mod paths; -mod reference; -mod request_body; -mod response; -mod schema; -mod security_scheme; -mod tag; - -use super::v2::{models as v2, models::Either}; - -use parameter::non_body_parameter_to_v3_parameter; -use reference::invalid_referenceor; -use response::OperationEitherResponse; +use super::v2::models as v2; +mod models; /// Convert this crates openapi v2 (`DefaultApiRaw`) to `openapiv3::OpenAPI` pub fn openapiv2_to_v3(v2: v2::DefaultApiRaw) -> openapiv3::OpenAPI { diff --git a/core/src/v3/contact.rs b/core/src/v3/models/contact.rs similarity index 100% rename from core/src/v3/contact.rs rename to core/src/v3/models/contact.rs diff --git a/core/src/v3/external_documentation.rs b/core/src/v3/models/external_documentation.rs similarity index 100% rename from core/src/v3/external_documentation.rs rename to core/src/v3/models/external_documentation.rs diff --git a/core/src/v3/header.rs b/core/src/v3/models/header.rs similarity index 100% rename from core/src/v3/header.rs rename to core/src/v3/models/header.rs diff --git a/core/src/v3/info.rs b/core/src/v3/models/info.rs similarity index 100% rename from core/src/v3/info.rs rename to core/src/v3/models/info.rs diff --git a/core/src/v3/license.rs b/core/src/v3/models/license.rs similarity index 100% rename from core/src/v3/license.rs rename to core/src/v3/models/license.rs diff --git a/core/src/v3/models/mod.rs b/core/src/v3/models/mod.rs new file mode 100644 index 000000000..17db1b4ba --- /dev/null +++ b/core/src/v3/models/mod.rs @@ -0,0 +1,21 @@ +mod contact; +mod external_documentation; +mod header; +mod info; +mod license; +mod openapi; +mod operation; +mod parameter; +mod paths; +mod reference; +mod request_body; +mod response; +mod schema; +mod security_scheme; +mod tag; + +pub use super::super::v2::{models as v2, models::Either}; + +use parameter::non_body_parameter_to_v3_parameter; +use reference::invalid_referenceor; +use response::OperationEitherResponse; diff --git a/core/src/v3/openapi.rs b/core/src/v3/models/openapi.rs similarity index 100% rename from core/src/v3/openapi.rs rename to core/src/v3/models/openapi.rs diff --git a/core/src/v3/operation.rs b/core/src/v3/models/operation.rs similarity index 100% rename from core/src/v3/operation.rs rename to core/src/v3/models/operation.rs diff --git a/core/src/v3/parameter.rs b/core/src/v3/models/parameter.rs similarity index 100% rename from core/src/v3/parameter.rs rename to core/src/v3/models/parameter.rs diff --git a/core/src/v3/paths.rs b/core/src/v3/models/paths.rs similarity index 100% rename from core/src/v3/paths.rs rename to core/src/v3/models/paths.rs diff --git a/core/src/v3/reference.rs b/core/src/v3/models/reference.rs similarity index 100% rename from core/src/v3/reference.rs rename to core/src/v3/models/reference.rs diff --git a/core/src/v3/request_body.rs b/core/src/v3/models/request_body.rs similarity index 100% rename from core/src/v3/request_body.rs rename to core/src/v3/models/request_body.rs diff --git a/core/src/v3/response.rs b/core/src/v3/models/response.rs similarity index 100% rename from core/src/v3/response.rs rename to core/src/v3/models/response.rs diff --git a/core/src/v3/schema.rs b/core/src/v3/models/schema.rs similarity index 100% rename from core/src/v3/schema.rs rename to core/src/v3/models/schema.rs diff --git a/core/src/v3/security_scheme.rs b/core/src/v3/models/security_scheme.rs similarity index 100% rename from core/src/v3/security_scheme.rs rename to core/src/v3/models/security_scheme.rs diff --git a/core/src/v3/tag.rs b/core/src/v3/models/tag.rs similarity index 100% rename from core/src/v3/tag.rs rename to core/src/v3/models/tag.rs diff --git a/src/bin/main.rs b/src/bin/main.rs index 9e1c37f3c..3045b4ac0 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -1,4 +1,6 @@ use anyhow::Error; +#[cfg(feature = "v3-poc")] +use heck::ToSnakeCase; use paperclip::{ v2::{ self, @@ -26,6 +28,11 @@ fn parse_spec(s: &str) -> Result, Error> { let fd = File::open(s)?; Ok(v2::from_reader(fd)?) } +#[cfg(feature = "v3-poc")] +fn parse_spec_v3(s: &str) -> Result { + let fd = File::open(s)?; + Ok(v2::from_reader_v3(fd)?) +} #[derive(Debug)] enum OApiVersion { @@ -36,8 +43,8 @@ enum OApiVersion { #[derive(Debug, StructOpt)] struct Opt { /// Path to OpenAPI spec in JSON/YAML format (also supports publicly accessible URLs). - #[structopt(parse(try_from_str = parse_spec))] - spec: ResolvableApi, + #[structopt(long)] + spec: std::path::PathBuf, /// OpenAPI version (e.g., v2). #[structopt(long = "api", parse(try_from_str = parse_version))] api: OApiVersion, @@ -47,6 +54,14 @@ struct Opt { /// Emit CLI target instead. #[structopt(long = "cli")] cli: bool, + #[cfg(feature = "v3-poc")] + /// Don't Render models. + #[structopt(long)] + no_models: bool, + #[cfg(feature = "v3-poc")] + /// Don't Render operations. + #[structopt(long)] + no_ops: bool, /// Do not make the crate a root crate. #[structopt(long = "no-root")] no_root: bool, @@ -57,19 +72,49 @@ struct Opt { /// Version (defaults to 0.1.0) #[structopt(long = "version")] pub version: Option, + #[cfg(feature = "v3-poc")] + /// Edition (defaults to 2018) + #[structopt(long = "edition")] + pub edition: Option, } fn parse_args_and_run() -> Result<(), Error> { - let opt = Opt::from_args(); + let opt: Opt = Opt::from_args(); + + if let Some(o) = &opt.output { + fs::create_dir_all(o)?; + } + + #[cfg(feature = "v3-poc")] + if let OApiVersion::V3 = opt.api { + let spec = parse_spec_v3(&opt.spec.to_string_lossy().to_string())?; + let name = opt.name.map(Ok::).unwrap_or_else(|| { + Ok(fs::canonicalize(std::path::Path::new("."))? + .file_name() + .ok_or(PaperClipError::InvalidCodegenDirectory)? + .to_string_lossy() + .into_owned() + .to_snake_case()) + })?; + let info = paperclip::v3::PackageInfo { + libname: name.to_snake_case(), + name, + version: opt.version.unwrap_or_else(|| "0.1.0".into()), + edition: opt.edition.unwrap_or_else(|| "2021".into()), + }; + paperclip::v3::OpenApiV3::new(spec, opt.output, info).run(!opt.no_models, !opt.no_ops)?; + return Ok(()); + } + + #[cfg(not(feature = "v3-poc"))] if let OApiVersion::V3 = opt.api { return Err(PaperClipError::UnsupportedOpenAPIVersion.into()); } - let spec = opt.spec.resolve()?; + let spec = parse_spec(&opt.spec.to_string_lossy())?.resolve()?; let mut state = EmitterState::default(); if let Some(o) = opt.output { - fs::create_dir_all(&o)?; state.working_dir = o; } diff --git a/src/lib.rs b/src/lib.rs index ba4d42874..7ed144f6d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,3 +32,6 @@ pub mod actix { #[cfg(feature = "actix4")] pub use paperclip_core::v2::HttpResponseWrapper; } + +#[cfg(feature = "v3-poc")] +pub mod v3; diff --git a/src/v2/mod.rs b/src/v2/mod.rs index 3105a30bb..88c7697c8 100644 --- a/src/v2/mod.rs +++ b/src/v2/mod.rs @@ -133,3 +133,31 @@ where api.spec_format = fmt; Ok(api) } + +#[cfg(feature = "v3-poc")] +/// Deserialize the schema from the given reader. Currently, this only supports +/// JSON and YAML formats. +pub fn from_reader_v3(mut reader: R) -> Result +where + R: Read, +{ + let mut buf = [b' ']; + while buf[0].is_ascii_whitespace() { + reader.read_exact(&mut buf)?; + } + let reader = buf.as_ref().chain(reader); + + let (api, _fmt) = if buf[0] == b'{' { + ( + serde_json::from_reader::<_, openapiv3::OpenAPI>(reader)?, + SpecFormat::Json, + ) + } else { + ( + serde_yaml::from_reader::<_, openapiv3::OpenAPI>(reader)?, + SpecFormat::Yaml, + ) + }; + + Ok(api) +} diff --git a/src/v3/mod.rs b/src/v3/mod.rs new file mode 100644 index 000000000..519f1ad85 --- /dev/null +++ b/src/v3/mod.rs @@ -0,0 +1 @@ +pub use paperclip_codegen::v3_03::{OpenApiV3, PackageInfo};