summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorCyborus <cyborus@cyborus.xyz>2024-02-09 16:58:44 +0100
committerCyborus <cyborus@cyborus.xyz>2024-02-09 20:37:43 +0100
commitcb1f2d9ae86223c18af43885eb62800963496e34 (patch)
tree7ea352e52f56575b0a86a608739684a0330d3aba
parentmore general dereferencing (diff)
downloadforgejo-api-cb1f2d9ae86223c18af43885eb62800963496e34.tar.xz
forgejo-api-cb1f2d9ae86223c18af43885eb62800963496e34.zip
strongly typed header returns
-rw-r--r--generator/src/methods.rs20
-rw-r--r--generator/src/structs.rs125
-rw-r--r--src/generated.rs248
-rw-r--r--src/lib.rs17
-rw-r--r--tests/ci_test.rs10
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>,
diff --git a/src/lib.rs b/src/lib.rs
index b877b01..161aac1 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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,