From dce19ce90ce1bb13bf5a3789774fb87877e87e8c Mon Sep 17 00:00:00 2001 From: Afsal Thaj Date: Mon, 4 Nov 2024 12:01:52 +1100 Subject: [PATCH 1/7] Support XML::Value with the right content header --- src/rust/client_gen.rs | 166 +++++++++++++++++++++++------------------ src/rust/types.rs | 29 +++++-- 2 files changed, 118 insertions(+), 77 deletions(-) diff --git a/src/rust/client_gen.rs b/src/rust/client_gen.rs index f203243..1d6d285 100644 --- a/src/rust/client_gen.rs +++ b/src/rust/client_gen.rs @@ -268,7 +268,7 @@ fn match_tag(tag: &Option, path_item: &ReferenceOr) -> bool { fn param_data_to_type(data: &ParameterData, ref_cache: &mut RefCache) -> Result { match &data.format { ParameterSchemaOrContent::Schema(ref_or_schema) => { - ref_or_schema_type(ref_or_schema, ref_cache) + ref_or_schema_type(ref_or_schema, ref_cache, None) } ParameterSchemaOrContent::Content(_) => { Err(Error::unimplemented("Content parameter is not supported.")) @@ -301,79 +301,95 @@ fn request_body_params( "Unexpected ref request body: '{reference}'." ))), ReferenceOr::Item(body) => { - if body.content.len() != 1 { - Err(Error::unimplemented("Content with not exactly 1 option.")) - } else { - let (content_type, media_type) = body.content.first().unwrap(); - - if content_type.starts_with("application/json") { - let schema = match &media_type.schema { - None => Err(Error::unimplemented("JSON content without schema.")), - Some(schema) => Ok(schema), - }; + let (content_type, media_type) = + // TODO: It is in standards that a REST API may support multiple content types + // and we shouldn't fail. Handle this to complete the PR + body.content.first().unwrap(); + + if content_type.starts_with("application/json") { + let schema = match &media_type.schema { + None => Err(Error::unimplemented("JSON content without schema.")), + Some(schema) => Ok(schema), + }; + + let param = Param { + original_name: "".to_string(), + name: "value".to_string(), + tpe: ref_or_schema_type(schema?, ref_cache, Some(content_type))?, + required: body.required, + kind: ParamKind::Body, + }; + + Ok(vec![param]) + } else if content_type == "application/octet-stream" { + Ok(vec![Param { + original_name: "".to_string(), + name: "value".to_string(), + tpe: DataType::Binary, + required: body.required, + kind: ParamKind::Body, + }]) + } else if content_type == "application/x-yaml" { + let schema = match &media_type.schema { + None => Err(Error::unimplemented("YAML content without schema.")), + Some(schema) => Ok(schema), + }; + + let param = Param { + original_name: "".to_string(), + name: "value".to_string(), + tpe: ref_or_schema_type(schema?, ref_cache, Some(content_type))?, + required: body.required, + kind: ParamKind::Body, + }; + + Ok(vec![param]) + } else if content_type == "multipart/form-data" { + match &media_type.schema { + None => Err(Error::unimplemented("Multipart content without schema.")), + Some(schema) => match schema { + ReferenceOr::Reference { reference } => Err(Error::unimplemented(format!( + "Unexpected ref multipart schema: '{reference}'." + ))), + ReferenceOr::Item(schema) => match &schema.schema_kind { + SchemaKind::Type(Type::Object(obj)) => { + fn multipart_param( + name: &str, + required: bool, + schema: &ReferenceOr>, + ref_cache: &mut RefCache, + ) -> Result { + Ok(Param { + original_name: name.to_string(), + name: name.to_case(Case::Snake), + tpe: ref_or_box_schema_type(schema, ref_cache)?, + required, + kind: ParamKind::Multipart, + }) + } - Ok(vec![Param { - original_name: "".to_string(), - name: "value".to_string(), - tpe: ref_or_schema_type(schema?, ref_cache)?, - required: body.required, - kind: ParamKind::Body, - }]) - } else if content_type == "application/octet-stream" { - Ok(vec![Param { - original_name: "".to_string(), - name: "value".to_string(), - tpe: DataType::Binary, - required: body.required, - kind: ParamKind::Body, - }]) - } else if content_type == "multipart/form-data" { - match &media_type.schema { - None => Err(Error::unimplemented("Multipart content without schema.")), - Some(schema) => match schema { - ReferenceOr::Reference { reference } => Err(Error::unimplemented( - format!("Unexpected ref multipart schema: '{reference}'."), + obj.properties + .iter() + .map(|(name, schema)| { + multipart_param( + name, + body.required && obj.required.contains(name), + schema, + ref_cache, + ) + }) + .collect() + } + _ => Err(Error::unimplemented( + "Object schema expected for multipart request body.", )), - ReferenceOr::Item(schema) => match &schema.schema_kind { - SchemaKind::Type(Type::Object(obj)) => { - fn multipart_param( - name: &str, - required: bool, - schema: &ReferenceOr>, - ref_cache: &mut RefCache, - ) -> Result { - Ok(Param { - original_name: name.to_string(), - name: name.to_case(Case::Snake), - tpe: ref_or_box_schema_type(schema, ref_cache)?, - required, - kind: ParamKind::Multipart, - }) - } - - obj.properties - .iter() - .map(|(name, schema)| { - multipart_param( - name, - body.required && obj.required.contains(name), - schema, - ref_cache, - ) - }) - .collect() - } - _ => Err(Error::unimplemented( - "Object schema expected for multipart request body.", - )), - }, }, - } - } else { - Err(Error::unimplemented(format!( - "Request body content type: '{content_type}'." - ))) + }, } + } else { + Err(Error::unimplemented(format!( + "Request body content type: '{content_type}'." + ))) } } } @@ -449,7 +465,7 @@ fn response_type(response: &ReferenceOr, ref_cache: &mut RefCache) -> Some(schema) => Ok(schema), }; - Ok(ref_or_schema_type(schema?, ref_cache)?) + Ok(ref_or_schema_type(schema?, ref_cache, Some(content_type))?) } else if content_type == "application/octet-stream" { Ok(DataType::Binary) } else { @@ -837,7 +853,13 @@ fn render_method_implementation(method: &Method, error_kind: &ErrorKind) -> Rust r#"request = request.header(reqwest::header::CONTENT_TYPE, "application/octet-stream");"#, ) + NewLine - } else { + } else if param.tpe == DataType::Yaml { + line( + r#"request = request.header(reqwest::header::CONTENT_TYPE, "application/x-yaml");"#, + ) + NewLine + } + // Not sure why everything else is assumed to be json (previously) + else { line(unit() + "request = request.json(" + ¶m.name + ");") + NewLine } } diff --git a/src/rust/types.rs b/src/rust/types.rs index 23b451e..1f58763 100644 --- a/src/rust/types.rs +++ b/src/rust/types.rs @@ -73,6 +73,7 @@ pub enum DataType { Array(Box), MapOf(Box), Json, + Yaml } pub fn escape_keywords(name: &str) -> String { @@ -143,6 +144,11 @@ impl DataType { let res = rust_name("serde_json::value", "Value"); to_ref(res, top_param) } + + DataType::Yaml => { + let res = rust_name("serde_yaml::value", "Value"); + to_ref(res, top_param) + } } } } @@ -159,7 +165,7 @@ pub fn ref_type_name(reference: &str, ref_cache: &mut RefCache) -> Result Result { +fn schema_type(schema: &Schema, ref_cache: &mut RefCache, content_type: Option<&str>) -> Result { match &schema.schema_kind { SchemaKind::Type(tpe) => match tpe { Type::String(string_type) => { @@ -213,7 +219,7 @@ fn schema_type(schema: &Schema, ref_cache: &mut RefCache) -> Result { "Object parameter with Any additional_properties is not supported.", )), AdditionalProperties::Schema(element_schema) => Ok(DataType::MapOf( - Box::new(ref_or_schema_type(element_schema, ref_cache)?), + Box::new(ref_or_schema_type(element_schema, ref_cache, None)?), )), } } else { @@ -238,17 +244,30 @@ fn schema_type(schema: &Schema, ref_cache: &mut RefCache) -> Result { SchemaKind::AllOf { .. } => Err(Error::unimplemented("AllOf parameter is not supported.")), SchemaKind::AnyOf { .. } => Err(Error::unimplemented("AnyOf parameter is not supported.")), SchemaKind::Not { .. } => Err(Error::unimplemented("Not parameter is not supported.")), - SchemaKind::Any(_) => Ok(DataType::Json), + SchemaKind::Any(_) => { + if let Some(content_type) = content_type { + if content_type == "application/json" { + Ok(DataType::Json) + } else if content_type == "application/x-yaml" { + Ok(DataType::Yaml) + } else { + Err(Error::unexpected(format!("Cannot resolve the data type for content_type {} with `any` schema-kind", content_type))) + } + } else { + Err(Error::unexpected("Cannot resolve the data type for any schema-kind with no details on content_type")) + } + }, } } pub fn ref_or_schema_type( ref_or_schema: &ReferenceOr, ref_cache: &mut RefCache, + content_type: Option<&str> ) -> Result { match ref_or_schema { ReferenceOr::Reference { reference } => ref_type_name(reference, ref_cache), - ReferenceOr::Item(schema) => schema_type(schema, ref_cache), + ReferenceOr::Item(schema) => schema_type(schema, ref_cache, content_type), } } @@ -258,6 +277,6 @@ pub fn ref_or_box_schema_type( ) -> Result { match ref_or_schema { ReferenceOr::Reference { reference } => ref_type_name(reference, ref_cache), - ReferenceOr::Item(schema) => schema_type(schema, ref_cache), + ReferenceOr::Item(schema) => schema_type(schema, ref_cache, None), } } From 5b7e6ce6b8f50b0da3bb7981195ec5e1616474a4 Mon Sep 17 00:00:00 2001 From: Afsal Thaj Date: Mon, 4 Nov 2024 15:34:38 +1100 Subject: [PATCH 2/7] Make sure to send yaml as bytes --- src/rust/client_gen.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/rust/client_gen.rs b/src/rust/client_gen.rs index 1d6d285..7809ab4 100644 --- a/src/rust/client_gen.rs +++ b/src/rust/client_gen.rs @@ -854,9 +854,10 @@ fn render_method_implementation(method: &Method, error_kind: &ErrorKind) -> Rust ) + NewLine } else if param.tpe == DataType::Yaml { - line( - r#"request = request.header(reqwest::header::CONTENT_TYPE, "application/x-yaml");"#, - ) + NewLine + line(unit() + "request = request.body(serde_yaml::to_string(" + ¶m.name + ").unwrap_or_default().into_bytes());") + + line( + r#"request = request.header(reqwest::header::CONTENT_TYPE, "application/x-yaml");"#, + ) + NewLine } // Not sure why everything else is assumed to be json (previously) else { From e604370e86736aa1b82a17b5239817c0a02e7245 Mon Sep 17 00:00:00 2001 From: Afsal Thaj Date: Mon, 4 Nov 2024 16:40:15 +1100 Subject: [PATCH 3/7] Support all content_types --- src/rust.rs | 2 +- src/rust/client_gen.rs | 327 ++++++++++++++++++++++++++--------------- src/rust/printer.rs | 13 ++ src/rust/types.rs | 15 +- 4 files changed, 234 insertions(+), 123 deletions(-) diff --git a/src/rust.rs b/src/rust.rs index 8042153..c6233b9 100644 --- a/src/rust.rs +++ b/src/rust.rs @@ -18,4 +18,4 @@ pub(crate) mod error_gen; pub(crate) mod lib_gen; pub(crate) mod model_gen; mod printer; -pub(crate) mod types; +pub(crate) mod types; \ No newline at end of file diff --git a/src/rust/client_gen.rs b/src/rust/client_gen.rs index 7809ab4..304640e 100644 --- a/src/rust/client_gen.rs +++ b/src/rust/client_gen.rs @@ -27,7 +27,7 @@ use openapiv3::{ OpenAPI, Operation, Parameter, ParameterData, ParameterSchemaOrContent, PathItem, ReferenceOr, RequestBody, Response, Schema, SchemaKind, StatusCode, Tag, Type, }; -use std::collections::{BTreeMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet}; #[derive(Debug, Clone, PartialEq, Eq)] enum PathElement { @@ -186,6 +186,34 @@ struct Param { kind: ParamKind, } +#[derive(Debug, Clone)] +pub struct RequestBodyParams { + params: HashMap>, +} + +impl RequestBodyParams { + pub fn has_single_content_type(&self) -> bool { + self.params.len() == 1 + } + + pub fn get_default_request_body_param(&self) -> Option<&Vec> { + self.params.values().next().filter(|_| self.has_single_content_type()) + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct ContentType(pub String); + +impl ContentType { + pub fn is_json(&self) -> bool { + self.0 == "application/json" + } + + pub fn is_yaml(&self) -> bool { + self.0 == "application/x-yaml" + } +} + impl Param { fn render_declaration(&self) -> RustPrinter { let type_name = self.tpe.render_declaration(true); @@ -295,123 +323,120 @@ fn parameter(p: &ReferenceOr, ref_cache: &mut RefCache) -> Result, ref_cache: &mut RefCache, -) -> Result> { +) -> Result { + + let mut content_type_params = HashMap::new(); + match body { - ReferenceOr::Reference { reference } => Err(Error::unimplemented(format!( + ReferenceOr::Reference { reference } => return Err(Error::unimplemented(format!( "Unexpected ref request body: '{reference}'." ))), ReferenceOr::Item(body) => { - let (content_type, media_type) = - // TODO: It is in standards that a REST API may support multiple content types - // and we shouldn't fail. Handle this to complete the PR - body.content.first().unwrap(); - - if content_type.starts_with("application/json") { - let schema = match &media_type.schema { - None => Err(Error::unimplemented("JSON content without schema.")), - Some(schema) => Ok(schema), - }; - - let param = Param { - original_name: "".to_string(), - name: "value".to_string(), - tpe: ref_or_schema_type(schema?, ref_cache, Some(content_type))?, - required: body.required, - kind: ParamKind::Body, - }; - - Ok(vec![param]) - } else if content_type == "application/octet-stream" { - Ok(vec![Param { - original_name: "".to_string(), - name: "value".to_string(), - tpe: DataType::Binary, - required: body.required, - kind: ParamKind::Body, - }]) - } else if content_type == "application/x-yaml" { - let schema = match &media_type.schema { - None => Err(Error::unimplemented("YAML content without schema.")), - Some(schema) => Ok(schema), - }; - - let param = Param { - original_name: "".to_string(), - name: "value".to_string(), - tpe: ref_or_schema_type(schema?, ref_cache, Some(content_type))?, - required: body.required, - kind: ParamKind::Body, - }; - - Ok(vec![param]) - } else if content_type == "multipart/form-data" { - match &media_type.schema { - None => Err(Error::unimplemented("Multipart content without schema.")), - Some(schema) => match schema { - ReferenceOr::Reference { reference } => Err(Error::unimplemented(format!( - "Unexpected ref multipart schema: '{reference}'." - ))), - ReferenceOr::Item(schema) => match &schema.schema_kind { - SchemaKind::Type(Type::Object(obj)) => { - fn multipart_param( - name: &str, - required: bool, - schema: &ReferenceOr>, - ref_cache: &mut RefCache, - ) -> Result { - Ok(Param { - original_name: name.to_string(), - name: name.to_case(Case::Snake), - tpe: ref_or_box_schema_type(schema, ref_cache)?, - required, - kind: ParamKind::Multipart, - }) - } + for (content_type, media_type) in &body.content { + + if content_type.starts_with("application/json") { + let schema = match &media_type.schema { + None => Err(Error::unimplemented("JSON content without schema.")), + Some(schema) => Ok(schema), + }; + + content_type_params.insert(ContentType(content_type.clone()), vec![Param { + original_name: "".to_string(), + name: "value".to_string(), + tpe: ref_or_schema_type(schema?, ref_cache, Some(content_type.clone()))?, + required: body.required, + kind: ParamKind::Body, + }]); + } else if content_type == "application/octet-stream" { + content_type_params.insert(ContentType(content_type.clone()), vec![Param { + original_name: "".to_string(), + name: "value".to_string(), + tpe: DataType::Binary, + required: body.required, + kind: ParamKind::Body, + }]); + + } else if content_type == "application/x-yaml" { + let schema = match &media_type.schema { + None => Err(Error::unimplemented("YAML content without schema.")), + Some(schema) => Ok(schema), + }; + + let param = Param { + original_name: "".to_string(), + name: "value".to_string(), + tpe: ref_or_schema_type(schema?, ref_cache, Some(content_type.clone()))?, + required: body.required, + kind: ParamKind::Body, + }; - obj.properties - .iter() - .map(|(name, schema)| { - multipart_param( - name, - body.required && obj.required.contains(name), - schema, - ref_cache, - ) - }) - .collect() - } - _ => Err(Error::unimplemented( - "Object schema expected for multipart request body.", - )), + content_type_params.insert(ContentType(content_type.clone()), vec![param]); + + } else if content_type == "multipart/form-data" { + match &media_type.schema { + None => return Err(Error::unimplemented("Multipart content without schema.")), + Some(schema) => match schema { + ReferenceOr::Reference { reference } => return Err(Error::unimplemented(format!( + "Unexpected ref multipart schema: '{reference}'." + ))), + ReferenceOr::Item(schema) => match &schema.schema_kind { + SchemaKind::Type(Type::Object(obj)) => { + fn multipart_param( + name: &str, + required: bool, + schema: &ReferenceOr>, + ref_cache: &mut RefCache, + ) -> Result { + Ok(Param { + original_name: name.to_string(), + name: name.to_case(Case::Snake), + tpe: ref_or_box_schema_type(schema, ref_cache)?, + required, + kind: ParamKind::Multipart, + }) + } + + let params = obj.properties + .iter() + .map(|(name, schema)| { + multipart_param( + name, + body.required && obj.required.contains(name), + schema, + ref_cache, + ) + }) + .collect::>>()?; + + content_type_params.insert(ContentType(content_type.clone()), params); + } + _ => return Err(Error::unimplemented( + "Object schema expected for multipart request body.", + )), + }, }, - }, + } + } else { + return Err(Error::unimplemented(format!( + "Request body content type: '{content_type}'." + ))) } - } else { - Err(Error::unimplemented(format!( - "Request body content type: '{content_type}'." - ))) } } } + + Ok(RequestBodyParams { + params: content_type_params + }) } fn parameters(op: &PathOperation, ref_cache: &mut RefCache) -> Result> { - let params: Result> = op + op .op .parameters .iter() .map(|p| parameter(p, ref_cache)) - .collect(); - - let mut params = params?; - - if let Some(body) = &op.op.request_body { - for p in request_body_params(body, ref_cache)? { - params.push(p); - } - } - - Ok(params) + .collect() } fn as_code(code: &StatusCode) -> Option { @@ -465,7 +490,7 @@ fn response_type(response: &ReferenceOr, ref_cache: &mut RefCache) -> Some(schema) => Ok(schema), }; - Ok(ref_or_schema_type(schema?, ref_cache, Some(content_type))?) + Ok(ref_or_schema_type(schema?, ref_cache, Some(content_type.clone()))?) } else if content_type == "application/octet-stream" { Ok(DataType::Binary) } else { @@ -514,7 +539,7 @@ fn trait_method( op: &PathOperation, prefix_length: usize, ref_cache: &mut RefCache, -) -> Result { +) -> Result> { let (result_code, result_type) = method_result(&op.op.responses.responses, ref_cache)?; let name = if let Some(op_id) = &op.op.operation_id { @@ -523,16 +548,86 @@ fn trait_method( op.path.strip_prefix(prefix_length).method_name(&op.method) }; - Ok(Method { - name, - path: op.path.clone(), - original_path: op.original_path.clone(), - http_method: op.method.to_string(), - params: parameters(op, ref_cache)?, - result: result_type, - result_status_code: result_code.clone(), - errors: method_errors(&op.op.responses.responses, result_code, ref_cache)?, - }) + let mut main_params = parameters(op, ref_cache)?; + + if let Some(body) = &op.op.request_body { + let content_specific = request_body_params(body, ref_cache)?; + + if let Some(request_body_params) = content_specific.get_default_request_body_param() { + for i in request_body_params { + main_params.push(i.clone()) + } + + Ok(vec![Method { + name, + path: op.path.clone(), + original_path: op.original_path.clone(), + http_method: op.method.to_string(), + params: main_params, + result: result_type, + result_status_code: result_code.clone(), + errors: method_errors(&op.op.responses.responses, result_code, ref_cache)?, + }]) + } else { + let mut methods = vec![]; + + for (content_type, params) in content_specific.params { + if content_type.is_json() { + let mut new_params = main_params.clone(); + + for i in params { + new_params.push(i) + } + + let method_name = format!("{}_json", name); + methods.push( + Method { + name: method_name, + path: op.path.clone(), + original_path: op.original_path.clone(), + http_method: op.method.to_string(), + params: new_params.clone(), + result: result_type.clone(), + result_status_code: result_code.clone(), + errors: method_errors(&op.op.responses.responses, result_code.clone(), ref_cache)?, + }); + } else if content_type.is_yaml() { + let mut new_params = main_params.clone(); + + for i in params { + new_params.push(i) + } + + let method_name = format!("{}_yaml", name); + + methods.push( + Method { + name: method_name, + path: op.path.clone(), + original_path: op.original_path.clone(), + http_method: op.method.to_string(), + params: new_params.clone(), + result: result_type.clone(), + result_status_code: result_code.clone(), + errors: method_errors(&op.op.responses.responses, result_code.clone(), ref_cache)?, + }); + } + } + + Ok(methods) + } + } else { + Ok(vec![Method { + name, + path: op.path.clone(), + original_path: op.original_path.clone(), + http_method: op.method.to_string(), + params: main_params, + result: result_type, + result_status_code: result_code.clone(), + errors: method_errors(&op.op.responses.responses, result_code, ref_cache)?, + }]) + } } fn trait_methods( @@ -540,10 +635,12 @@ fn trait_methods( prefix_length: usize, ref_cache: &mut RefCache, ) -> Result> { - operations + let res = operations .iter() .map(|op| trait_method(op, prefix_length, ref_cache)) - .collect() + .collect::>>>()?; + + Ok(res.into_iter().flatten().collect()) } fn render_errors(method_name: &str, error_kind: &ErrorKind, errors: &MethodErrors) -> RustResult { diff --git a/src/rust/printer.rs b/src/rust/printer.rs index 416d91d..e0e5be5 100644 --- a/src/rust/printer.rs +++ b/src/rust/printer.rs @@ -135,6 +135,19 @@ pub fn rust_name(import: &str, name: &str) -> TreePrinter { }) } +pub fn rust_name_with_alias(import: &str, name: &str, alias: &str) -> TreePrinter { + let import_name = if name.ends_with('!') { + &name[0..name.len() - 1] + } else { + name + }; + TreePrinter::leaf(RustCode { + imports: HashSet::from([RustUse(format!("{import}::{import_name} as {alias}"))]), + code: alias.to_string(), + }) +} + + impl IntoRustTree for TreePrinter { fn tree(self) -> TreePrinter { self diff --git a/src/rust/types.rs b/src/rust/types.rs index 1f58763..2473cb2 100644 --- a/src/rust/types.rs +++ b/src/rust/types.rs @@ -15,7 +15,7 @@ use crate::printer::TreePrinter; use crate::rust::lib_gen::ModuleName; use crate::rust::model_gen::RefCache; -use crate::rust::printer::{rust_name, unit, RustContext}; +use crate::rust::printer::{rust_name, rust_name_with_alias, unit, RustContext}; use crate::{Error, Result}; use convert_case::{Case, Casing}; use openapiv3::{ @@ -73,9 +73,10 @@ pub enum DataType { Array(Box), MapOf(Box), Json, - Yaml + Yaml, } + pub fn escape_keywords(name: &str) -> String { if name == "type" { "r#type".to_string() @@ -146,7 +147,7 @@ impl DataType { } DataType::Yaml => { - let res = rust_name("serde_yaml::value", "Value"); + let res = rust_name_with_alias("serde_yaml::value", "Value", "YamlValue"); to_ref(res, top_param) } } @@ -165,7 +166,7 @@ pub fn ref_type_name(reference: &str, ref_cache: &mut RefCache) -> Result) -> Result { +fn schema_type(schema: &Schema, ref_cache: &mut RefCache, content_type: Option) -> Result { match &schema.schema_kind { SchemaKind::Type(tpe) => match tpe { Type::String(string_type) => { @@ -246,9 +247,9 @@ fn schema_type(schema: &Schema, ref_cache: &mut RefCache, content_type: Option<& SchemaKind::Not { .. } => Err(Error::unimplemented("Not parameter is not supported.")), SchemaKind::Any(_) => { if let Some(content_type) = content_type { - if content_type == "application/json" { + if &content_type == "application/json" { Ok(DataType::Json) - } else if content_type == "application/x-yaml" { + } else if &content_type == "application/x-yaml" { Ok(DataType::Yaml) } else { Err(Error::unexpected(format!("Cannot resolve the data type for content_type {} with `any` schema-kind", content_type))) @@ -263,7 +264,7 @@ fn schema_type(schema: &Schema, ref_cache: &mut RefCache, content_type: Option<& pub fn ref_or_schema_type( ref_or_schema: &ReferenceOr, ref_cache: &mut RefCache, - content_type: Option<&str> + content_type: Option ) -> Result { match ref_or_schema { ReferenceOr::Reference { reference } => ref_type_name(reference, ref_cache), From 70c44d3586e026605444ab0589aa7b9674fbd810 Mon Sep 17 00:00:00 2001 From: Afsal Thaj Date: Mon, 4 Nov 2024 17:16:13 +1100 Subject: [PATCH 4/7] Reformat --- src/rust.rs | 2 +- src/rust/client_gen.rs | 160 ++++++++++++++++++++++++----------------- src/rust/printer.rs | 1 - src/rust/types.rs | 16 +++-- 4 files changed, 108 insertions(+), 71 deletions(-) diff --git a/src/rust.rs b/src/rust.rs index c6233b9..8042153 100644 --- a/src/rust.rs +++ b/src/rust.rs @@ -18,4 +18,4 @@ pub(crate) mod error_gen; pub(crate) mod lib_gen; pub(crate) mod model_gen; mod printer; -pub(crate) mod types; \ No newline at end of file +pub(crate) mod types; diff --git a/src/rust/client_gen.rs b/src/rust/client_gen.rs index 304640e..3d13608 100644 --- a/src/rust/client_gen.rs +++ b/src/rust/client_gen.rs @@ -197,7 +197,10 @@ impl RequestBodyParams { } pub fn get_default_request_body_param(&self) -> Option<&Vec> { - self.params.values().next().filter(|_| self.has_single_content_type()) + self.params + .values() + .next() + .filter(|_| self.has_single_content_type()) } } @@ -324,38 +327,47 @@ fn request_body_params( body: &ReferenceOr, ref_cache: &mut RefCache, ) -> Result { - let mut content_type_params = HashMap::new(); match body { - ReferenceOr::Reference { reference } => return Err(Error::unimplemented(format!( - "Unexpected ref request body: '{reference}'." - ))), + ReferenceOr::Reference { reference } => { + return Err(Error::unimplemented(format!( + "Unexpected ref request body: '{reference}'." + ))) + } ReferenceOr::Item(body) => { - for (content_type, media_type) in &body.content { - + for (content_type, media_type) in &body.content { if content_type.starts_with("application/json") { let schema = match &media_type.schema { None => Err(Error::unimplemented("JSON content without schema.")), Some(schema) => Ok(schema), }; - content_type_params.insert(ContentType(content_type.clone()), vec![Param { - original_name: "".to_string(), - name: "value".to_string(), - tpe: ref_or_schema_type(schema?, ref_cache, Some(content_type.clone()))?, - required: body.required, - kind: ParamKind::Body, - }]); + content_type_params.insert( + ContentType(content_type.clone()), + vec![Param { + original_name: "".to_string(), + name: "value".to_string(), + tpe: ref_or_schema_type( + schema?, + ref_cache, + Some(content_type.clone()), + )?, + required: body.required, + kind: ParamKind::Body, + }], + ); } else if content_type == "application/octet-stream" { - content_type_params.insert(ContentType(content_type.clone()), vec![Param { - original_name: "".to_string(), - name: "value".to_string(), - tpe: DataType::Binary, - required: body.required, - kind: ParamKind::Body, - }]); - + content_type_params.insert( + ContentType(content_type.clone()), + vec![Param { + original_name: "".to_string(), + name: "value".to_string(), + tpe: DataType::Binary, + required: body.required, + kind: ParamKind::Body, + }], + ); } else if content_type == "application/x-yaml" { let schema = match &media_type.schema { None => Err(Error::unimplemented("YAML content without schema.")), @@ -371,14 +383,17 @@ fn request_body_params( }; content_type_params.insert(ContentType(content_type.clone()), vec![param]); - } else if content_type == "multipart/form-data" { match &media_type.schema { - None => return Err(Error::unimplemented("Multipart content without schema.")), + None => { + return Err(Error::unimplemented("Multipart content without schema.")) + } Some(schema) => match schema { - ReferenceOr::Reference { reference } => return Err(Error::unimplemented(format!( - "Unexpected ref multipart schema: '{reference}'." - ))), + ReferenceOr::Reference { reference } => { + return Err(Error::unimplemented(format!( + "Unexpected ref multipart schema: '{reference}'." + ))) + } ReferenceOr::Item(schema) => match &schema.schema_kind { SchemaKind::Type(Type::Object(obj)) => { fn multipart_param( @@ -396,7 +411,8 @@ fn request_body_params( }) } - let params = obj.properties + let params = obj + .properties .iter() .map(|(name, schema)| { multipart_param( @@ -408,31 +424,33 @@ fn request_body_params( }) .collect::>>()?; - content_type_params.insert(ContentType(content_type.clone()), params); + content_type_params + .insert(ContentType(content_type.clone()), params); + } + _ => { + return Err(Error::unimplemented( + "Object schema expected for multipart request body.", + )) } - _ => return Err(Error::unimplemented( - "Object schema expected for multipart request body.", - )), }, }, } } else { - return Err(Error::unimplemented(format!( + return Err(Error::unimplemented(format!( "Request body content type: '{content_type}'." - ))) + ))); } } } } Ok(RequestBodyParams { - params: content_type_params + params: content_type_params, }) } fn parameters(op: &PathOperation, ref_cache: &mut RefCache) -> Result> { - op - .op + op.op .parameters .iter() .map(|p| parameter(p, ref_cache)) @@ -490,7 +508,11 @@ fn response_type(response: &ReferenceOr, ref_cache: &mut RefCache) -> Some(schema) => Ok(schema), }; - Ok(ref_or_schema_type(schema?, ref_cache, Some(content_type.clone()))?) + Ok(ref_or_schema_type( + schema?, + ref_cache, + Some(content_type.clone()), + )?) } else if content_type == "application/octet-stream" { Ok(DataType::Binary) } else { @@ -580,17 +602,20 @@ fn trait_method( } let method_name = format!("{}_json", name); - methods.push( - Method { - name: method_name, - path: op.path.clone(), - original_path: op.original_path.clone(), - http_method: op.method.to_string(), - params: new_params.clone(), - result: result_type.clone(), - result_status_code: result_code.clone(), - errors: method_errors(&op.op.responses.responses, result_code.clone(), ref_cache)?, - }); + methods.push(Method { + name: method_name, + path: op.path.clone(), + original_path: op.original_path.clone(), + http_method: op.method.to_string(), + params: new_params.clone(), + result: result_type.clone(), + result_status_code: result_code.clone(), + errors: method_errors( + &op.op.responses.responses, + result_code.clone(), + ref_cache, + )?, + }); } else if content_type.is_yaml() { let mut new_params = main_params.clone(); @@ -600,17 +625,20 @@ fn trait_method( let method_name = format!("{}_yaml", name); - methods.push( - Method { - name: method_name, - path: op.path.clone(), - original_path: op.original_path.clone(), - http_method: op.method.to_string(), - params: new_params.clone(), - result: result_type.clone(), - result_status_code: result_code.clone(), - errors: method_errors(&op.op.responses.responses, result_code.clone(), ref_cache)?, - }); + methods.push(Method { + name: method_name, + path: op.path.clone(), + original_path: op.original_path.clone(), + http_method: op.method.to_string(), + params: new_params.clone(), + result: result_type.clone(), + result_status_code: result_code.clone(), + errors: method_errors( + &op.op.responses.responses, + result_code.clone(), + ref_cache, + )?, + }); } } @@ -951,10 +979,14 @@ fn render_method_implementation(method: &Method, error_kind: &ErrorKind) -> Rust ) + NewLine } else if param.tpe == DataType::Yaml { - line(unit() + "request = request.body(serde_yaml::to_string(" + ¶m.name + ").unwrap_or_default().into_bytes());") + - line( - r#"request = request.header(reqwest::header::CONTENT_TYPE, "application/x-yaml");"#, - ) + NewLine + line( + unit() + + "request = request.body(serde_yaml::to_string(" + + ¶m.name + + ").unwrap_or_default().into_bytes());", + ) + line( + r#"request = request.header(reqwest::header::CONTENT_TYPE, "application/x-yaml");"#, + ) + NewLine } // Not sure why everything else is assumed to be json (previously) else { diff --git a/src/rust/printer.rs b/src/rust/printer.rs index e0e5be5..9ef5db7 100644 --- a/src/rust/printer.rs +++ b/src/rust/printer.rs @@ -147,7 +147,6 @@ pub fn rust_name_with_alias(import: &str, name: &str, alias: &str) -> TreePrinte }) } - impl IntoRustTree for TreePrinter { fn tree(self) -> TreePrinter { self diff --git a/src/rust/types.rs b/src/rust/types.rs index 2473cb2..2924c05 100644 --- a/src/rust/types.rs +++ b/src/rust/types.rs @@ -76,7 +76,6 @@ pub enum DataType { Yaml, } - pub fn escape_keywords(name: &str) -> String { if name == "type" { "r#type".to_string() @@ -166,7 +165,11 @@ pub fn ref_type_name(reference: &str, ref_cache: &mut RefCache) -> Result) -> Result { +fn schema_type( + schema: &Schema, + ref_cache: &mut RefCache, + content_type: Option, +) -> Result { match &schema.schema_kind { SchemaKind::Type(tpe) => match tpe { Type::String(string_type) => { @@ -252,19 +255,22 @@ fn schema_type(schema: &Schema, ref_cache: &mut RefCache, content_type: Option, ref_cache: &mut RefCache, - content_type: Option + content_type: Option, ) -> Result { match ref_or_schema { ReferenceOr::Reference { reference } => ref_type_name(reference, ref_cache), From eaeb965be794ba07ec7e7e8690f608968f8ab543 Mon Sep 17 00:00:00 2001 From: Afsal Thaj Date: Mon, 4 Nov 2024 17:17:26 +1100 Subject: [PATCH 5/7] Fix checks --- src/rust/client_gen.rs | 4 ++-- src/rust/lib_gen.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/rust/client_gen.rs b/src/rust/client_gen.rs index 3d13608..b19f385 100644 --- a/src/rust/client_gen.rs +++ b/src/rust/client_gen.rs @@ -192,11 +192,11 @@ pub struct RequestBodyParams { } impl RequestBodyParams { - pub fn has_single_content_type(&self) -> bool { + fn has_single_content_type(&self) -> bool { self.params.len() == 1 } - pub fn get_default_request_body_param(&self) -> Option<&Vec> { + fn get_default_request_body_param(&self) -> Option<&Vec> { self.params .values() .next() diff --git a/src/rust/lib_gen.rs b/src/rust/lib_gen.rs index 80e9da5..f01d9ad 100644 --- a/src/rust/lib_gen.rs +++ b/src/rust/lib_gen.rs @@ -142,11 +142,11 @@ mod tests { "lib", &[ ModuleDef { - name: ModuleName::new("abc".to_string()), + name: ModuleName::new("abc"), exports: vec!["C".to_string(), "B".to_string()], }, ModuleDef { - name: ModuleName::new("xyz".to_string()), + name: ModuleName::new("xyz"), exports: vec!["A".to_string(), "Y".to_string()], }, ], From be8adf90f16d264f15c6eafd7d35f73dad74479b Mon Sep 17 00:00:00 2001 From: Afsal Thaj Date: Mon, 4 Nov 2024 23:10:49 +1100 Subject: [PATCH 6/7] use json if content type is * --- src/rust/client_gen.rs | 4 ++-- src/rust/types.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/rust/client_gen.rs b/src/rust/client_gen.rs index b19f385..baa04ca 100644 --- a/src/rust/client_gen.rs +++ b/src/rust/client_gen.rs @@ -337,7 +337,7 @@ fn request_body_params( } ReferenceOr::Item(body) => { for (content_type, media_type) in &body.content { - if content_type.starts_with("application/json") { + if content_type.starts_with("application/json") || content_type == "*/*" { let schema = match &media_type.schema { None => Err(Error::unimplemented("JSON content without schema.")), Some(schema) => Ok(schema), @@ -368,7 +368,7 @@ fn request_body_params( kind: ParamKind::Body, }], ); - } else if content_type == "application/x-yaml" { + } else if content_type.contains("application/x-yaml") { let schema = match &media_type.schema { None => Err(Error::unimplemented("YAML content without schema.")), Some(schema) => Ok(schema), diff --git a/src/rust/types.rs b/src/rust/types.rs index 2924c05..d0d0c8a 100644 --- a/src/rust/types.rs +++ b/src/rust/types.rs @@ -250,7 +250,7 @@ fn schema_type( SchemaKind::Not { .. } => Err(Error::unimplemented("Not parameter is not supported.")), SchemaKind::Any(_) => { if let Some(content_type) = content_type { - if &content_type == "application/json" { + if &content_type == "application/json" || &content_type == "*/*" { Ok(DataType::Json) } else if &content_type == "application/x-yaml" { Ok(DataType::Yaml) From e30d452e38b8a90c1784abaee2c8b3bd2a97c066 Mon Sep 17 00:00:00 2001 From: Afsal Thaj Date: Tue, 5 Nov 2024 14:35:15 +1100 Subject: [PATCH 7/7] Update client_gen --- src/rust/client_gen.rs | 149 +++++++++++++++++++---------------------- 1 file changed, 69 insertions(+), 80 deletions(-) diff --git a/src/rust/client_gen.rs b/src/rust/client_gen.rs index baa04ca..6ef16b3 100644 --- a/src/rust/client_gen.rs +++ b/src/rust/client_gen.rs @@ -557,18 +557,19 @@ fn method_errors( Ok(MethodErrors { codes: codes? }) } -fn trait_method( +fn trait_methods_specific_to_content_type( op: &PathOperation, prefix_length: usize, ref_cache: &mut RefCache, ) -> Result> { let (result_code, result_type) = method_result(&op.op.responses.responses, ref_cache)?; - let name = if let Some(op_id) = &op.op.operation_id { - op_id.to_case(Case::Snake) - } else { - op.path.strip_prefix(prefix_length).method_name(&op.method) - }; + let name = op + .op + .operation_id + .as_ref() + .map(|op_id| op_id.to_case(Case::Snake)) + .unwrap_or_else(|| op.path.strip_prefix(prefix_length).method_name(&op.method)); let mut main_params = parameters(op, ref_cache)?; @@ -576,85 +577,73 @@ fn trait_method( let content_specific = request_body_params(body, ref_cache)?; if let Some(request_body_params) = content_specific.get_default_request_body_param() { - for i in request_body_params { - main_params.push(i.clone()) - } - - Ok(vec![Method { + main_params.extend(request_body_params.iter().cloned()); + return Ok(vec![create_method( name, - path: op.path.clone(), - original_path: op.original_path.clone(), - http_method: op.method.to_string(), - params: main_params, - result: result_type, - result_status_code: result_code.clone(), - errors: method_errors(&op.op.responses.responses, result_code, ref_cache)?, - }]) - } else { - let mut methods = vec![]; - - for (content_type, params) in content_specific.params { - if content_type.is_json() { - let mut new_params = main_params.clone(); + op, + &main_params, + result_type, + result_code, + ref_cache, + )?]); + } - for i in params { - new_params.push(i) - } + let mut methods = Vec::new(); + for (content_type, params) in content_specific.params { + let method_name = match_content_type(content_type, &name)?; + let new_params = [main_params.clone(), params].concat(); + methods.push(create_method( + method_name, + op, + &new_params, + result_type.clone(), + result_code.clone(), + ref_cache, + )?); + } - let method_name = format!("{}_json", name); - methods.push(Method { - name: method_name, - path: op.path.clone(), - original_path: op.original_path.clone(), - http_method: op.method.to_string(), - params: new_params.clone(), - result: result_type.clone(), - result_status_code: result_code.clone(), - errors: method_errors( - &op.op.responses.responses, - result_code.clone(), - ref_cache, - )?, - }); - } else if content_type.is_yaml() { - let mut new_params = main_params.clone(); - - for i in params { - new_params.push(i) - } + Ok(methods) + } else { + Ok(vec![create_method( + name, + op, + &main_params, + result_type, + result_code, + ref_cache, + )?]) + } +} - let method_name = format!("{}_yaml", name); - - methods.push(Method { - name: method_name, - path: op.path.clone(), - original_path: op.original_path.clone(), - http_method: op.method.to_string(), - params: new_params.clone(), - result: result_type.clone(), - result_status_code: result_code.clone(), - errors: method_errors( - &op.op.responses.responses, - result_code.clone(), - ref_cache, - )?, - }); - } - } +fn create_method( + name: String, + op: &PathOperation, + params: &[Param], + result_type: DataType, + result_code: StatusCode, + ref_cache: &mut RefCache, +) -> Result { + Ok(Method { + name, + path: op.path.clone(), + original_path: op.original_path.clone(), + http_method: op.method.to_string(), + params: params.to_vec(), + result: result_type, + result_status_code: result_code.clone(), + errors: method_errors(&op.op.responses.responses, result_code, ref_cache)?, + }) +} - Ok(methods) - } +fn match_content_type(content_type: ContentType, base_name: &str) -> Result { + if content_type.is_json() { + Ok(format!("{}_json", base_name)) + } else if content_type.is_yaml() { + Ok(format!("{}_yaml", base_name)) } else { - Ok(vec![Method { - name, - path: op.path.clone(), - original_path: op.original_path.clone(), - http_method: op.method.to_string(), - params: main_params, - result: result_type, - result_status_code: result_code.clone(), - errors: method_errors(&op.op.responses.responses, result_code, ref_cache)?, - }]) + Err(Error::unimplemented( + "Multiple content types supported only for JSON and YAML", + )) } } @@ -665,7 +654,7 @@ fn trait_methods( ) -> Result> { let res = operations .iter() - .map(|op| trait_method(op, prefix_length, ref_cache)) + .map(|op| trait_methods_specific_to_content_type(op, prefix_length, ref_cache)) .collect::>>>()?; Ok(res.into_iter().flatten().collect())