diff options
author | Cyborus <cyborus@cyborus.xyz> | 2024-02-09 16:58:44 +0100 |
---|---|---|
committer | Cyborus <cyborus@cyborus.xyz> | 2024-02-09 20:37:43 +0100 |
commit | cb1f2d9ae86223c18af43885eb62800963496e34 (patch) | |
tree | 7ea352e52f56575b0a86a608739684a0330d3aba | |
parent | more general dereferencing (diff) | |
download | forgejo-api-cb1f2d9ae86223c18af43885eb62800963496e34.tar.xz forgejo-api-cb1f2d9ae86223c18af43885eb62800963496e34.zip |
strongly typed header returns
-rw-r--r-- | generator/src/methods.rs | 20 | ||||
-rw-r--r-- | generator/src/structs.rs | 125 | ||||
-rw-r--r-- | src/generated.rs | 248 | ||||
-rw-r--r-- | src/lib.rs | 17 | ||||
-rw-r--r-- | tests/ci_test.rs | 10 |
5 files changed, 408 insertions, 12 deletions
diff --git a/generator/src/methods.rs b/generator/src/methods.rs index 4a97e18..1cfd276 100644 --- a/generator/src/methods.rs +++ b/generator/src/methods.rs @@ -1,5 +1,5 @@ use crate::openapi::*; -use eyre::WrapErr; +use eyre::{OptionExt, WrapErr}; use heck::ToSnakeCase; use std::fmt::Write; @@ -185,7 +185,7 @@ pub fn param_type(param: &NonBodyParameter, owned: bool) -> eyre::Result<String> ) } -fn param_type_inner( +pub fn param_type_inner( ty: &ParameterType, format: Option<&str>, items: Option<&Items>, @@ -270,7 +270,17 @@ fn response_ref_type_name( let response = schema.deref(spec)?; let mut ty = ResponseType::default(); if response.headers.is_some() { - ty.headers = Some("reqwest::header::HeaderMap".into()); + let parent_name = match &schema { + 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)); } if let Some(schema) = &response.schema { ty.body = Some(crate::schema_ref_type_name(spec, schema)?); @@ -422,9 +432,9 @@ fn create_method_response(spec: &OpenApiV2, op: &Operation) -> eyre::Result<Stri .map(|s| s.starts_with("Option<")) .unwrap() { - Some("Some(response.headers().clone())") + Some("Some(response.headers().try_into()?)") } else { - Some("response.headers().clone()") + Some("response.headers().try_into()?") } } None => { diff --git a/generator/src/structs.rs b/generator/src/structs.rs index 556b913..1d07ab4 100644 --- a/generator/src/structs.rs +++ b/generator/src/structs.rs @@ -7,12 +7,21 @@ 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"); + s.push_str("use crate::StructureError;"); if let Some(definitions) = &spec.definitions { for (name, schema) in definitions { let strukt = create_struct_for_definition(&spec, name, schema)?; s.push_str(&strukt); } } + if let Some(responses) = &spec.responses { + for (name, response) in responses { + if let Some(headers) = &response.headers { + let strukt = create_header_struct(name, headers)?; + s.push_str(&strukt); + } + } + } for (_, item) in &spec.paths { let strukt = create_query_structs_for_path(item)?; s.push_str(&strukt); @@ -329,3 +338,119 @@ if !{name}.is_empty() {{ Ok(out) } + +fn create_header_struct( + name: &str, + headers: &std::collections::BTreeMap<String, Header>, +) -> eyre::Result<String> { + let ty_name = format!("{name}Headers").to_pascal_case(); + let mut fields = String::new(); + let mut imp = String::new(); + let mut imp_ret = String::new(); + for (header_name, header) in headers { + let ty = header_type(header)?; + let field_name = crate::sanitize_ident(header_name); + fields.push_str("pub "); + fields.push_str(&field_name); + fields.push_str(": Option<"); + fields.push_str(&ty); + fields.push_str(">,\n"); + + write!( + &mut imp, + " + let {field_name} = map.get(\"{header_name}\").map(|s| -> Result<_, _> {{ + let s = s.to_str().map_err(|_| StructureError::HeaderNotAscii)?; + " + ) + .unwrap(); + match &header._type { + ParameterType::String => imp.push_str("Ok(s.to_string())"), + ParameterType::Number => match header.format.as_deref() { + Some("float") => { + imp.push_str("s.parse::<f32>().map_err(|_| StructureError::HeaderParseFailed)") + } + Some("double") | _ => { + imp.push_str("s.parse::<f64>()).map_err(|_| StructureError::HeaderParseFailed)") + } + }, + ParameterType::Integer => match header.format.as_deref() { + Some("int64") => { + imp.push_str("s.parse::<u64>().map_err(|_| StructureError::HeaderParseFailed)") + } + Some("int32") | _ => { + imp.push_str("s.parse::<u32>().map_err(|_| StructureError::HeaderParseFailed)") + } + }, + ParameterType::Boolean => { + imp.push_str("s.parse::<bool>().map_err(|_| StructureError::HeaderParseFailed)") + } + ParameterType::Array => { + let sep = match header.collection_format { + Some(CollectionFormat::Csv) | None => ",", + Some(CollectionFormat::Ssv) => " ", + Some(CollectionFormat::Tsv) => "\\t", + Some(CollectionFormat::Pipes) => "|", + Some(CollectionFormat::Multi) => { + eyre::bail!("multi format not supported in headers") + } + }; + let items = header + .items + .as_ref() + .ok_or_else(|| eyre::eyre!("items property must be set for arrays"))?; + if items._type == ParameterType::String { + imp.push_str("Ok("); + } + imp.push_str("s.split(\""); + imp.push_str(sep); + imp.push_str("\").map(|s| "); + imp.push_str(match items._type { + ParameterType::String => "s.to_string()).collect::<Vec<_>>())", + ParameterType::Number => match items.format.as_deref() { + Some("float") => "s.parse::<f32>()).collect::<Result<Vec<_>, _>>().map_err(|_| StructureError::HeaderParseFailed)", + Some("double") | _ => "s.parse::<f64>()).collect::<Result<Vec<_>, _>>().map_err(|_| StructureError::HeaderParseFailed)", + }, + ParameterType::Integer => match items.format.as_deref() { + Some("int64") => "s.parse::<u64>()).collect::<Result<Vec<_>, _>>().map_err(|_| StructureError::HeaderParseFailed)", + Some("int32") | _ => "s.parse::<u32>()).collect::<Result<Vec<_>, _>>().map_err(|_| StructureError::HeaderParseFailed)", + }, + ParameterType::Boolean => "s.parse::<bool>()).collect::<Result<Vec<_>, _>>().map_err(|_| StructureError::HeaderParseFailed)", + ParameterType::Array => eyre::bail!("nested arrays not supported in headers"), + ParameterType::File => eyre::bail!("files not supported in headers"), + }); + } + ParameterType::File => eyre::bail!("files not supported in headers"), + } + imp.push_str("}).transpose()?;"); + + imp_ret.push_str(&field_name); + imp_ret.push_str(", "); + } + + Ok(format!( + " +pub struct {ty_name} {{ + {fields} +}} + +impl TryFrom<&reqwest::header::HeaderMap> for {ty_name} {{ + type Error = StructureError; + + fn try_from(map: &reqwest::header::HeaderMap) -> Result<Self, Self::Error> {{ + {imp} + Ok(Self {{ {imp_ret} }}) + }} +}} +" + )) +} + +pub fn header_type(header: &Header) -> eyre::Result<String> { + crate::methods::param_type_inner( + &header._type, + header.format.as_deref(), + header.items.as_ref(), + true, + ) +} diff --git a/src/generated.rs b/src/generated.rs index ac42f78..544b4d6 100644 --- a/src/generated.rs +++ b/src/generated.rs @@ -1818,13 +1818,13 @@ impl crate::Forgejo { owner: &str, repo: &str, query: RepoGetAllCommitsQuery, - ) -> Result<(reqwest::header::HeaderMap, Vec<Commit>), ForgejoError> { + ) -> Result<(CommitListHeaders, Vec<Commit>), ForgejoError> { let request = self .get(&format!("repos/{owner}/{repo}/commits?{query}")) .build()?; let response = self.execute(request).await?; match response.status().as_u16() { - 200 => Ok((response.headers().clone(), response.json().await?)), + 200 => Ok((response.headers().try_into()?, response.json().await?)), _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), } } @@ -4471,7 +4471,7 @@ impl crate::Forgejo { repo: &str, index: u64, query: RepoGetPullRequestCommitsQuery, - ) -> Result<(reqwest::header::HeaderMap, Vec<Commit>), ForgejoError> { + ) -> Result<(CommitListHeaders, Vec<Commit>), ForgejoError> { let request = self .get(&format!( "repos/{owner}/{repo}/pulls/{index}/commits?{query}" @@ -4479,7 +4479,7 @@ impl crate::Forgejo { .build()?; let response = self.execute(request).await?; match response.status().as_u16() { - 200 => Ok((response.headers().clone(), response.json().await?)), + 200 => Ok((response.headers().try_into()?, response.json().await?)), _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), } } @@ -4495,13 +4495,13 @@ impl crate::Forgejo { repo: &str, index: u64, query: RepoGetPullRequestFilesQuery, - ) -> Result<(reqwest::header::HeaderMap, Vec<ChangedFile>), ForgejoError> { + ) -> Result<(ChangedFileListHeaders, Vec<ChangedFile>), ForgejoError> { let request = self .get(&format!("repos/{owner}/{repo}/pulls/{index}/files?{query}")) .build()?; let response = self.execute(request).await?; match response.status().as_u16() { - 200 => Ok((response.headers().clone(), response.json().await?)), + 200 => Ok((response.headers().try_into()?, response.json().await?)), _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), } } @@ -7217,6 +7217,7 @@ impl crate::Forgejo { } use structs::*; pub mod structs { + use crate::StructureError; /// APIError is an api error with a message /// #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] @@ -9623,6 +9624,241 @@ pub mod structs { pub title: Option<String>, } + pub struct ChangedFileListHeaders { + pub x_has_more: Option<bool>, + pub x_page: Option<u64>, + pub x_page_count: Option<u64>, + pub x_per_page: Option<u64>, + pub x_total: Option<u64>, + } + + impl TryFrom<&reqwest::header::HeaderMap> for ChangedFileListHeaders { + type Error = StructureError; + + fn try_from(value: &reqwest::header::HeaderMap) -> Result<Self, Self::Error> { + let x_has_more = value + .get("X-HasMore") + .map(|s| -> Result<_, _> { + let s = s.to_str().map_err(|_| StructureError::HeaderNotAscii)?; + s.parse::<bool>() + .map_err(|_| StructureError::HeaderParseFailed) + }) + .transpose()?; + let x_page = value + .get("X-Page") + .map(|s| -> Result<_, _> { + let s = s.to_str().map_err(|_| StructureError::HeaderNotAscii)?; + s.parse::<u64>() + .map_err(|_| StructureError::HeaderParseFailed) + }) + .transpose()?; + let x_page_count = value + .get("X-PageCount") + .map(|s| -> Result<_, _> { + let s = s.to_str().map_err(|_| StructureError::HeaderNotAscii)?; + s.parse::<u64>() + .map_err(|_| StructureError::HeaderParseFailed) + }) + .transpose()?; + let x_per_page = value + .get("X-PerPage") + .map(|s| -> Result<_, _> { + let s = s.to_str().map_err(|_| StructureError::HeaderNotAscii)?; + s.parse::<u64>() + .map_err(|_| StructureError::HeaderParseFailed) + }) + .transpose()?; + let x_total = value + .get("X-Total") + .map(|s| -> Result<_, _> { + let s = s.to_str().map_err(|_| StructureError::HeaderNotAscii)?; + s.parse::<u64>() + .map_err(|_| StructureError::HeaderParseFailed) + }) + .transpose()?; + Ok(Self { + x_has_more, + x_page, + x_page_count, + x_per_page, + x_total, + }) + } + } + + pub struct CommitListHeaders { + pub x_has_more: Option<bool>, + pub x_page: Option<u64>, + pub x_page_count: Option<u64>, + pub x_per_page: Option<u64>, + pub x_total: Option<u64>, + } + + impl TryFrom<&reqwest::header::HeaderMap> for CommitListHeaders { + type Error = StructureError; + + fn try_from(value: &reqwest::header::HeaderMap) -> Result<Self, Self::Error> { + let x_has_more = value + .get("X-HasMore") + .map(|s| -> Result<_, _> { + let s = s.to_str().map_err(|_| StructureError::HeaderNotAscii)?; + s.parse::<bool>() + .map_err(|_| StructureError::HeaderParseFailed) + }) + .transpose()?; + let x_page = value + .get("X-Page") + .map(|s| -> Result<_, _> { + let s = s.to_str().map_err(|_| StructureError::HeaderNotAscii)?; + s.parse::<u64>() + .map_err(|_| StructureError::HeaderParseFailed) + }) + .transpose()?; + let x_page_count = value + .get("X-PageCount") + .map(|s| -> Result<_, _> { + let s = s.to_str().map_err(|_| StructureError::HeaderNotAscii)?; + s.parse::<u64>() + .map_err(|_| StructureError::HeaderParseFailed) + }) + .transpose()?; + let x_per_page = value + .get("X-PerPage") + .map(|s| -> Result<_, _> { + let s = s.to_str().map_err(|_| StructureError::HeaderNotAscii)?; + s.parse::<u64>() + .map_err(|_| StructureError::HeaderParseFailed) + }) + .transpose()?; + let x_total = value + .get("X-Total") + .map(|s| -> Result<_, _> { + let s = s.to_str().map_err(|_| StructureError::HeaderNotAscii)?; + s.parse::<u64>() + .map_err(|_| StructureError::HeaderParseFailed) + }) + .transpose()?; + Ok(Self { + x_has_more, + x_page, + x_page_count, + x_per_page, + x_total, + }) + } + } + + pub struct ErrorHeaders { + pub message: Option<String>, + pub url: Option<String>, + } + + impl TryFrom<&reqwest::header::HeaderMap> for ErrorHeaders { + type Error = StructureError; + + fn try_from(value: &reqwest::header::HeaderMap) -> Result<Self, Self::Error> { + let message = value + .get("message") + .map(|s| -> Result<_, _> { + let s = s.to_str().map_err(|_| StructureError::HeaderNotAscii)?; + Ok(s.to_string()) + }) + .transpose()?; + let url = value + .get("url") + .map(|s| -> Result<_, _> { + let s = s.to_str().map_err(|_| StructureError::HeaderNotAscii)?; + Ok(s.to_string()) + }) + .transpose()?; + Ok(Self { message, url }) + } + } + + pub struct ForbiddenHeaders { + pub message: Option<String>, + pub url: Option<String>, + } + + impl TryFrom<&reqwest::header::HeaderMap> for ForbiddenHeaders { + type Error = StructureError; + + fn try_from(value: &reqwest::header::HeaderMap) -> Result<Self, Self::Error> { + let message = value + .get("message") + .map(|s| -> Result<_, _> { + let s = s.to_str().map_err(|_| StructureError::HeaderNotAscii)?; + Ok(s.to_string()) + }) + .transpose()?; + let url = value + .get("url") + .map(|s| -> Result<_, _> { + let s = s.to_str().map_err(|_| StructureError::HeaderNotAscii)?; + Ok(s.to_string()) + }) + .transpose()?; + Ok(Self { message, url }) + } + } + + pub struct InvalidTopicsErrorHeaders { + pub invalid_topics: Option<Vec<String>>, + pub message: Option<String>, + } + + impl TryFrom<&reqwest::header::HeaderMap> for InvalidTopicsErrorHeaders { + type Error = StructureError; + + fn try_from(value: &reqwest::header::HeaderMap) -> Result<Self, Self::Error> { + let invalid_topics = value + .get("invalidTopics") + .map(|s| -> Result<_, _> { + let s = s.to_str().map_err(|_| StructureError::HeaderNotAscii)?; + Ok(s.split(",").map(|s| s.to_string()).collect::<Vec<_>>()) + }) + .transpose()?; + let message = value + .get("message") + .map(|s| -> Result<_, _> { + let s = s.to_str().map_err(|_| StructureError::HeaderNotAscii)?; + Ok(s.to_string()) + }) + .transpose()?; + Ok(Self { + invalid_topics, + message, + }) + } + } + + pub struct ValidationErrorHeaders { + pub message: Option<String>, + pub url: Option<String>, + } + + impl TryFrom<&reqwest::header::HeaderMap> for ValidationErrorHeaders { + type Error = StructureError; + + fn try_from(value: &reqwest::header::HeaderMap) -> Result<Self, Self::Error> { + let message = value + .get("message") + .map(|s| -> Result<_, _> { + let s = s.to_str().map_err(|_| StructureError::HeaderNotAscii)?; + Ok(s.to_string()) + }) + .transpose()?; + let url = value + .get("url") + .map(|s| -> Result<_, _> { + let s = s.to_str().map_err(|_| StructureError::HeaderNotAscii)?; + Ok(s.to_string()) + }) + .transpose()?; + Ok(Self { message, url }) + } + } + pub struct AdminCronListQuery { pub page: Option<u32>, pub limit: Option<u32>, @@ -23,7 +23,7 @@ pub enum ForgejoError { #[error("API key should be ascii")] KeyNotAscii, #[error("the response from forgejo was not properly structured")] - BadStructure(#[source] serde_json::Error, String), + BadStructure(#[from] StructureError), #[error("unexpected status code {} {}", .0.as_u16(), .0.canonical_reason().unwrap_or(""))] UnexpectedStatusCode(StatusCode), #[error("{} {}{}", .0.as_u16(), .0.canonical_reason().unwrap_or(""), .1.as_ref().map(|s| format!(": {s}")).unwrap_or_default())] @@ -32,6 +32,21 @@ pub enum ForgejoError { AuthTooLong, } +#[derive(thiserror::Error, Debug)] +pub enum StructureError { + #[error("{contents}")] + Serde { + e: serde_json::Error, + contents: String, + }, + #[error("failed to find header `{0}`")] + HeaderMissing(&'static str), + #[error("header was not ascii")] + HeaderNotAscii, + #[error("failed to parse header")] + HeaderParseFailed, +} + /// Method of authentication to connect to the Forgejo host with. pub enum Auth<'a> { /// Application Access Token. Grants access to scope enabled for the diff --git a/tests/ci_test.rs b/tests/ci_test.rs index 3435ac5..1421300 100644 --- a/tests/ci_test.rs +++ b/tests/ci_test.rs @@ -168,6 +168,16 @@ async fn repo() { .await .is_ok(); assert!(!is_merged, "pr should not yet be merged"); + let pr_files_query = RepoGetPullRequestFilesQuery { + skip_to: None, + whitespace: None, + page: None, + limit: None, + }; + let (_, _) = api + .repo_get_pull_request_files("TestingAdmin", "test", pr.number.unwrap(), pr_files_query) + .await + .unwrap(); let merge_opt = MergePullRequestOption { r#do: "merge".into(), merge_commit_id: None, |