use crate::{openapi::*, schema_ref_type_name}; use eyre::WrapErr; use heck::{ToPascalCase, ToSnakeCase}; use std::fmt::Write; pub fn create_methods(spec: &OpenApiV2) -> eyre::Result { let mut s = String::new(); s.push_str("use crate::ForgejoError;\n"); s.push_str("use std::collections::BTreeMap;"); s.push_str("use super::structs::*;\n"); s.push_str("\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 { 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 { let doc = method_docs(spec, 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 { let doc = method_docs(spec, 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 { let doc = method_docs(spec, 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 { let doc = method_docs(spec, 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 { let doc = method_docs(spec, 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 { let doc = method_docs(spec, 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 { let doc = method_docs(spec, 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(spec: &OpenApiV2, op: &Operation) -> eyre::Result { let mut out = String::new(); let mut prev = false; if let Some(summary) = &op.summary { let summary = summary .replace("gitea", "Forgejo") .replace("Gitea", "Forgejo"); 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 = param.deref(spec)?; match ¶m._in { ParameterIn::Path { param: _ } | ParameterIn::FormData { param: _ } => { write!(&mut out, "/// - `{}`", param.name)?; if let Some(description) = ¶m.description { write!(&mut out, ": {}", description)?; } writeln!(&mut out)?; } ParameterIn::Body { schema } => { write!(&mut out, "/// - `{}`", param.name)?; let ty = schema_ref_type_name(spec, &schema)?; if let Some(description) = ¶m.description { write!(&mut out, ": {}\n\n/// See [`{}`]", description, ty)?; } else { write!(&mut out, ": See [`{}`]", ty)?; } writeln!(&mut out)?; } _ => (), } } } if out.ends_with("/// \n") { out.truncate(out.len() - 5); } Ok(out) } fn fn_signature_from_op(spec: &OpenApiV2, op: &Operation) -> eyre::Result { 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 { let mut args = "&self".to_string(); let mut has_query = false; // let mut has_headers = false; if let Some(params) = &op.parameters { for param in params { let full_param = param.deref(spec)?; match &full_param._in { ParameterIn::Path { param } => { let type_name = param_type(¶m, false)?; args.push_str(", "); args.push_str(&crate::sanitize_ident(&full_param.name)); args.push_str(": "); args.push_str(&type_name); } ParameterIn::Query { param: _ } => has_query = true, ParameterIn::Header { param: _ } => (), // has_headers = true, ParameterIn::Body { schema } => { let ty = crate::schema_ref_type_name(spec, schema)?; args.push_str(", "); args.push_str(&crate::sanitize_ident(&full_param.name)); args.push_str(": "); args.push_str(&ty); } ParameterIn::FormData { param: _ } => { args.push_str(", "); args.push_str(&crate::sanitize_ident(&full_param.name)); args.push_str(": Vec"); } } } } 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: &NonBodyParameter, owned: bool) -> eyre::Result { param_type_inner( ¶m._type, param.format.as_deref(), param.items.as_ref(), owned, ) } pub fn param_type_inner( ty: &ParameterType, format: Option<&str>, items: Option<&Items>, owned: bool, ) -> eyre::Result { let ty_name = match ty { ParameterType::String => match format.as_deref() { Some("date") => "time::Date", Some("date-time") => "time::OffsetDateTime", Some("url" | "ssh-url") => "url::Url", _ => { 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("uint32") => "u32", Some("uint64") => "u64", Some("int32") => "i32", Some("int64") => "i64", _ => "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") } else { format!("&[u8]") } } }; Ok(ty_name) } fn fn_return_from_op(spec: &OpenApiV2, op: &Operation) -> eyre::Result { let responses = op .responses .http_codes .iter() .filter(|(k, _)| k.starts_with("2")) .map(|(_, v)| response_ref_type_name(spec, v, op)) .collect::, _>>()?; 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, response_ref: &MaybeRef, op: &Operation, ) -> eyre::Result { let response = response_ref.deref(spec)?; let mut ty = ResponseType::default(); if response.headers.is_some() { let parent_name = match &response_ref { MaybeRef::Ref { _ref } => _ref .rsplit_once("/") .ok_or_else(|| eyre::eyre!("invalid ref"))? .1 .to_string(), MaybeRef::Value { value: _ } => { eyre::bail!("could not find parent name for header type") } }; ty.headers = Some(format!("{}Headers", parent_name)); } let produces = op .produces .as_deref() .or_else(|| spec.produces.as_deref()) .unwrap_or_default(); // can't use .contains() because Strings let produces_json = produces.iter().any(|i| matches!(&**i, "application/json")); let produces_text = produces.iter().any(|i| i.starts_with("text/")); let produces_other = produces .iter() .any(|i| !matches!(&**i, "application/json") && !i.starts_with("text/")); match (produces_json, produces_text, produces_other) { (true, false, false) => { if let Some(schema) = &response.schema { ty.kind = Some(ResponseKind::Json); let mut body = crate::schema_ref_type_name(spec, schema)?; if let MaybeRef::Value { value } = schema { let op_name = op.operation_id.as_deref().ok_or_else(|| eyre::eyre!("no operation id"))?.to_pascal_case(); crate::schema_subtype_name(spec, &op_name, "Response", value, &mut body)?; } ty.body = Some(body); }; } (false, _, true) => { ty.kind = Some(ResponseKind::Bytes); ty.body = Some("Vec".into()); } (false, true, false) => { ty.kind = Some(ResponseKind::Text); ty.body = Some("String".into()); } (false, false, false) => { ty.kind = None; ty.body = None; } _ => eyre::bail!("produces value unsupported. json: {produces_json}, text: {produces_text}, other: {produces_other}"), }; Ok(ty) } fn create_method_body( spec: &OpenApiV2, method: &str, path: &str, op: &Operation, ) -> eyre::Result { let request = create_method_request(spec, method, path, op)?; let response = create_method_response(spec, op)?; Ok(format!("{request}\n {response}")) } fn create_method_request( spec: &OpenApiV2, method: &str, path: &str, op: &Operation, ) -> eyre::Result { 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 = param.deref(spec)?; let name = crate::sanitize_ident(¶m.name); match ¶m._in { ParameterIn::Path { param: _ } => (/* do nothing */), ParameterIn::Query { param: _ } => has_query = true, ParameterIn::Header { param: _ } => (), // _has_headers = true, ParameterIn::Body { schema: _ } => { 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 { param: _ } => { 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)?; if has_query { fmt_str.push_str("?{query}"); } let path_arg = if fmt_str.contains("{") { format!("&format!(\"{fmt_str}\")") } 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 { match ¶m._in { ParameterIn::Body { schema } => crate::schema_is_string(spec, schema), ParameterIn::Path { param } | ParameterIn::Query { param } | ParameterIn::Header { param } | ParameterIn::FormData { param } => { let is_str = match param._type { ParameterType::String => true, _ => false, }; Ok(is_str) } } } fn sanitize_path_arg(mut path: &str) -> eyre::Result { 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, op: &Operation) -> eyre::Result { 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, op)?; 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 branch_ret = response_ref_type_name(spec, res, op)?; let res = res.deref(spec)?; 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().try_into()?)") } else { Some("response.headers().try_into()?") } } None => { if fn_ret.headers.is_some() { Some("None") } else { None } } }; handlers.extend(header_handler); let body_handler = match branch_ret.kind { Some(ResponseKind::Text) => { if optional { Some("Some(response.text().await?)") } else { Some("response.text().await?") } } Some(ResponseKind::Json) => { if optional { Some("Some(response.json().await?)") } else { Some("response.json().await?") } } Some(ResponseKind::Bytes) => { if optional { Some("Some(response.bytes().await?[..].to_vec())") } else { Some("response.bytes().await?[..].to_vec()") } } 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, body: Option, kind: Option, } impl ResponseType { fn merge(self, other: Self) -> eyre::Result { let headers = match (self.headers, other.headers) { (Some(a), Some(b)) if a != b => eyre::bail!("incompatible header types in response"), (Some(a), None) if !a.starts_with("Option<") => Some(format!("Option<{a}>")), (None, Some(b)) if !b.starts_with("Option<") => Some(format!("Option<{b}>")), (a, b) => a.or(b), }; let body = 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) if !a.starts_with("Option<") => { Some(format!("Option<{a}>")) } (Some("()") | None, Some(b)) if !b.starts_with("Option<") => { Some(format!("Option<{b}>")) } (_, _) => self.body.or(other.body), }; use ResponseKind::*; let kind = match (self.kind, other.kind) { (a, None) => a, (None, b) => b, (Some(Json), Some(Json)) => Some(Json), (Some(Bytes), Some(Bytes)) => Some(Bytes), (Some(Bytes), Some(Text)) => Some(Bytes), (Some(Text), Some(Bytes)) => Some(Bytes), (Some(Text), Some(Text)) => Some(Text), _ => eyre::bail!("incompatible response kinds"), }; let new = Self { headers, body, kind, }; Ok(new) } } #[derive(Debug, Clone, Copy)] enum ResponseKind { Json, Text, Bytes, } 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(()) } } } }