diff options
author | Cyborus <cyborus@cyborus.xyz> | 2024-01-27 18:47:51 +0100 |
---|---|---|
committer | Cyborus <cyborus@cyborus.xyz> | 2024-01-27 19:00:11 +0100 |
commit | 2c467ea6cf02f87fb0663734b280cc323fa894da (patch) | |
tree | 3e120734faae4d12591b3fefa3dd85bf232ccf73 /generator | |
parent | remove debug panic (diff) | |
download | forgejo-api-2c467ea6cf02f87fb0663734b280cc323fa894da.tar.xz forgejo-api-2c467ea6cf02f87fb0663734b280cc323fa894da.zip |
split generator into modules
Diffstat (limited to 'generator')
-rw-r--r-- | generator/src/main.rs | 904 | ||||
-rw-r--r-- | generator/src/methods.rs | 562 | ||||
-rw-r--r-- | generator/src/structs.rs | 338 |
3 files changed, 906 insertions, 898 deletions
diff --git a/generator/src/main.rs b/generator/src/main.rs index 0342a7b..4e4054c 100644 --- a/generator/src/main.rs +++ b/generator/src/main.rs @@ -1,38 +1,17 @@ use std::ffi::{OsStr, OsString}; +mod methods; mod openapi; +mod structs; -use eyre::Context; -use heck::{ToPascalCase, ToSnakeCase}; -use openapi::{ - CollectionFormat, Items, MaybeRef, OpenApiV2, Operation, Parameter, ParameterIn, ParameterType, - Primitive, Response, Schema, SchemaType, -}; -use std::fmt::Write; +use heck::ToSnakeCase; +use openapi::*; fn main() -> eyre::Result<()> { let spec = get_spec()?; let mut s = String::new(); - s.push_str("use crate::ForgejoError;\n"); - s.push_str("impl crate::Forgejo {\n"); - for (path, item) in &spec.paths { - s.push_str(&create_methods_for_path(&spec, path, item).wrap_err_with(|| path.clone())?); - } - s.push_str("}\n"); - - s.push_str("use structs::*;\n"); - s.push_str("pub mod structs {\n"); - if let Some(definitions) = &spec.definitions { - for (name, schema) in definitions { - let strukt = create_struct_for_definition(&spec, name, schema)?; - s.push_str(&strukt); - } - } - for (path, item) in &spec.paths { - let strukt = create_query_structs_for_path(&spec, path, item)?; - s.push_str(&strukt); - } - s.push_str("\n}"); + s.push_str(&methods::create_methods(&spec)?); + s.push_str(&structs::create_structs(&spec)?); save_generated(&s)?; Ok(()) } @@ -65,187 +44,6 @@ fn run_rustfmt_on(path: &OsStr) { } } -fn create_methods_for_path( - spec: &OpenApiV2, - path: &str, - item: &openapi::PathItem, -) -> eyre::Result<String> { - let mut s = String::new(); - if let Some(op) = &item.get { - s.push_str(&create_get_method(spec, path, op).wrap_err("GET")?); - } - if let Some(op) = &item.put { - s.push_str(&create_put_method(spec, path, op).wrap_err("PUT")?); - } - if let Some(op) = &item.post { - s.push_str(&create_post_method(spec, path, op).wrap_err("POST")?); - } - if let Some(op) = &item.delete { - s.push_str(&create_delete_method(spec, path, op).wrap_err("DELETE")?); - } - if let Some(op) = &item.options { - s.push_str(&create_options_method(spec, path, op).wrap_err("OPTIONS")?); - } - if let Some(op) = &item.head { - s.push_str(&create_head_method(spec, path, op).wrap_err("HEAD")?); - } - if let Some(op) = &item.patch { - s.push_str(&create_patch_method(spec, path, op).wrap_err("PATCH")?); - } - Ok(s) -} - -fn fn_signature_from_op(spec: &OpenApiV2, op: &Operation) -> eyre::Result<String> { - let name = op - .operation_id - .as_deref() - .ok_or_else(|| eyre::eyre!("operation did not have id"))? - .to_snake_case() - .replace("o_auth2", "oauth2"); - let args = fn_args_from_op(spec, op)?; - let ty = fn_return_from_op(spec, op)?; - Ok(format!( - "pub async fn {name}({args}) -> Result<{ty}, ForgejoError>" - )) -} - -fn fn_args_from_op(spec: &OpenApiV2, op: &Operation) -> eyre::Result<String> { - let mut args = "&self".to_string(); - let mut has_query = false; - let mut has_headers = false; - let mut has_form = false; - if let Some(params) = &op.parameters { - for param in params { - let param = match ¶m { - MaybeRef::Value { value } => value, - MaybeRef::Ref { _ref } => eyre::bail!("todo: add deref parameters"), - }; - match param._in { - ParameterIn::Path => { - let type_name = param_type(¶m, false)?; - args.push_str(", "); - args.push_str(&sanitize_ident(¶m.name)); - args.push_str(": "); - args.push_str(&type_name); - } - ParameterIn::Query => has_query = true, - ParameterIn::Header => has_headers = true, - ParameterIn::Body => { - let schema_ref = param.schema.as_ref().unwrap(); - let ty = schema_ref_type_name(spec, &schema_ref)?; - args.push_str(", "); - args.push_str(&sanitize_ident(¶m.name)); - args.push_str(": "); - args.push_str(&ty); - } - ParameterIn::FormData => { - args.push_str(", "); - args.push_str(&sanitize_ident(¶m.name)); - args.push_str(": Vec<u8>"); - } - } - } - } - if has_query { - let query_ty = query_struct_name(op)?; - args.push_str(", query: "); - args.push_str(&query_ty); - } - Ok(args) -} - -fn query_struct_name(op: &Operation) -> eyre::Result<String> { - let mut ty = op - .operation_id - .as_deref() - .ok_or_else(|| eyre::eyre!("operation did not have id"))? - .to_pascal_case() - .replace("o_auth2", "oauth2"); - ty.push_str("Query"); - Ok(ty) -} - -fn fn_return_from_op(spec: &OpenApiV2, op: &Operation) -> eyre::Result<ResponseType> { - let mut responses = op - .responses - .http_codes - .iter() - .filter(|(k, _)| k.starts_with("2")) - .map(|(_, v)| response_ref_type_name(spec, v)) - .collect::<Result<Vec<_>, _>>()?; - let mut iter = responses.into_iter(); - let mut response = iter - .next() - .ok_or_else(|| eyre::eyre!("must have at least one response type"))?; - for next in iter { - response = response.merge(next)?; - } - - Ok(response) -} - -#[derive(Debug, Default)] -struct ResponseType { - headers: Option<String>, - body: Option<String>, -} - -impl ResponseType { - fn merge(self, other: Self) -> eyre::Result<Self> { - let mut new = Self::default(); - match (self.headers, other.headers) { - (Some(a), Some(b)) if a != b => eyre::bail!("incompatible header types in response"), - (Some(a), None) => new.headers = Some(format!("Option<{a}>")), - (None, Some(b)) => new.headers = Some(format!("Option<{b}>")), - (a, b) => new.headers = a.or(b), - }; - match (self.body.as_deref(), other.body.as_deref()) { - (Some(a), Some(b)) if a != b => eyre::bail!("incompatible header types in response"), - (Some(a), Some("()") | None) => new.body = Some(format!("Option<{a}>")), - (Some("()") | None, Some(b)) => new.body = Some(format!("Option<{b}>")), - (a, b) => new.body = self.body.or(other.body), - }; - Ok(new) - } -} - -impl std::fmt::Display for ResponseType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut tys = Vec::new(); - tys.extend(self.headers.as_deref()); - tys.extend(self.body.as_deref()); - match tys[..] { - [single] => f.write_str(single), - _ => { - write!(f, "(")?; - for (i, item) in tys.iter().copied().enumerate() { - f.write_str(item)?; - if i + 1 < tys.len() { - write!(f, ", ")?; - } - } - write!(f, ")")?; - Ok(()) - } - } - } -} - -fn response_ref_type_name( - spec: &OpenApiV2, - schema: &MaybeRef<Response>, -) -> eyre::Result<ResponseType> { - let (_, response) = deref_response(spec, schema)?; - let mut ty = ResponseType::default(); - if response.headers.is_some() { - ty.headers = Some("reqwest::header::HeaderMap".into()); - } - if let Some(schema) = &response.schema { - ty.body = Some(schema_ref_type_name(spec, schema)?); - }; - Ok(ty) -} - fn schema_ref_type_name(spec: &OpenApiV2, schema: &MaybeRef<Schema>) -> eyre::Result<String> { let (name, schema) = deref_definition(spec, &schema)?; schema_type_name(spec, name, schema) @@ -306,125 +104,6 @@ fn schema_type_name( } } -fn param_type(param: &Parameter, owned: bool) -> eyre::Result<String> { - let _type = param - ._type - .as_ref() - .ok_or_else(|| eyre::eyre!("no type provided for path param"))?; - param_type_inner(_type, param.format.as_deref(), param.items.as_ref(), owned) -} - -fn param_type_inner( - ty: &ParameterType, - format: Option<&str>, - items: Option<&Items>, - owned: bool, -) -> eyre::Result<String> { - let ty_name = match ty { - ParameterType::String => match format.as_deref() { - Some("date") => "time::Date", - Some("date-time") => "time::OffsetDateTime", - _ => { - if owned { - "String" - } else { - "&str" - } - } - } - .into(), - ParameterType::Number => match format.as_deref() { - Some("float") => "f32", - Some("double") => "f64", - _ => "f64", - } - .into(), - ParameterType::Integer => match format.as_deref() { - Some("int32") => "u32", - Some("int64") => "u64", - _ => "u32", - } - .into(), - ParameterType::Boolean => "bool".into(), - ParameterType::Array => { - let item = items - .as_ref() - .ok_or_else(|| eyre::eyre!("array must have item type defined"))?; - let item_ty_name = param_type_inner( - &item._type, - item.format.as_deref(), - item.items.as_deref(), - owned, - )?; - if owned { - format!("Vec<{item_ty_name}>") - } else { - format!("&[{item_ty_name}]") - } - } - ParameterType::File => { - if owned { - format!("Vec<u8>") - } else { - format!("&[u8]") - } - } - }; - Ok(ty_name) -} - -fn method_docs(op: &Operation) -> eyre::Result<String> { - let mut out = String::new(); - let mut prev = false; - if let Some(summary) = &op.summary { - write!(&mut out, "/// {summary}\n")?; - prev = true; - } - if let Some(params) = &op.parameters { - if prev { - out.push_str("///\n"); - } - for param in params { - let param = match ¶m { - MaybeRef::Value { value } => value, - MaybeRef::Ref { _ref } => eyre::bail!("pipis"), - }; - match param._in { - ParameterIn::Path | ParameterIn::Body | ParameterIn::FormData => { - write!(&mut out, "/// - `{}`", param.name)?; - if let Some(description) = ¶m.description { - write!(&mut out, ": {}", description)?; - } - writeln!(&mut out)?; - } - _ => (), - } - } - } - Ok(out) -} - -fn deref_response<'a>( - spec: &'a OpenApiV2, - r: &'a MaybeRef<Response>, -) -> eyre::Result<(Option<&'a str>, &'a Response)> { - let r = match r { - MaybeRef::Value { value } => return Ok((None, value)), - MaybeRef::Ref { _ref } => _ref, - }; - let name = r - .strip_prefix("#/responses/") - .ok_or_else(|| eyre::eyre!("invalid response reference"))?; - let global_responses = spec - .responses - .as_ref() - .ok_or_else(|| eyre::eyre!("no global responses"))?; - let response = global_responses - .get(name) - .ok_or_else(|| eyre::eyre!("referenced response does not exist"))?; - Ok((Some(name), response)) -} - fn deref_definition<'a>( spec: &'a OpenApiV2, r: &'a MaybeRef<Schema>, @@ -446,200 +125,6 @@ fn deref_definition<'a>( Ok((Some(name), definition)) } -fn create_method_body( - spec: &OpenApiV2, - method: &str, - path: &str, - op: &Operation, -) -> eyre::Result<String> { - let request = create_method_request(spec, method, path, op)?; - let response = create_method_response(spec, method, path, op)?; - Ok(format!("{request}\n {response}")) -} - -fn create_method_request( - spec: &OpenApiV2, - method: &str, - path: &str, - op: &Operation, -) -> eyre::Result<String> { - let mut has_query = false; - let mut has_headers = false; - let mut body_method = String::new(); - if let Some(params) = &op.parameters { - for param in params { - let param = match ¶m { - MaybeRef::Value { value } => value, - MaybeRef::Ref { _ref } => eyre::bail!("todo: add deref parameters"), - }; - let name = sanitize_ident(¶m.name); - match param._in { - ParameterIn::Path => (/* do nothing */), - ParameterIn::Query => has_query = true, - ParameterIn::Header => has_headers = true, - ParameterIn::Body => { - if !body_method.is_empty() { - eyre::bail!("cannot have more than one body parameter"); - } - if param_is_string(spec, param)? { - body_method = format!(".body({name})"); - } else { - body_method = format!(".json(&{name})"); - } - } - ParameterIn::FormData => { - if !body_method.is_empty() { - eyre::bail!("cannot have more than one body parameter"); - } - body_method = format!(".multipart(reqwest::multipart::Form::new().part(\"attachment\", reqwest::multipart::Part::bytes({name}).file_name(\"file\").mime_str(\"*/*\").unwrap()))"); - } - } - } - } - let mut fmt_str = sanitize_path_arg(path)?; - let mut fmt_args = String::new(); - if has_query { - fmt_str.push_str("?{}"); - fmt_args.push_str(", query.to_string()"); - } - let path_arg = if fmt_str.contains("{") { - format!("&format!(\"{fmt_str}\"{fmt_args})") - } else { - format!("\"{fmt_str}\"") - }; - - let out = format!("let request = self.{method}({path_arg}){body_method}.build()?;"); - Ok(out) -} - -fn sanitize_path_arg(mut path: &str) -> eyre::Result<String> { - let mut out = String::new(); - loop { - let (head, tail) = match path.split_once("{") { - Some(i) => i, - None => { - out.push_str(path); - break; - } - }; - path = tail; - out.push_str(head); - out.push('{'); - let (head, tail) = match path.split_once("}") { - Some(i) => i, - None => { - eyre::bail!("unmatched bracket"); - } - }; - path = tail; - out.push_str(&head.to_snake_case()); - out.push('}'); - } - if out.starts_with("/") { - out.remove(0); - } - Ok(out) -} - -fn create_method_response( - spec: &OpenApiV2, - method: &str, - path: &str, - op: &Operation, -) -> eyre::Result<String> { - let mut has_empty = false; - let mut only_empty = true; - for (code, res) in &op.responses.http_codes { - let response = response_ref_type_name(spec, res)?; - if !code.starts_with("2") { - continue; - } - if matches!(response.body.as_deref(), Some("()") | None) { - has_empty = true; - } else { - only_empty = false; - } - } - let fn_ret = fn_return_from_op(spec, op)?; - let optional = has_empty && !only_empty; - let mut out = String::new(); - out.push_str("let response = self.execute(request).await?;\n"); - out.push_str("match response.status().as_u16() {\n"); - for (code, res) in &op.responses.http_codes { - let (_, res) = deref_response(spec, res)?; - if !code.starts_with("2") { - continue; - } - out.push_str(code); - out.push_str(" => Ok("); - let mut handlers = Vec::new(); - let header_handler = match &res.headers { - Some(_) => { - if fn_ret - .headers - .as_ref() - .map(|s| s.starts_with("Option<")) - .unwrap() - { - Some("Some(response.headers().clone())") - } else { - Some("response.headers().clone()") - } - } - None => { - if fn_ret.headers.is_some() { - Some("None") - } else { - None - } - } - }; - handlers.extend(header_handler); - let body_handler = match &res.schema { - Some(schema) if schema_is_string(spec, schema)? => { - if optional { - Some("Some(response.text().await?)") - } else { - Some("response.text().await?") - } - } - Some(_) => { - if optional { - Some("Some(response.json().await?)") - } else { - Some("response.json().await?") - } - } - None => { - if optional { - Some("None") - } else { - None - } - } - }; - handlers.extend(body_handler); - match handlers[..] { - [single] => out.push_str(single), - _ => { - out.push('('); - for (i, item) in handlers.iter().copied().enumerate() { - out.push_str(item); - if i + 1 < handlers.len() { - out.push_str(", "); - } - } - out.push(')'); - } - } - out.push_str("),\n"); - } - out.push_str("_ => Err(ForgejoError::UnexpectedStatusCode(response.status()))\n"); - out.push_str("}\n"); - - Ok(out) -} - fn schema_is_string(spec: &OpenApiV2, schema: &MaybeRef<Schema>) -> eyre::Result<bool> { let (_, schema) = deref_definition(spec, schema)?; let is_str = match schema._type { @@ -649,74 +134,6 @@ fn schema_is_string(spec: &OpenApiV2, schema: &MaybeRef<Schema>) -> eyre::Result Ok(is_str) } -fn param_is_string(spec: &OpenApiV2, param: &Parameter) -> eyre::Result<bool> { - match param._in { - ParameterIn::Body => { - let schema_ref = param - .schema - .as_ref() - .ok_or_else(|| eyre::eyre!("body param did not have schema"))?; - schema_is_string(spec, schema_ref) - } - _ => { - let is_str = match param._type { - Some(ParameterType::String) => true, - _ => false, - }; - Ok(is_str) - } - } -} - -fn create_get_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> { - let doc = method_docs(op)?; - let sig = fn_signature_from_op(spec, op)?; - let body = create_method_body(spec, "get", path, op)?; - Ok(format!("{doc}{sig} {{\n {body}\n}}\n\n")) -} - -fn create_put_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> { - let doc = method_docs(op)?; - let sig = fn_signature_from_op(spec, op)?; - let body = create_method_body(spec, "put", path, op)?; - Ok(format!("{doc}{sig} {{\n {body}\n}}\n\n")) -} - -fn create_post_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> { - let doc = method_docs(op)?; - let sig = fn_signature_from_op(spec, op)?; - let body = create_method_body(spec, "post", path, op)?; - Ok(format!("{doc}{sig} {{\n {body}\n}}\n\n")) -} - -fn create_delete_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> { - let doc = method_docs(op)?; - let sig = fn_signature_from_op(spec, op)?; - let body = create_method_body(spec, "delete", path, op)?; - Ok(format!("{doc}{sig} {{\n {body}\n}}\n\n")) -} - -fn create_options_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> { - let doc = method_docs(op)?; - let sig = fn_signature_from_op(spec, op)?; - let body = create_method_body(spec, "options", path, op)?; - Ok(format!("{doc}{sig} {{\n {body}\n}}\n\n")) -} - -fn create_head_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> { - let doc = method_docs(op)?; - let sig = fn_signature_from_op(spec, op)?; - let body = create_method_body(spec, "head", path, op)?; - Ok(format!("{doc}{sig} {{\n {body}\n}}\n\n")) -} - -fn create_patch_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> { - let doc = method_docs(op)?; - let sig = fn_signature_from_op(spec, op)?; - let body = create_method_body(spec, "patch", path, op)?; - Ok(format!("{doc}{sig} {{\n {body}\n}}\n\n")) -} - fn sanitize_ident(s: &str) -> String { let mut s = s.to_snake_case(); let keywords = [ @@ -782,312 +199,3 @@ fn sanitize_ident(s: &str) -> String { } s } - -fn create_struct_for_definition( - spec: &OpenApiV2, - name: &str, - schema: &Schema, -) -> eyre::Result<String> { - if matches!(schema._type, Some(SchemaType::One(Primitive::Array))) { - return Ok(String::new()); - } - - let docs = create_struct_docs(schema)?; - let mut fields = String::new(); - let required = schema.required.as_deref().unwrap_or_default(); - if let Some(properties) = &schema.properties { - for (prop_name, prop_schema) in properties { - let prop_ty = schema_ref_type_name(spec, prop_schema)?; - let field_name = sanitize_ident(prop_name); - let mut field_ty = prop_ty.clone(); - if field_name.ends_with("url") && field_name != "ssh_url" && field_ty == "String" { - field_ty = "url::Url".into() - } - if field_ty == name { - field_ty = format!("Box<{field_ty}>") - } - if !required.contains(prop_name) { - field_ty = format!("Option<{field_ty}>") - } - if field_ty == "Option<url::Url>" { - fields.push_str("#[serde(deserialize_with = \"crate::none_if_blank_url\")]\n"); - } - if field_ty == "time::OffsetDateTime" { - fields.push_str("#[serde(with = \"time::serde::rfc3339\")]\n"); - } - if field_ty == "Option<time::OffsetDateTime>" { - fields.push_str("#[serde(with = \"time::serde::rfc3339::option\")]\n"); - } - if &field_name != prop_name { - fields.push_str("#[serde(rename = \""); - fields.push_str(prop_name); - fields.push_str("\")]\n"); - } - fields.push_str("pub "); - fields.push_str(&field_name); - fields.push_str(": "); - fields.push_str(&field_ty); - fields.push_str(",\n"); - } - } - - if let Some(additonal_schema) = &schema.additional_properties { - let prop_ty = schema_ref_type_name(spec, additonal_schema)?; - fields.push_str("#[serde(flatten)]\n"); - fields.push_str("pub additional: std::collections::BTreeMap<String, "); - fields.push_str(&prop_ty); - fields.push_str(">,\n"); - } - - let out = format!("{docs}#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]\npub struct {name} {{\n{fields}}}\n\n"); - Ok(out) -} - -fn create_struct_docs(schema: &Schema) -> eyre::Result<String> { - let doc = match &schema.description { - Some(desc) => { - let mut out = String::new(); - for line in desc.lines() { - out.push_str("/// "); - out.push_str(line); - out.push_str("\n/// \n"); - } - out - } - None => String::new(), - }; - Ok(doc) -} - -fn create_query_structs_for_path( - spec: &OpenApiV2, - path: &str, - item: &openapi::PathItem, -) -> eyre::Result<String> { - let mut s = String::new(); - if let Some(op) = &item.get { - s.push_str(&create_query_struct(spec, path, op).wrap_err("GET")?); - } - if let Some(op) = &item.put { - s.push_str(&create_query_struct(spec, path, op).wrap_err("PUT")?); - } - if let Some(op) = &item.post { - s.push_str(&create_query_struct(spec, path, op).wrap_err("POST")?); - } - if let Some(op) = &item.delete { - s.push_str(&create_query_struct(spec, path, op).wrap_err("DELETE")?); - } - if let Some(op) = &item.options { - s.push_str(&create_query_struct(spec, path, op).wrap_err("OPTIONS")?); - } - if let Some(op) = &item.head { - s.push_str(&create_query_struct(spec, path, op).wrap_err("HEAD")?); - } - if let Some(op) = &item.patch { - s.push_str(&create_query_struct(spec, path, op).wrap_err("PATCH")?); - } - Ok(s) -} - -fn create_query_struct(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> { - let params = match &op.parameters { - Some(params) => params, - None => return Ok(String::new()), - }; - - let mut fields = String::new(); - let mut imp = String::new(); - for param in params { - let param = match ¶m { - MaybeRef::Value { value } => value, - MaybeRef::Ref { _ref } => eyre::bail!("todo: add deref parameters"), - }; - if param._in == ParameterIn::Query { - let ty = param_type(param, true)?; - let field_name = sanitize_ident(¶m.name); - let required = param.required.unwrap_or_default(); - fields.push_str("pub "); - fields.push_str(&field_name); - fields.push_str(": "); - if required { - fields.push_str(&ty); - } else { - fields.push_str("Option<"); - fields.push_str(&ty); - fields.push_str(">"); - } - fields.push_str(",\n"); - - let mut handler = String::new(); - let ty = param - ._type - .as_ref() - .ok_or_else(|| eyre::eyre!("no type provided for query field"))?; - if required { - writeln!(&mut handler, "let {field_name} = &self.{field_name};")?; - } else { - writeln!( - &mut handler, - "if let Some({field_name}) = &self.{field_name} {{" - )?; - } - match ty { - ParameterType::String => match param.format.as_deref() { - Some("date-time" | "date") => { - writeln!( - &mut handler, - "write!(f, \"{}={{field_name}}&\", field_name = {field_name}.format(&time::format_description::well_known::Rfc3339).unwrap())?;", - param.name)?; - } - _ => { - writeln!( - &mut handler, - "write!(f, \"{}={{{}}}&\")?;", - param.name, - field_name.strip_prefix("r#").unwrap_or(&field_name) - )?; - } - }, - ParameterType::Number | ParameterType::Integer | ParameterType::Boolean => { - writeln!( - &mut handler, - "write!(f, \"{}={{{}}}&\")?;", - param.name, - field_name.strip_prefix("r#").unwrap_or(&field_name) - )?; - } - ParameterType::Array => { - let format = param.collection_format.unwrap_or(CollectionFormat::Csv); - let item = param - .items - .as_ref() - .ok_or_else(|| eyre::eyre!("array must have item type defined"))?; - let item_pusher = match item._type { - ParameterType::String => { - match param.format.as_deref() { - Some("date-time" | "date") => { - "write!(f, \"{{date}}\", item.format(&time::format_description::well_known::Rfc3339).unwrap())?;" - }, - _ => { - "write!(f, \"{item}\")?;" - } - } - }, - ParameterType::Number | - ParameterType::Integer | - ParameterType::Boolean => { - "write!(f, \"{item}\")?;" - }, - ParameterType::Array => { - eyre::bail!("nested arrays not supported in query"); - }, - ParameterType::File => eyre::bail!("cannot send file in query"), - }; - match format { - CollectionFormat::Csv => { - handler.push_str(&simple_query_array( - param, - item_pusher, - &field_name, - ",", - )?); - } - CollectionFormat::Ssv => { - handler.push_str(&simple_query_array( - param, - item_pusher, - &field_name, - " ", - )?); - } - CollectionFormat::Tsv => { - handler.push_str(&simple_query_array( - param, - item_pusher, - &field_name, - "\\t", - )?); - } - CollectionFormat::Pipes => { - handler.push_str(&simple_query_array( - param, - item_pusher, - &field_name, - "|", - )?); - } - CollectionFormat::Multi => { - writeln!(&mut handler)?; - writeln!(&mut handler, "if !{field_name}.is_empty() {{")?; - writeln!(&mut handler, "for item in {field_name} {{")?; - writeln!(&mut handler, "write!(f, \"{}=\")?;", param.name)?; - handler.push_str(item_pusher); - handler.push('\n'); - writeln!(&mut handler, "write!(f, \"&\")?;")?; - writeln!(&mut handler, "}}")?; - writeln!(&mut handler, "}}")?; - } - } - } - ParameterType::File => eyre::bail!("cannot send file in query"), - } - if !required { - writeln!(&mut handler, "}}")?; - } - imp.push_str(&handler); - } - } - - let result = if fields.is_empty() { - String::new() - } else { - let op_name = op - .operation_id - .as_ref() - .ok_or_else(|| eyre::eyre!("no op id found"))? - .to_pascal_case(); - format!( - " -pub struct {op_name}Query {{ - {fields} -}} - -impl std::fmt::Display for {op_name}Query {{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{ - {imp} - Ok(()) - }} -}} -" - ) - }; - - Ok(result) -} - -fn simple_query_array( - param: &Parameter, - item_pusher: &str, - name: &str, - sep: &str, -) -> eyre::Result<String> { - let mut out = String::new(); - - writeln!( - &mut out, - " -if !{name}.is_empty() {{ - write!(f, \"{}=\")?; - for (item, i) in {name}.iter().enumerate() {{ - {item_pusher} - if i < {name}.len() - 1 {{ - write!(f, \"{sep}\")?; - }} - }} - write!(f, \"&\")?; -}}", - param.name - )?; - - Ok(out) -} diff --git a/generator/src/methods.rs b/generator/src/methods.rs new file mode 100644 index 0000000..aeebf30 --- /dev/null +++ b/generator/src/methods.rs @@ -0,0 +1,562 @@ +use crate::openapi::*; +use eyre::WrapErr; +use heck::ToSnakeCase; +use std::fmt::Write; + +pub fn create_methods(spec: &OpenApiV2) -> eyre::Result<String> { + let mut s = String::new(); + s.push_str("use crate::ForgejoError;\n"); + s.push_str("impl crate::Forgejo {\n"); + for (path, item) in &spec.paths { + s.push_str(&create_methods_for_path(&spec, path, item).wrap_err_with(|| path.clone())?); + } + s.push_str("}\n"); + Ok(s) +} + +fn create_methods_for_path(spec: &OpenApiV2, path: &str, item: &PathItem) -> eyre::Result<String> { + let mut s = String::new(); + if let Some(op) = &item.get { + s.push_str(&create_get_method(spec, path, op).wrap_err("GET")?); + } + if let Some(op) = &item.put { + s.push_str(&create_put_method(spec, path, op).wrap_err("PUT")?); + } + if let Some(op) = &item.post { + s.push_str(&create_post_method(spec, path, op).wrap_err("POST")?); + } + if let Some(op) = &item.delete { + s.push_str(&create_delete_method(spec, path, op).wrap_err("DELETE")?); + } + if let Some(op) = &item.options { + s.push_str(&create_options_method(spec, path, op).wrap_err("OPTIONS")?); + } + if let Some(op) = &item.head { + s.push_str(&create_head_method(spec, path, op).wrap_err("HEAD")?); + } + if let Some(op) = &item.patch { + s.push_str(&create_patch_method(spec, path, op).wrap_err("PATCH")?); + } + Ok(s) +} + +fn create_get_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> { + let doc = method_docs(op)?; + let sig = fn_signature_from_op(spec, op)?; + let body = create_method_body(spec, "get", path, op)?; + Ok(format!("{doc}{sig} {{\n {body}\n}}\n\n")) +} + +fn create_put_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> { + let doc = method_docs(op)?; + let sig = fn_signature_from_op(spec, op)?; + let body = create_method_body(spec, "put", path, op)?; + Ok(format!("{doc}{sig} {{\n {body}\n}}\n\n")) +} + +fn create_post_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> { + let doc = method_docs(op)?; + let sig = fn_signature_from_op(spec, op)?; + let body = create_method_body(spec, "post", path, op)?; + Ok(format!("{doc}{sig} {{\n {body}\n}}\n\n")) +} + +fn create_delete_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> { + let doc = method_docs(op)?; + let sig = fn_signature_from_op(spec, op)?; + let body = create_method_body(spec, "delete", path, op)?; + Ok(format!("{doc}{sig} {{\n {body}\n}}\n\n")) +} + +fn create_options_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> { + let doc = method_docs(op)?; + let sig = fn_signature_from_op(spec, op)?; + let body = create_method_body(spec, "options", path, op)?; + Ok(format!("{doc}{sig} {{\n {body}\n}}\n\n")) +} + +fn create_head_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> { + let doc = method_docs(op)?; + let sig = fn_signature_from_op(spec, op)?; + let body = create_method_body(spec, "head", path, op)?; + Ok(format!("{doc}{sig} {{\n {body}\n}}\n\n")) +} + +fn create_patch_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> { + let doc = method_docs(op)?; + let sig = fn_signature_from_op(spec, op)?; + let body = create_method_body(spec, "patch", path, op)?; + Ok(format!("{doc}{sig} {{\n {body}\n}}\n\n")) +} + +fn method_docs(op: &Operation) -> eyre::Result<String> { + let mut out = String::new(); + let mut prev = false; + if let Some(summary) = &op.summary { + write!(&mut out, "/// {summary}\n")?; + prev = true; + } + if let Some(params) = &op.parameters { + if prev { + out.push_str("///\n"); + } + for param in params { + let param = match ¶m { + MaybeRef::Value { value } => value, + MaybeRef::Ref { _ref } => eyre::bail!("pipis"), + }; + match param._in { + ParameterIn::Path | ParameterIn::Body | ParameterIn::FormData => { + write!(&mut out, "/// - `{}`", param.name)?; + if let Some(description) = ¶m.description { + write!(&mut out, ": {}", description)?; + } + writeln!(&mut out)?; + } + _ => (), + } + } + } + Ok(out) +} + +fn fn_signature_from_op(spec: &OpenApiV2, op: &Operation) -> eyre::Result<String> { + let name = op + .operation_id + .as_deref() + .ok_or_else(|| eyre::eyre!("operation did not have id"))? + .to_snake_case() + .replace("o_auth2", "oauth2"); + let args = fn_args_from_op(spec, op)?; + let ty = fn_return_from_op(spec, op)?; + Ok(format!( + "pub async fn {name}({args}) -> Result<{ty}, ForgejoError>" + )) +} + +fn fn_args_from_op(spec: &OpenApiV2, op: &Operation) -> eyre::Result<String> { + let mut args = "&self".to_string(); + let mut has_query = false; + let mut has_headers = false; + let mut has_form = false; + if let Some(params) = &op.parameters { + for param in params { + let param = match ¶m { + MaybeRef::Value { value } => value, + MaybeRef::Ref { _ref } => eyre::bail!("todo: add deref parameters"), + }; + match param._in { + ParameterIn::Path => { + let type_name = param_type(¶m, false)?; + args.push_str(", "); + args.push_str(&crate::sanitize_ident(¶m.name)); + args.push_str(": "); + args.push_str(&type_name); + } + ParameterIn::Query => has_query = true, + ParameterIn::Header => has_headers = true, + ParameterIn::Body => { + let schema_ref = param.schema.as_ref().unwrap(); + let ty = crate::schema_ref_type_name(spec, &schema_ref)?; + args.push_str(", "); + args.push_str(&crate::sanitize_ident(¶m.name)); + args.push_str(": "); + args.push_str(&ty); + } + ParameterIn::FormData => { + args.push_str(", "); + args.push_str(&crate::sanitize_ident(¶m.name)); + args.push_str(": Vec<u8>"); + } + } + } + } + if has_query { + let query_ty = crate::structs::query_struct_name(op)?; + args.push_str(", query: "); + args.push_str(&query_ty); + } + Ok(args) +} + +pub fn param_type(param: &Parameter, owned: bool) -> eyre::Result<String> { + let _type = param + ._type + .as_ref() + .ok_or_else(|| eyre::eyre!("no type provided for path param"))?; + param_type_inner(_type, param.format.as_deref(), param.items.as_ref(), owned) +} + +fn param_type_inner( + ty: &ParameterType, + format: Option<&str>, + items: Option<&Items>, + owned: bool, +) -> eyre::Result<String> { + let ty_name = match ty { + ParameterType::String => match format.as_deref() { + Some("date") => "time::Date", + Some("date-time") => "time::OffsetDateTime", + _ => { + if owned { + "String" + } else { + "&str" + } + } + } + .into(), + ParameterType::Number => match format.as_deref() { + Some("float") => "f32", + Some("double") => "f64", + _ => "f64", + } + .into(), + ParameterType::Integer => match format.as_deref() { + Some("int32") => "u32", + Some("int64") => "u64", + _ => "u32", + } + .into(), + ParameterType::Boolean => "bool".into(), + ParameterType::Array => { + let item = items + .as_ref() + .ok_or_else(|| eyre::eyre!("array must have item type defined"))?; + let item_ty_name = param_type_inner( + &item._type, + item.format.as_deref(), + item.items.as_deref(), + owned, + )?; + if owned { + format!("Vec<{item_ty_name}>") + } else { + format!("&[{item_ty_name}]") + } + } + ParameterType::File => { + if owned { + format!("Vec<u8>") + } else { + format!("&[u8]") + } + } + }; + Ok(ty_name) +} + +fn fn_return_from_op(spec: &OpenApiV2, op: &Operation) -> eyre::Result<ResponseType> { + let responses = op + .responses + .http_codes + .iter() + .filter(|(k, _)| k.starts_with("2")) + .map(|(_, v)| response_ref_type_name(spec, v)) + .collect::<Result<Vec<_>, _>>()?; + let mut iter = responses.into_iter(); + let mut response = iter + .next() + .ok_or_else(|| eyre::eyre!("must have at least one response type"))?; + for next in iter { + response = response.merge(next)?; + } + + Ok(response) +} + +fn response_ref_type_name( + spec: &OpenApiV2, + schema: &MaybeRef<Response>, +) -> eyre::Result<ResponseType> { + let (_, response) = deref_response(spec, schema)?; + let mut ty = ResponseType::default(); + if response.headers.is_some() { + ty.headers = Some("reqwest::header::HeaderMap".into()); + } + if let Some(schema) = &response.schema { + ty.body = Some(crate::schema_ref_type_name(spec, schema)?); + }; + Ok(ty) +} + +fn deref_response<'a>( + spec: &'a OpenApiV2, + r: &'a MaybeRef<Response>, +) -> eyre::Result<(Option<&'a str>, &'a Response)> { + let r = match r { + MaybeRef::Value { value } => return Ok((None, value)), + MaybeRef::Ref { _ref } => _ref, + }; + let name = r + .strip_prefix("#/responses/") + .ok_or_else(|| eyre::eyre!("invalid response reference"))?; + let global_responses = spec + .responses + .as_ref() + .ok_or_else(|| eyre::eyre!("no global responses"))?; + let response = global_responses + .get(name) + .ok_or_else(|| eyre::eyre!("referenced response does not exist"))?; + Ok((Some(name), response)) +} + +fn create_method_body( + spec: &OpenApiV2, + method: &str, + path: &str, + op: &Operation, +) -> eyre::Result<String> { + let request = create_method_request(spec, method, path, op)?; + let response = create_method_response(spec, method, path, op)?; + Ok(format!("{request}\n {response}")) +} + +fn create_method_request( + spec: &OpenApiV2, + method: &str, + path: &str, + op: &Operation, +) -> eyre::Result<String> { + let mut has_query = false; + let mut has_headers = false; + let mut body_method = String::new(); + if let Some(params) = &op.parameters { + for param in params { + let param = match ¶m { + MaybeRef::Value { value } => value, + MaybeRef::Ref { _ref } => eyre::bail!("todo: add deref parameters"), + }; + let name = crate::sanitize_ident(¶m.name); + match param._in { + ParameterIn::Path => (/* do nothing */), + ParameterIn::Query => has_query = true, + ParameterIn::Header => has_headers = true, + ParameterIn::Body => { + if !body_method.is_empty() { + eyre::bail!("cannot have more than one body parameter"); + } + if param_is_string(spec, param)? { + body_method = format!(".body({name})"); + } else { + body_method = format!(".json(&{name})"); + } + } + ParameterIn::FormData => { + if !body_method.is_empty() { + eyre::bail!("cannot have more than one body parameter"); + } + body_method = format!(".multipart(reqwest::multipart::Form::new().part(\"attachment\", reqwest::multipart::Part::bytes({name}).file_name(\"file\").mime_str(\"*/*\").unwrap()))"); + } + } + } + } + let mut fmt_str = sanitize_path_arg(path)?; + let mut fmt_args = String::new(); + if has_query { + fmt_str.push_str("?{}"); + fmt_args.push_str(", query.to_string()"); + } + let path_arg = if fmt_str.contains("{") { + format!("&format!(\"{fmt_str}\"{fmt_args})") + } else { + format!("\"{fmt_str}\"") + }; + + let out = format!("let request = self.{method}({path_arg}){body_method}.build()?;"); + Ok(out) +} + +fn param_is_string(spec: &OpenApiV2, param: &Parameter) -> eyre::Result<bool> { + match param._in { + ParameterIn::Body => { + let schema_ref = param + .schema + .as_ref() + .ok_or_else(|| eyre::eyre!("body param did not have schema"))?; + crate::schema_is_string(spec, schema_ref) + } + _ => { + let is_str = match param._type { + Some(ParameterType::String) => true, + _ => false, + }; + Ok(is_str) + } + } +} + +fn sanitize_path_arg(mut path: &str) -> eyre::Result<String> { + let mut out = String::new(); + loop { + let (head, tail) = match path.split_once("{") { + Some(i) => i, + None => { + out.push_str(path); + break; + } + }; + path = tail; + out.push_str(head); + out.push('{'); + let (head, tail) = match path.split_once("}") { + Some(i) => i, + None => { + eyre::bail!("unmatched bracket"); + } + }; + path = tail; + out.push_str(&head.to_snake_case()); + out.push('}'); + } + if out.starts_with("/") { + out.remove(0); + } + Ok(out) +} + +fn create_method_response( + spec: &OpenApiV2, + method: &str, + path: &str, + op: &Operation, +) -> eyre::Result<String> { + let mut has_empty = false; + let mut only_empty = true; + for (code, res) in &op.responses.http_codes { + let response = response_ref_type_name(spec, res)?; + if !code.starts_with("2") { + continue; + } + if matches!(response.body.as_deref(), Some("()") | None) { + has_empty = true; + } else { + only_empty = false; + } + } + let fn_ret = fn_return_from_op(spec, op)?; + let optional = has_empty && !only_empty; + let mut out = String::new(); + out.push_str("let response = self.execute(request).await?;\n"); + out.push_str("match response.status().as_u16() {\n"); + for (code, res) in &op.responses.http_codes { + let (_, res) = deref_response(spec, res)?; + if !code.starts_with("2") { + continue; + } + out.push_str(code); + out.push_str(" => Ok("); + let mut handlers = Vec::new(); + let header_handler = match &res.headers { + Some(_) => { + if fn_ret + .headers + .as_ref() + .map(|s| s.starts_with("Option<")) + .unwrap() + { + Some("Some(response.headers().clone())") + } else { + Some("response.headers().clone()") + } + } + None => { + if fn_ret.headers.is_some() { + Some("None") + } else { + None + } + } + }; + handlers.extend(header_handler); + let body_handler = match &res.schema { + Some(schema) if crate::schema_is_string(spec, schema)? => { + if optional { + Some("Some(response.text().await?)") + } else { + Some("response.text().await?") + } + } + Some(_) => { + if optional { + Some("Some(response.json().await?)") + } else { + Some("response.json().await?") + } + } + None => { + if optional { + Some("None") + } else { + None + } + } + }; + handlers.extend(body_handler); + match handlers[..] { + [single] => out.push_str(single), + _ => { + out.push('('); + for (i, item) in handlers.iter().copied().enumerate() { + out.push_str(item); + if i + 1 < handlers.len() { + out.push_str(", "); + } + } + out.push(')'); + } + } + out.push_str("),\n"); + } + out.push_str("_ => Err(ForgejoError::UnexpectedStatusCode(response.status()))\n"); + out.push_str("}\n"); + + Ok(out) +} + +#[derive(Debug, Default)] +struct ResponseType { + headers: Option<String>, + body: Option<String>, +} + +impl ResponseType { + fn merge(self, other: Self) -> eyre::Result<Self> { + let mut new = Self::default(); + match (self.headers, other.headers) { + (Some(a), Some(b)) if a != b => eyre::bail!("incompatible header types in response"), + (Some(a), None) => new.headers = Some(format!("Option<{a}>")), + (None, Some(b)) => new.headers = Some(format!("Option<{b}>")), + (a, b) => new.headers = a.or(b), + }; + match (self.body.as_deref(), other.body.as_deref()) { + (Some(a), Some(b)) if a != b => eyre::bail!("incompatible header types in response"), + (Some(a), Some("()") | None) => new.body = Some(format!("Option<{a}>")), + (Some("()") | None, Some(b)) => new.body = Some(format!("Option<{b}>")), + (a, b) => new.body = self.body.or(other.body), + }; + Ok(new) + } +} + +impl std::fmt::Display for ResponseType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut tys = Vec::new(); + tys.extend(self.headers.as_deref()); + tys.extend(self.body.as_deref()); + match tys[..] { + [single] => f.write_str(single), + _ => { + write!(f, "(")?; + for (i, item) in tys.iter().copied().enumerate() { + f.write_str(item)?; + if i + 1 < tys.len() { + write!(f, ", ")?; + } + } + write!(f, ")")?; + Ok(()) + } + } + } +} diff --git a/generator/src/structs.rs b/generator/src/structs.rs new file mode 100644 index 0000000..55483b4 --- /dev/null +++ b/generator/src/structs.rs @@ -0,0 +1,338 @@ +use crate::openapi::*; +use eyre::WrapErr; +use heck::ToPascalCase; +use std::fmt::Write; + +pub fn create_structs(spec: &OpenApiV2) -> eyre::Result<String> { + let mut s = String::new(); + s.push_str("use structs::*;\n"); + s.push_str("pub mod structs {\n"); + if let Some(definitions) = &spec.definitions { + for (name, schema) in definitions { + let strukt = create_struct_for_definition(&spec, name, schema)?; + s.push_str(&strukt); + } + } + for (path, item) in &spec.paths { + let strukt = create_query_structs_for_path(&spec, path, item)?; + s.push_str(&strukt); + } + s.push_str("\n}"); + Ok(s) +} + +pub fn create_struct_for_definition( + spec: &OpenApiV2, + name: &str, + schema: &Schema, +) -> eyre::Result<String> { + if matches!(schema._type, Some(SchemaType::One(Primitive::Array))) { + return Ok(String::new()); + } + + let docs = create_struct_docs(schema)?; + let mut fields = String::new(); + let required = schema.required.as_deref().unwrap_or_default(); + if let Some(properties) = &schema.properties { + for (prop_name, prop_schema) in properties { + let prop_ty = crate::schema_ref_type_name(spec, prop_schema)?; + let field_name = crate::sanitize_ident(prop_name); + let mut field_ty = prop_ty.clone(); + if field_name.ends_with("url") && field_name != "ssh_url" && field_ty == "String" { + field_ty = "url::Url".into() + } + if field_ty == name { + field_ty = format!("Box<{field_ty}>") + } + if !required.contains(prop_name) { + field_ty = format!("Option<{field_ty}>") + } + if field_ty == "Option<url::Url>" { + fields.push_str("#[serde(deserialize_with = \"crate::none_if_blank_url\")]\n"); + } + if field_ty == "time::OffsetDateTime" { + fields.push_str("#[serde(with = \"time::serde::rfc3339\")]\n"); + } + if field_ty == "Option<time::OffsetDateTime>" { + fields.push_str("#[serde(with = \"time::serde::rfc3339::option\")]\n"); + } + if &field_name != prop_name { + fields.push_str("#[serde(rename = \""); + fields.push_str(prop_name); + fields.push_str("\")]\n"); + } + fields.push_str("pub "); + fields.push_str(&field_name); + fields.push_str(": "); + fields.push_str(&field_ty); + fields.push_str(",\n"); + } + } + + if let Some(additonal_schema) = &schema.additional_properties { + let prop_ty = crate::schema_ref_type_name(spec, additonal_schema)?; + fields.push_str("#[serde(flatten)]\n"); + fields.push_str("pub additional: std::collections::BTreeMap<String, "); + fields.push_str(&prop_ty); + fields.push_str(">,\n"); + } + + let out = format!("{docs}#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]\npub struct {name} {{\n{fields}}}\n\n"); + Ok(out) +} + +fn create_struct_docs(schema: &Schema) -> eyre::Result<String> { + let doc = match &schema.description { + Some(desc) => { + let mut out = String::new(); + for line in desc.lines() { + out.push_str("/// "); + out.push_str(line); + out.push_str("\n/// \n"); + } + out + } + None => String::new(), + }; + Ok(doc) +} + +pub fn create_query_structs_for_path( + spec: &OpenApiV2, + path: &str, + item: &PathItem, +) -> eyre::Result<String> { + let mut s = String::new(); + if let Some(op) = &item.get { + s.push_str(&create_query_struct(spec, path, op).wrap_err("GET")?); + } + if let Some(op) = &item.put { + s.push_str(&create_query_struct(spec, path, op).wrap_err("PUT")?); + } + if let Some(op) = &item.post { + s.push_str(&create_query_struct(spec, path, op).wrap_err("POST")?); + } + if let Some(op) = &item.delete { + s.push_str(&create_query_struct(spec, path, op).wrap_err("DELETE")?); + } + if let Some(op) = &item.options { + s.push_str(&create_query_struct(spec, path, op).wrap_err("OPTIONS")?); + } + if let Some(op) = &item.head { + s.push_str(&create_query_struct(spec, path, op).wrap_err("HEAD")?); + } + if let Some(op) = &item.patch { + s.push_str(&create_query_struct(spec, path, op).wrap_err("PATCH")?); + } + Ok(s) +} + +pub fn query_struct_name(op: &Operation) -> eyre::Result<String> { + let mut ty = op + .operation_id + .as_deref() + .ok_or_else(|| eyre::eyre!("operation did not have id"))? + .to_pascal_case() + .replace("o_auth2", "oauth2"); + ty.push_str("Query"); + Ok(ty) +} + +fn create_query_struct(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> { + let params = match &op.parameters { + Some(params) => params, + None => return Ok(String::new()), + }; + + let mut fields = String::new(); + let mut imp = String::new(); + for param in params { + let param = match ¶m { + MaybeRef::Value { value } => value, + MaybeRef::Ref { _ref } => eyre::bail!("todo: add deref parameters"), + }; + if param._in == ParameterIn::Query { + let ty = crate::methods::param_type(param, true)?; + let field_name = crate::sanitize_ident(¶m.name); + let required = param.required.unwrap_or_default(); + fields.push_str("pub "); + fields.push_str(&field_name); + fields.push_str(": "); + if required { + fields.push_str(&ty); + } else { + fields.push_str("Option<"); + fields.push_str(&ty); + fields.push_str(">"); + } + fields.push_str(",\n"); + + let mut handler = String::new(); + let ty = param + ._type + .as_ref() + .ok_or_else(|| eyre::eyre!("no type provided for query field"))?; + if required { + writeln!(&mut handler, "let {field_name} = &self.{field_name};")?; + } else { + writeln!( + &mut handler, + "if let Some({field_name}) = &self.{field_name} {{" + )?; + } + match ty { + ParameterType::String => match param.format.as_deref() { + Some("date-time" | "date") => { + writeln!( + &mut handler, + "write!(f, \"{}={{field_name}}&\", field_name = {field_name}.format(&time::format_description::well_known::Rfc3339).unwrap())?;", + param.name)?; + } + _ => { + writeln!( + &mut handler, + "write!(f, \"{}={{{}}}&\")?;", + param.name, + field_name.strip_prefix("r#").unwrap_or(&field_name) + )?; + } + }, + ParameterType::Number | ParameterType::Integer | ParameterType::Boolean => { + writeln!( + &mut handler, + "write!(f, \"{}={{{}}}&\")?;", + param.name, + field_name.strip_prefix("r#").unwrap_or(&field_name) + )?; + } + ParameterType::Array => { + let format = param.collection_format.unwrap_or(CollectionFormat::Csv); + let item = param + .items + .as_ref() + .ok_or_else(|| eyre::eyre!("array must have item type defined"))?; + let item_pusher = match item._type { + ParameterType::String => { + match param.format.as_deref() { + Some("date-time" | "date") => { + "write!(f, \"{{date}}\", item.format(&time::format_description::well_known::Rfc3339).unwrap())?;" + }, + _ => { + "write!(f, \"{item}\")?;" + } + } + }, + ParameterType::Number | + ParameterType::Integer | + ParameterType::Boolean => { + "write!(f, \"{item}\")?;" + }, + ParameterType::Array => { + eyre::bail!("nested arrays not supported in query"); + }, + ParameterType::File => eyre::bail!("cannot send file in query"), + }; + match format { + CollectionFormat::Csv => { + handler.push_str(&simple_query_array( + param, + item_pusher, + &field_name, + ",", + )?); + } + CollectionFormat::Ssv => { + handler.push_str(&simple_query_array( + param, + item_pusher, + &field_name, + " ", + )?); + } + CollectionFormat::Tsv => { + handler.push_str(&simple_query_array( + param, + item_pusher, + &field_name, + "\\t", + )?); + } + CollectionFormat::Pipes => { + handler.push_str(&simple_query_array( + param, + item_pusher, + &field_name, + "|", + )?); + } + CollectionFormat::Multi => { + writeln!(&mut handler)?; + writeln!(&mut handler, "if !{field_name}.is_empty() {{")?; + writeln!(&mut handler, "for item in {field_name} {{")?; + writeln!(&mut handler, "write!(f, \"{}=\")?;", param.name)?; + handler.push_str(item_pusher); + handler.push('\n'); + writeln!(&mut handler, "write!(f, \"&\")?;")?; + writeln!(&mut handler, "}}")?; + writeln!(&mut handler, "}}")?; + } + } + } + ParameterType::File => eyre::bail!("cannot send file in query"), + } + if !required { + writeln!(&mut handler, "}}")?; + } + imp.push_str(&handler); + } + } + + let result = if fields.is_empty() { + String::new() + } else { + let op_name = query_struct_name(op)?; + format!( + " +pub struct {op_name} {{ + {fields} +}} + +impl std::fmt::Display for {op_name} {{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{ + {imp} + Ok(()) + }} +}} +" + ) + }; + + Ok(result) +} + +fn simple_query_array( + param: &Parameter, + item_pusher: &str, + name: &str, + sep: &str, +) -> eyre::Result<String> { + let mut out = String::new(); + + writeln!( + &mut out, + " +if !{name}.is_empty() {{ + write!(f, \"{}=\")?; + for (item, i) in {name}.iter().enumerate() {{ + {item_pusher} + if i < {name}.len() - 1 {{ + write!(f, \"{sep}\")?; + }} + }} + write!(f, \"&\")?; +}}", + param.name + )?; + + Ok(out) +} |