diff --git a/Cargo.lock b/Cargo.lock index 7360368..ea64452 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -136,9 +136,9 @@ dependencies = [ [[package]] name = "eyre" -version = "0.6.9" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80f656be11ddf91bd709454d15d5bd896fbaf4cc3314e69349e4d1569f5b46cd" +checksum = "b6267a1fa6f59179ea4afc8e50fd8612a3cc60bc858f786ff877a4a8cb042799" dependencies = [ "indenter", "once_cell", @@ -191,9 +191,9 @@ dependencies = [ [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -237,6 +237,17 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "generator" +version = "0.1.0" +dependencies = [ + "eyre", + "heck", + "serde", + "serde_json", + "url", +] + [[package]] name = "gimli" version = "0.28.0" @@ -268,6 +279,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "http" version = "0.2.9" @@ -341,9 +358,9 @@ dependencies = [ [[package]] name = "idna" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -531,9 +548,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project-lite" @@ -561,18 +578,18 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -684,18 +701,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.192" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.192" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" dependencies = [ "proc-macro2", "quote", @@ -704,9 +721,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" dependencies = [ "itoa", "ryu", @@ -762,9 +779,9 @@ checksum = "b5097ec7ea7218135541ad96348f1441d0c616537dd4ed9c47205920c35d7d97" [[package]] name = "syn" -version = "2.0.39" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -983,9 +1000,9 @@ dependencies = [ [[package]] name = "url" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", diff --git a/Cargo.toml b/Cargo.toml index c53d74d..7590571 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,4 @@ +workspace = { members = ["generator"] } [package] name = "forgejo-api" version = "0.1.0" diff --git a/generator/Cargo.toml b/generator/Cargo.toml new file mode 100644 index 0000000..21821e0 --- /dev/null +++ b/generator/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "generator" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +eyre = "0.6.11" +heck = "0.4.1" +serde = { version = "1.0.195", features = ["derive"] } +serde_json = "1.0.111" +url = { version = "2.5.0", features = ["serde"] } diff --git a/generator/src/main.rs b/generator/src/main.rs new file mode 100644 index 0000000..64173bf --- /dev/null +++ b/generator/src/main.rs @@ -0,0 +1,287 @@ +use std::{ffi::OsString, path::PathBuf}; + +mod methods; +mod openapi; +mod structs; + +use heck::{ToPascalCase, ToSnakeCase}; +use openapi::*; + +fn main() -> eyre::Result<()> { + let spec = get_spec()?; + let files = [ + ("mod.rs".into(), "pub mod structs;\npub mod methods;".into()), + ("methods.rs".into(), methods::create_methods(&spec)?), + ("structs.rs".into(), structs::create_structs(&spec)?), + ]; + save_generated(&files)?; + Ok(()) +} + +fn get_spec() -> eyre::Result { + let path = std::env::var_os("FORGEJO_API_SPEC_PATH") + .unwrap_or_else(|| OsString::from("./swagger.v1.json")); + let file = std::fs::read(path)?; + let spec = serde_json::from_slice::(&file)?; + spec.validate()?; + Ok(spec) +} + +fn save_generated(files: &[(String, String)]) -> eyre::Result<()> { + let root_path = PathBuf::from( + std::env::var_os("FORGEJO_API_GENERATED_PATH") + .unwrap_or_else(|| OsString::from("./src/generated/")), + ); + for (path, file) in files { + let path = root_path.join(path); + std::fs::create_dir_all(path.parent().ok_or_else(|| eyre::eyre!("no parent dir"))?)?; + std::fs::write(&path, file)?; + run_rustfmt_on(&path); + } + Ok(()) +} + +fn run_rustfmt_on(path: &std::path::Path) { + let mut rustfmt = std::process::Command::new("rustfmt"); + + rustfmt.arg(path); + rustfmt.args(["--edition", "2021"]); + + if let Err(e) = rustfmt.status() { + println!("Tried to format {path:?}, but failed to do so! :("); + println!("Error:\n{e}"); + } +} + +fn schema_ref_type_name(spec: &OpenApiV2, schema: &MaybeRef) -> eyre::Result { + let name = if let MaybeRef::Ref { _ref } = schema { + _ref.rsplit_once("/").map(|(_, b)| b) + } else { + None + }; + let schema = schema.deref(spec)?; + schema_type_name(spec, name, schema) +} + +fn schema_type_name( + spec: &OpenApiV2, + definition_name: Option<&str>, + schema: &Schema, +) -> eyre::Result { + if let Some(ty) = &schema._type { + match ty { + SchemaType::One(prim) => { + let name = match prim { + Primitive::String => match schema.format.as_deref() { + Some("date") => "time::Date", + Some("date-time") => "time::OffsetDateTime", + _ => "String", + } + .to_string(), + Primitive::Number => match schema.format.as_deref() { + Some("float") => "f32", + Some("double") => "f64", + _ => "f64", + } + .to_string(), + Primitive::Integer => match schema.format.as_deref() { + Some("int32") => "u32", + Some("int64") => "u64", + _ => "u32", + } + .to_string(), + Primitive::Boolean => "bool".to_string(), + Primitive::Array => { + let item_name = match &schema.items { + Some(item_schema) => schema_ref_type_name(spec, item_schema)?, + None => "serde_json::Value".into(), + }; + format!("Vec<{item_name}>") + } + Primitive::Null => "()".to_string(), + Primitive::Object => { + match (&schema.title, definition_name) { + // Some of the titles are actually descriptions; not sure why + // Checking for a space filters that out + (Some(title), _) if !title.contains(' ') => title.to_string(), + (_, Some(definition_name)) => definition_name.to_string(), + (_, None) => "BTreeMap".to_string(), + } + } + }; + Ok(name.to_owned()) + } + SchemaType::List(_) => todo!(), + } + } else { + Ok("serde_json::Value".into()) + } +} + +fn schema_is_string(spec: &OpenApiV2, schema: &MaybeRef) -> eyre::Result { + let schema = schema.deref(spec)?; + let is_str = match schema._type { + Some(SchemaType::One(Primitive::String)) => true, + _ => false, + }; + Ok(is_str) +} + +fn sanitize_ident(s: &str) -> String { + let mut s = s.to_snake_case(); + let keywords = [ + "as", + "break", + "const", + "continue", + "crate", + "else", + "enum", + "extern", + "false", + "fn", + "for", + "if", + "impl", + "in", + "let", + "loop", + "match", + "mod", + "move", + "mut", + "pub", + "ref", + "return", + "self", + "Self", + "static", + "struct", + "super", + "trait", + "true", + "type", + "unsafe", + "use", + "where", + "while", + "abstract", + "become", + "box", + "do", + "final", + "macro", + "override", + "priv", + "typeof", + "unsized", + "virtual", + "yield", + "async", + "await", + "dyn", + "try", + "macro_rules", + "union", + ]; + if s == "self" { + s = "this".into(); + } + if keywords.contains(&&*s) { + s.insert_str(0, "r#"); + } + s +} + +fn schema_subtype_name( + spec: &OpenApiV2, + parent_name: &str, + name: &str, + schema: &Schema, + ty: &mut String, +) -> eyre::Result { + let b = match schema { + Schema { + _type: Some(SchemaType::One(Primitive::Object)), + properties: Some(_), + .. + } + | Schema { + _type: Some(SchemaType::One(Primitive::String)), + _enum: Some(_), + .. + } => { + *ty = format!("{parent_name}{}", name.to_pascal_case()); + true + } + Schema { + _type: Some(SchemaType::One(Primitive::Object)), + properties: None, + additional_properties: Some(additional), + .. + } => { + let additional = additional.deref(spec)?; + let mut additional_ty = crate::schema_type_name(spec, None, additional)?; + schema_subtype_name(spec, parent_name, name, additional, &mut additional_ty)?; + *ty = format!("BTreeMap"); + true + } + Schema { + _type: Some(SchemaType::One(Primitive::Array)), + items: Some(items), + .. + } => { + if let MaybeRef::Value { value } = &**items { + if schema_subtype_name(spec, parent_name, name, value, ty)? { + *ty = format!("Vec<{ty}>"); + true + } else { + false + } + } else { + false + } + } + _ => false, + }; + Ok(b) +} +fn schema_subtypes( + spec: &OpenApiV2, + parent_name: &str, + name: &str, + schema: &Schema, + subtypes: &mut Vec, +) -> eyre::Result<()> { + match schema { + Schema { + _type: Some(SchemaType::One(Primitive::Object)), + properties: Some(_), + .. + } => { + let name = format!("{parent_name}{}", name.to_pascal_case()); + let subtype = structs::create_struct_for_definition(spec, &name, schema)?; + subtypes.push(subtype); + } + Schema { + _type: Some(SchemaType::One(Primitive::String)), + _enum: Some(_enum), + .. + } => { + let name = format!("{parent_name}{}", name.to_pascal_case()); + let subtype = structs::create_enum(&name, schema.description.as_deref(), _enum, false)?; + subtypes.push(subtype); + } + Schema { + _type: Some(SchemaType::One(Primitive::Array)), + items: Some(items), + .. + } => { + if let MaybeRef::Value { value } = &**items { + schema_subtypes(spec, parent_name, name, value, subtypes)?; + } + } + _ => (), + }; + Ok(()) +} diff --git a/generator/src/methods.rs b/generator/src/methods.rs new file mode 100644 index 0000000..6877437 --- /dev/null +++ b/generator/src/methods.rs @@ -0,0 +1,612 @@ +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 { + 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", + _ => { + 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") + } 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 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 fn_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) => Some(format!("Option<{a}>")), + (None, Some(b)) => 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) => Some(format!("Option<{a}>")), + (Some("()") | None, Some(b)) => 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(()) + } + } + } +} diff --git a/generator/src/openapi.rs b/generator/src/openapi.rs new file mode 100644 index 0000000..35dd9ba --- /dev/null +++ b/generator/src/openapi.rs @@ -0,0 +1,1447 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use eyre::WrapErr; +use url::Url; + +trait JsonDeref: std::any::Any { + fn deref_any<'a>(&'a self, path: &str) -> eyre::Result<&'a dyn std::any::Any>; +} + +impl JsonDeref for bool { + fn deref_any<'a>(&'a self, path: &str) -> eyre::Result<&'a dyn std::any::Any> { + if path.is_empty() { + Ok(self) + } else { + Err(eyre::eyre!("not found")) + } + } +} + +impl JsonDeref for u64 { + fn deref_any<'a>(&'a self, path: &str) -> eyre::Result<&'a dyn std::any::Any> { + if path.is_empty() { + Ok(self) + } else { + Err(eyre::eyre!("not found")) + } + } +} + +impl JsonDeref for f64 { + fn deref_any<'a>(&'a self, path: &str) -> eyre::Result<&'a dyn std::any::Any> { + if path.is_empty() { + Ok(self) + } else { + Err(eyre::eyre!("not found")) + } + } +} + +impl JsonDeref for String { + fn deref_any<'a>(&'a self, path: &str) -> eyre::Result<&'a dyn std::any::Any> { + if path.is_empty() { + Ok(self) + } else { + Err(eyre::eyre!("not found")) + } + } +} + +impl JsonDeref for Url { + fn deref_any<'a>(&'a self, path: &str) -> eyre::Result<&'a dyn std::any::Any> { + if path.is_empty() { + Ok(self) + } else { + Err(eyre::eyre!("not found")) + } + } +} + +impl JsonDeref for Option { + fn deref_any<'a>(&'a self, path: &str) -> eyre::Result<&'a dyn std::any::Any> { + match self { + Some(x) => x.deref_any(path), + None => Err(eyre::eyre!("not found")), + } + } +} + +impl JsonDeref for Box { + fn deref_any<'a>(&'a self, path: &str) -> eyre::Result<&'a dyn std::any::Any> { + T::deref_any(&**self, path) + } +} + +impl JsonDeref for Vec { + fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { + if path.is_empty() { + return Ok(self); + } + let (head, tail) = path.split_once("/").unwrap_or((path, "")); + let idx = head.parse::().wrap_err("not found")?; + let value = self.get(idx).ok_or_else(|| eyre::eyre!("not found"))?; + value.deref_any(tail) + } +} + +impl JsonDeref for BTreeMap { + fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { + if path.is_empty() { + return Ok(self); + } + let (head, tail) = path.split_once("/").unwrap_or((path, "")); + let value = self.get(head).ok_or_else(|| eyre::eyre!("not found"))?; + value.deref_any(tail) + } +} + +impl JsonDeref for serde_json::Value { + fn deref_any<'a>(&'a self, path: &str) -> eyre::Result<&'a dyn std::any::Any> { + match self { + serde_json::Value::Null => eyre::bail!("not found"), + serde_json::Value::Bool(b) => b.deref_any(path), + serde_json::Value::Number(x) => x.deref_any(path), + serde_json::Value::String(s) => s.deref_any(path), + serde_json::Value::Array(list) => list.deref_any(path), + serde_json::Value::Object(map) => map.deref_any(path), + } + } +} + +impl JsonDeref for serde_json::Map { + fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { + if path.is_empty() { + return Ok(self); + } + let (head, tail) = path.split_once("/").unwrap_or((path, "")); + let value = self.get(head).ok_or_else(|| eyre::eyre!("not found"))?; + value.deref_any(tail) + } +} + +impl JsonDeref for serde_json::Number { + fn deref_any<'a>(&'a self, path: &str) -> eyre::Result<&'a dyn std::any::Any> { + if path.is_empty() { + Ok(self) + } else { + eyre::bail!("not found") + } + } +} + +#[derive(serde::Deserialize, Debug, PartialEq)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct OpenApiV2 { + pub swagger: String, + pub info: SpecInfo, + pub host: Option, + pub base_path: Option, + pub schemes: Option>, + pub consumes: Option>, + pub produces: Option>, + pub paths: BTreeMap, + pub definitions: Option>, + pub parameters: Option>, + pub responses: Option>, + pub security_definitions: Option>, + pub security: Option>>>, + pub tags: Option>, + pub external_docs: Option, +} + +impl JsonDeref for OpenApiV2 { + fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { + let path = path + .strip_prefix("#/") + .ok_or_else(|| eyre::eyre!("invalid ref prefix"))?; + if path.is_empty() { + return Ok(self); + } + let (head, tail) = path.split_once("/").unwrap_or((path, "")); + match head { + "swagger" => self.swagger.deref_any(tail), + "info" => self.info.deref_any(tail), + "host" => self.host.deref_any(tail), + "base_path" => self.base_path.deref_any(tail), + "schemes" => self.schemes.deref_any(tail), + "consumes" => self.consumes.deref_any(tail), + "produces" => self.produces.deref_any(tail), + "paths" => self.paths.deref_any(tail), + "definitions" => self.definitions.deref_any(tail), + "parameters" => self.parameters.deref_any(tail), + "responses" => self.responses.deref_any(tail), + "security_definitions" => self.security_definitions.deref_any(tail), + "security" => self.security.deref_any(tail), + "tags" => self.tags.deref_any(tail), + "external_docs" => self.external_docs.deref_any(tail), + _ => eyre::bail!("not found: {head}"), + } + } +} + +impl OpenApiV2 { + pub fn validate(&self) -> eyre::Result<()> { + eyre::ensure!(self.swagger == "2.0", "swagger version must be 2.0"); + if let Some(host) = &self.host { + eyre::ensure!(!host.contains("://"), "openapi.host cannot contain scheme"); + eyre::ensure!(!host.contains("/"), "openapi.host cannot contain path"); + } + if let Some(base_path) = &self.base_path { + eyre::ensure!( + base_path.starts_with("/"), + "openapi.base_path must start with a forward slash" + ); + } + if let Some(schemes) = &self.schemes { + for scheme in schemes { + eyre::ensure!( + matches!(&**scheme, "http" | "https" | "ws" | "wss"), + "openapi.schemes must only be http, https, ws, or wss" + ); + } + } + for (path, path_item) in &self.paths { + eyre::ensure!( + path.starts_with("/"), + "members of openapi.paths must start with a forward slash; {path} does not" + ); + let mut operation_ids = BTreeSet::new(); + path_item + .validate(&mut operation_ids) + .wrap_err_with(|| format!("OpenApiV2.paths[\"{path}\"]"))?; + } + if let Some(definitions) = &self.definitions { + for (name, schema) in definitions { + schema + .validate() + .wrap_err_with(|| format!("OpenApiV2.definitions[\"{name}\"]"))?; + } + } + if let Some(params) = &self.parameters { + for (name, param) in params { + param + .validate() + .wrap_err_with(|| format!("OpenApiV2.parameters[\"{name}\"]"))?; + } + } + if let Some(responses) = &self.responses { + for (name, responses) in responses { + responses + .validate() + .wrap_err_with(|| format!("OpenApiV2.responses[\"{name}\"]"))?; + } + } + Ok(()) + } + + pub fn deref(&self, path: &str) -> eyre::Result<&T> { + self.deref_any(path).and_then(|a| { + a.downcast_ref::() + .ok_or_else(|| eyre::eyre!("incorrect type found at reference")) + }) + } +} + +#[derive(serde::Deserialize, Debug, PartialEq)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct SpecInfo { + pub title: String, + pub description: Option, + pub terms_of_service: Option, + pub contact: Option, + pub license: Option, + pub version: String, +} + +impl JsonDeref for SpecInfo { + fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { + if path.is_empty() { + return Ok(self); + } + let (head, tail) = path.split_once("/").unwrap_or((path, "")); + match head { + "title" => self.title.deref_any(tail), + "description" => self.description.deref_any(tail), + "terms_of_service" => self.terms_of_service.deref_any(tail), + "contact" => self.contact.deref_any(tail), + "license" => self.license.deref_any(tail), + "version" => self.version.deref_any(tail), + _ => eyre::bail!("not found: {head}"), + } + } +} + +#[derive(serde::Deserialize, Debug, PartialEq)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct Contact { + pub name: Option, + pub url: Option, + pub email: Option, +} + +impl JsonDeref for Contact { + fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { + if path.is_empty() { + return Ok(self); + } + let (head, tail) = path.split_once("/").unwrap_or((path, "")); + match head { + "name" => self.name.deref_any(tail), + "url" => self.url.deref_any(tail), + "email" => self.email.deref_any(tail), + _ => eyre::bail!("not found: {head}"), + } + } +} + +#[derive(serde::Deserialize, Debug, PartialEq)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct License { + pub name: String, + pub url: Option, +} + +impl JsonDeref for License { + fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { + if path.is_empty() { + return Ok(self); + } + let (head, tail) = path.split_once("/").unwrap_or((path, "")); + match head { + "name" => self.name.deref_any(tail), + "url" => self.url.deref_any(tail), + _ => eyre::bail!("not found: {head}"), + } + } +} + +#[derive(serde::Deserialize, Debug, PartialEq)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct PathItem { + #[serde(rename = "$ref")] + pub _ref: Option, + pub get: Option, + pub put: Option, + pub post: Option, + pub delete: Option, + pub options: Option, + pub head: Option, + pub patch: Option, + pub parameters: Option>>, +} + +impl JsonDeref for PathItem { + fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { + if path.is_empty() { + return Ok(self); + } + let (head, tail) = path.split_once("/").unwrap_or((path, "")); + match head { + "$ref" => self._ref.deref_any(tail), + "get" => self.get.deref_any(tail), + "put" => self.put.deref_any(tail), + "post" => self.post.deref_any(tail), + "delete" => self.delete.deref_any(tail), + "options" => self.options.deref_any(tail), + "head" => self.head.deref_any(tail), + "patch" => self.patch.deref_any(tail), + "parameters" => self.parameters.deref_any(tail), + _ => eyre::bail!("not found: {head}"), + } + } +} + +impl PathItem { + fn validate<'a>(&'a self, ids: &mut BTreeSet<&'a str>) -> eyre::Result<()> { + if let Some(op) = &self.get { + op.validate(ids).wrap_err("PathItem.get")?; + } + if let Some(op) = &self.put { + op.validate(ids).wrap_err("PathItem.patch")?; + } + if let Some(op) = &self.post { + op.validate(ids).wrap_err("PathItem.patch")?; + } + if let Some(op) = &self.delete { + op.validate(ids).wrap_err("PathItem.patch")?; + } + if let Some(op) = &self.options { + op.validate(ids).wrap_err("PathItem.patch")?; + } + if let Some(op) = &self.head { + op.validate(ids).wrap_err("PathItem.patch")?; + } + if let Some(op) = &self.patch { + op.validate(ids).wrap_err("PathItem.patch")?; + } + if let Some(params) = &self.parameters { + for param in params { + if let MaybeRef::Value { value } = param { + value.validate().wrap_err("PathItem.parameters")?; + } + } + } + Ok(()) + } +} + +#[derive(serde::Deserialize, Debug, PartialEq)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct Operation { + pub tags: Option>, + pub summary: Option, + pub description: Option, + pub external_docs: Option, + pub operation_id: Option, + pub consumes: Option>, + pub produces: Option>, + pub parameters: Option>>, + pub responses: Responses, + pub schemes: Option>, + pub deprecated: Option, + pub security: Option>>>, +} + +impl JsonDeref for Operation { + fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { + if path.is_empty() { + return Ok(self); + } + let (head, tail) = path.split_once("/").unwrap_or((path, "")); + match head { + "tags" => self.tags.deref_any(tail), + "summary" => self.summary.deref_any(tail), + "description" => self.description.deref_any(tail), + "external_docs" => self.external_docs.deref_any(tail), + "operation_id" => self.operation_id.deref_any(tail), + "consumes" => self.consumes.deref_any(tail), + "produces" => self.produces.deref_any(tail), + "parameters" => self.parameters.deref_any(tail), + "responses" => self.responses.deref_any(tail), + "schemes" => self.schemes.deref_any(tail), + "deprecated" => self.deprecated.deref_any(tail), + "security" => self.security.deref_any(tail), + _ => eyre::bail!("not found: {head}"), + } + } +} + +impl Operation { + fn validate<'a>(&'a self, ids: &mut BTreeSet<&'a str>) -> eyre::Result<()> { + if let Some(operation_id) = self.operation_id.as_deref() { + let is_new = ids.insert(operation_id); + eyre::ensure!(is_new, "duplicate operation id"); + } + if let Some(params) = &self.parameters { + for param in params { + if let MaybeRef::Value { value } = param { + value.validate().wrap_err("Operation.parameters")?; + } + } + } + self.responses.validate().wrap_err("operation response")?; + if let Some(schemes) = &self.schemes { + for scheme in schemes { + eyre::ensure!( + matches!(&**scheme, "http" | "https" | "ws" | "wss"), + "openapi.schemes must only be http, https, ws, or wss" + ); + } + } + Ok(()) + } +} + +#[derive(serde::Deserialize, Debug, PartialEq)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct ExternalDocs { + pub description: Option, + pub url: Url, +} + +impl JsonDeref for ExternalDocs { + fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { + if path.is_empty() { + return Ok(self); + } + let (head, tail) = path.split_once("/").unwrap_or((path, "")); + match head { + "description" => self.description.deref_any(tail), + "url" => self.url.deref_any(tail), + _ => eyre::bail!("not found: {head}"), + } + } +} + +#[derive(serde::Deserialize, Debug, PartialEq)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct Parameter { + pub name: String, + pub description: Option, + #[serde(flatten)] + pub _in: ParameterIn, +} + +impl JsonDeref for Parameter { + fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { + if path.is_empty() { + return Ok(self); + } + let (head, tail) = path.split_once("/").unwrap_or((path, "")); + match head { + "name" => self.name.deref_any(tail), + "description" => self.description.deref_any(tail), + "in" => { + if tail.is_empty() { + Ok(match &self._in { + ParameterIn::Body { schema: _ } => &"body" as _, + ParameterIn::Path { param: _ } => &"path" as _, + ParameterIn::Query { param: _ } => &"query" as _, + ParameterIn::Header { param: _ } => &"header" as _, + ParameterIn::FormData { param: _ } => &"formData" as _, + }) + } else { + eyre::bail!("not found") + } + } + _ => self._in.deref_any(path), + } + } +} + +impl Parameter { + fn validate(&self) -> eyre::Result<()> { + self._in.validate() + } +} + +#[derive(serde::Deserialize, Debug, PartialEq)] +#[serde(rename_all(deserialize = "camelCase"))] +#[serde(tag = "in")] +pub enum ParameterIn { + Body { + schema: MaybeRef, + }, + Path { + #[serde(flatten)] + param: NonBodyParameter, + }, + Query { + #[serde(flatten)] + param: NonBodyParameter, + }, + Header { + #[serde(flatten)] + param: NonBodyParameter, + }, + FormData { + #[serde(flatten)] + param: NonBodyParameter, + }, +} + +impl JsonDeref for ParameterIn { + fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { + let (head, tail) = path.split_once("/").unwrap_or((path, "")); + match self { + ParameterIn::Body { schema } => match head { + "schema" => schema.deref_any(tail), + _ => eyre::bail!("not found: {head}"), + }, + ParameterIn::Path { param } + | ParameterIn::Query { param } + | ParameterIn::Header { param } + | ParameterIn::FormData { param } => param.deref_any(path), + } + } +} + +impl ParameterIn { + fn validate(&self) -> eyre::Result<()> { + match self { + ParameterIn::Path { param } => { + eyre::ensure!(param.required, "path parameters must be required"); + param.validate().wrap_err("path param") + } + ParameterIn::Query { param } => param.validate().wrap_err("query param"), + ParameterIn::Header { param } => param.validate().wrap_err("header param"), + ParameterIn::Body { schema } => { + if let MaybeRef::Value { value } = schema { + value.validate().wrap_err("body param") + } else { + Ok(()) + } + } + ParameterIn::FormData { param } => param.validate().wrap_err("form param"), + } + } +} + +#[derive(serde::Deserialize, Debug, PartialEq)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct NonBodyParameter { + #[serde(default)] + pub required: bool, + #[serde(rename = "type")] + pub _type: ParameterType, + pub format: Option, + pub allow_empty_value: Option, + pub items: Option, + pub collection_format: Option, + pub default: Option, + pub maximum: Option, + pub exclusive_maximum: Option, + pub minimum: Option, + pub exclusive_minimum: Option, + pub max_length: Option, + pub min_length: Option, + pub pattern: Option, // should be regex + pub max_items: Option, + pub min_items: Option, + pub unique_items: Option, + #[serde(rename = "enum")] + pub _enum: Option>, + pub multiple_of: Option, +} + +impl JsonDeref for NonBodyParameter { + fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { + if path.is_empty() { + return Ok(self); + } + let (head, tail) = path.split_once("/").unwrap_or((path, "")); + match head { + "required" => self.required.deref_any(tail), + "type" => self._type.deref_any(tail), + "format" => self.format.deref_any(tail), + "allow_empty_value" => self.allow_empty_value.deref_any(tail), + "items" => self.items.deref_any(tail), + "collection_format" => self.collection_format.deref_any(tail), + "default" => self.default.deref_any(tail), + "maximum" => self.maximum.deref_any(tail), + "exclusive_maximum" => self.exclusive_maximum.deref_any(tail), + "minimum" => self.minimum.deref_any(tail), + "exclusive_minimum" => self.exclusive_minimum.deref_any(tail), + "max_length" => self.max_length.deref_any(tail), + "min_length" => self.min_length.deref_any(tail), + "pattern" => self.pattern.deref_any(tail), // should be regex + "max_items" => self.max_items.deref_any(tail), + "min_items" => self.min_items.deref_any(tail), + "unique_items" => self.unique_items.deref_any(tail), + "enum" => self._enum.deref_any(tail), + "multiple_of" => self.multiple_of.deref_any(tail), + _ => eyre::bail!("not found: {head}"), + } + } +} + +impl NonBodyParameter { + fn validate(&self) -> eyre::Result<()> { + if self._type == ParameterType::Array { + eyre::ensure!( + self.items.is_some(), + "array paramters must define their item types" + ); + } + if let Some(items) = &self.items { + items.validate()?; + } + if let Some(default) = &self.default { + eyre::ensure!( + self._type.matches_value(default), + "param's default must match its type" + ); + } + if let Some(_enum) = &self._enum { + for variant in _enum { + eyre::ensure!( + self._type.matches_value(variant), + "header enum variant must match its type" + ); + } + } + if self.exclusive_maximum.is_some() { + eyre::ensure!( + self.maximum.is_some(), + "presence of `exclusiveMaximum` requires `maximum` be there too" + ); + } + if self.exclusive_minimum.is_some() { + eyre::ensure!( + self.minimum.is_some(), + "presence of `exclusiveMinimum` requires `minimum` be there too" + ); + } + if let Some(multiple_of) = self.multiple_of { + eyre::ensure!(multiple_of > 0, "multipleOf must be greater than 0"); + } + Ok(()) + } +} + +#[derive(serde::Deserialize, Debug, PartialEq)] +#[serde(rename_all(deserialize = "camelCase"))] +pub enum ParameterType { + String, + Number, + Integer, + Boolean, + Array, + File, +} + +impl JsonDeref for ParameterType { + fn deref_any<'a>(&'a self, path: &str) -> eyre::Result<&'a dyn std::any::Any> { + if path.is_empty() { + Ok(self) + } else { + Err(eyre::eyre!("not found")) + } + } +} + +impl ParameterType { + fn matches_value(&self, value: &serde_json::Value) -> bool { + match (self, value) { + (ParameterType::String, serde_json::Value::String(_)) + | (ParameterType::Number, serde_json::Value::Number(_)) + | (ParameterType::Integer, serde_json::Value::Number(_)) + | (ParameterType::Boolean, serde_json::Value::Bool(_)) + | (ParameterType::Array, serde_json::Value::Array(_)) => true, + _ => false, + } + } +} + +#[derive(serde::Deserialize, Debug, PartialEq, Clone, Copy)] +#[serde(rename_all(deserialize = "camelCase"))] +pub enum CollectionFormat { + Csv, + Ssv, + Tsv, + Pipes, + Multi, +} + +impl JsonDeref for CollectionFormat { + fn deref_any<'a>(&'a self, path: &str) -> eyre::Result<&'a dyn std::any::Any> { + if path.is_empty() { + Ok(self) + } else { + Err(eyre::eyre!("not found")) + } + } +} + +#[derive(serde::Deserialize, Debug, PartialEq)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct Items { + #[serde(rename = "type")] + pub _type: ParameterType, + pub format: Option, + pub items: Option>, + pub collection_format: Option, + pub default: Option, + pub maximum: Option, + pub exclusive_maximum: Option, + pub minimum: Option, + pub exclusive_minimum: Option, + pub max_length: Option, + pub min_length: Option, + pub pattern: Option, // should be regex + pub max_items: Option, + pub min_items: Option, + pub unique_items: Option, + #[serde(rename = "enum")] + pub _enum: Option>, + pub multiple_of: Option, +} + +impl JsonDeref for Items { + fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { + if path.is_empty() { + return Ok(self); + } + let (head, tail) = path.split_once("/").unwrap_or((path, "")); + match head { + "type" => self._type.deref_any(tail), + "format" => self.format.deref_any(tail), + "items" => self.items.deref_any(tail), + "collection_format" => self.collection_format.deref_any(tail), + "default" => self.default.deref_any(tail), + "maximum" => self.maximum.deref_any(tail), + "exclusive_maximum" => self.exclusive_maximum.deref_any(tail), + "minimum" => self.minimum.deref_any(tail), + "exclusive_minimum" => self.exclusive_minimum.deref_any(tail), + "max_length" => self.max_length.deref_any(tail), + "min_length" => self.min_length.deref_any(tail), + "pattern" => self.pattern.deref_any(tail), // should be regex + "max_items" => self.max_items.deref_any(tail), + "min_items" => self.min_items.deref_any(tail), + "unique_items" => self.unique_items.deref_any(tail), + "enum" => self._enum.deref_any(tail), + "multiple_of" => self.multiple_of.deref_any(tail), + _ => eyre::bail!("not found: {head}"), + } + } +} + +impl Items { + fn validate(&self) -> eyre::Result<()> { + if self._type == ParameterType::Array { + eyre::ensure!( + self.items.is_some(), + "array paramters must define their item types" + ); + } + if let Some(items) = &self.items { + items.validate()?; + } + if let Some(default) = &self.default { + match (&self._type, default) { + (ParameterType::String, serde_json::Value::String(_)) + | (ParameterType::Number, serde_json::Value::Number(_)) + | (ParameterType::Integer, serde_json::Value::Number(_)) + | (ParameterType::Boolean, serde_json::Value::Bool(_)) + | (ParameterType::Array, serde_json::Value::Array(_)) => (), + (ParameterType::File, _) => eyre::bail!("file params cannot have default value"), + _ => eyre::bail!("param's default must match its type"), + }; + } + if let Some(_enum) = &self._enum { + for variant in _enum { + eyre::ensure!( + self._type.matches_value(variant), + "header enum variant must match its type" + ); + } + } + if self.exclusive_maximum.is_some() { + eyre::ensure!( + self.maximum.is_some(), + "presence of `exclusiveMaximum` requires `maximum` be there too" + ); + } + if self.exclusive_minimum.is_some() { + eyre::ensure!( + self.minimum.is_some(), + "presence of `exclusiveMinimum` requires `minimum` be there too" + ); + } + if let Some(multiple_of) = self.multiple_of { + eyre::ensure!(multiple_of > 0, "multipleOf must be greater than 0"); + } + Ok(()) + } +} + +#[derive(serde::Deserialize, Debug, PartialEq)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct Responses { + pub default: Option>, + #[serde(flatten)] + pub http_codes: BTreeMap>, +} + +impl JsonDeref for Responses { + fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { + if path.is_empty() { + return Ok(self); + } + let (head, tail) = path.split_once("/").unwrap_or((path, "")); + match head { + "default" => self.default.deref_any(tail), + code => self + .http_codes + .get(code) + .map(|r| r as _) + .ok_or_else(|| eyre::eyre!("not found")), + } + } +} + +impl Responses { + fn validate(&self) -> eyre::Result<()> { + if self.default.is_none() && self.http_codes.is_empty() { + eyre::bail!("must have at least one response"); + } + if let Some(MaybeRef::Value { value }) = &self.default { + value.validate().wrap_err("default response")?; + } + for (code, response) in &self.http_codes { + let code_int = code.parse::().wrap_err("http code must be a number")?; + eyre::ensure!( + code_int >= 100 && code_int < 1000, + "invalid http status code" + ); + if let MaybeRef::Value { value } = response { + value.validate().wrap_err_with(|| code.to_string())?; + } + } + Ok(()) + } +} + +#[derive(serde::Deserialize, Debug, PartialEq)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct Response { + pub description: String, + pub schema: Option>, + pub headers: Option>, + pub examples: Option>, +} + +impl JsonDeref for Response { + fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { + if path.is_empty() { + return Ok(self); + } + let (head, tail) = path.split_once("/").unwrap_or((path, "")); + match head { + "description" => self.description.deref_any(tail), + "schema" => self.schema.deref_any(tail), + "headers" => self.headers.deref_any(tail), + "examples" => self.examples.deref_any(tail), + _ => eyre::bail!("not found: {head}"), + } + } +} + +impl Response { + fn validate(&self) -> eyre::Result<()> { + if let Some(headers) = &self.headers { + for (_, value) in headers { + value.validate().wrap_err("response header")?; + } + } + if let Some(MaybeRef::Value { value }) = &self.schema { + value.validate().wrap_err("response")?; + } + Ok(()) + } +} + +#[derive(serde::Deserialize, Debug, PartialEq)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct Header { + pub description: Option, + #[serde(rename = "type")] + pub _type: ParameterType, + pub format: Option, + pub items: Option, + pub collection_format: Option, + pub default: Option, + pub maximum: Option, + pub exclusive_maximum: Option, + pub minimum: Option, + pub exclusive_minimum: Option, + pub max_length: Option, + pub min_length: Option, + pub pattern: Option, // should be regex + pub max_items: Option, + pub min_items: Option, + pub unique_items: Option, + #[serde(rename = "enum")] + pub _enum: Option>, + pub multiple_of: Option, +} + +impl JsonDeref for Header { + fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { + if path.is_empty() { + return Ok(self); + } + let (head, tail) = path.split_once("/").unwrap_or((path, "")); + match head { + "description" => self.description.deref_any(tail), + "type" => self._type.deref_any(tail), + "format" => self.format.deref_any(tail), + "items" => self.items.deref_any(tail), + "collection_format" => self.collection_format.deref_any(tail), + "default" => self.default.deref_any(tail), + "maximum" => self.maximum.deref_any(tail), + "exclusive_maximum" => self.exclusive_maximum.deref_any(tail), + "minimum" => self.minimum.deref_any(tail), + "exclusive_minimum" => self.exclusive_minimum.deref_any(tail), + "max_length" => self.max_length.deref_any(tail), + "min_length" => self.min_length.deref_any(tail), + "pattern" => self.pattern.deref_any(tail), // should be regex + "max_items" => self.max_items.deref_any(tail), + "min_items" => self.min_items.deref_any(tail), + "unique_items" => self.unique_items.deref_any(tail), + "enum" => self._enum.deref_any(tail), + "multiple_of" => self.multiple_of.deref_any(tail), + _ => eyre::bail!("not found: {head}"), + } + } +} + +impl Header { + fn validate(&self) -> eyre::Result<()> { + if self._type == ParameterType::Array { + eyre::ensure!( + self.items.is_some(), + "array paramters must define their item types" + ); + } + if let Some(default) = &self.default { + match (&self._type, default) { + (ParameterType::String, serde_json::Value::String(_)) + | (ParameterType::Number, serde_json::Value::Number(_)) + | (ParameterType::Integer, serde_json::Value::Number(_)) + | (ParameterType::Boolean, serde_json::Value::Bool(_)) + | (ParameterType::Array, serde_json::Value::Array(_)) => (), + (ParameterType::File, _) => eyre::bail!("file params cannot have default value"), + _ => eyre::bail!("param's default must match its type"), + }; + } + if let Some(_enum) = &self._enum { + for variant in _enum { + eyre::ensure!( + self._type.matches_value(variant), + "header enum variant must match its type" + ); + } + } + if self.exclusive_maximum.is_some() { + eyre::ensure!( + self.maximum.is_some(), + "presence of `exclusiveMaximum` requires `maximum` be there too" + ); + } + if self.exclusive_minimum.is_some() { + eyre::ensure!( + self.minimum.is_some(), + "presence of `exclusiveMinimum` requires `minimum` be there too" + ); + } + if let Some(multiple_of) = self.multiple_of { + eyre::ensure!(multiple_of > 0, "multipleOf must be greater than 0"); + } + Ok(()) + } +} + +#[derive(serde::Deserialize, Debug, PartialEq)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct Tag { + pub name: String, + pub description: Option, + pub external_docs: Option, +} + +impl JsonDeref for Tag { + fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { + if path.is_empty() { + return Ok(self); + } + let (head, tail) = path.split_once("/").unwrap_or((path, "")); + match head { + "name" => self.name.deref_any(tail), + "description" => self.description.deref_any(tail), + "external_docs" => self.external_docs.deref_any(tail), + _ => eyre::bail!("not found: {head}"), + } + } +} + +#[derive(serde::Deserialize, Debug, PartialEq)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct Schema { + pub format: Option, + pub title: Option, + pub description: Option, + pub default: Option, + pub multiple_of: Option, + pub maximum: Option, + pub exclusive_maximum: Option, + pub minimum: Option, + pub exclusive_minimum: Option, + pub max_length: Option, + pub min_length: Option, + pub pattern: Option, // should be regex + pub max_items: Option, + pub min_items: Option, + pub unique_items: Option, + pub max_properties: Option, + pub min_properties: Option, + pub required: Option>, + #[serde(rename = "enum")] + pub _enum: Option>, + #[serde(rename = "type")] + pub _type: Option, + pub properties: Option>>, + pub additional_properties: Option>>, + pub items: Option>>, + + pub discriminator: Option, + pub read_only: Option, + pub xml: Option, + pub external_docs: Option, + pub example: Option, +} + +impl JsonDeref for Schema { + fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { + if path.is_empty() { + return Ok(self); + } + let (head, tail) = path.split_once("/").unwrap_or((path, "")); + match head { + "format" => self.format.deref_any(tail), + "title" => self.title.deref_any(tail), + "description" => self.description.deref_any(tail), + "default" => self.default.deref_any(tail), + "multiple_of" => self.multiple_of.deref_any(tail), + "maximum" => self.maximum.deref_any(tail), + "exclusive_maximum" => self.exclusive_maximum.deref_any(tail), + "minimum" => self.minimum.deref_any(tail), + "exclusive_minimum" => self.exclusive_minimum.deref_any(tail), + "max_length" => self.max_length.deref_any(tail), + "min_length" => self.min_length.deref_any(tail), + "pattern" => self.pattern.deref_any(tail), // should be regex + "max_items" => self.max_items.deref_any(tail), + "min_items" => self.min_items.deref_any(tail), + "unique_items" => self.unique_items.deref_any(tail), + "max_properties" => self.max_properties.deref_any(tail), + "min_properties" => self.min_properties.deref_any(tail), + "required" => self.required.deref_any(tail), + "enum" => self._enum.deref_any(tail), + "type" => self._type.deref_any(tail), + "properties" => self.properties.deref_any(tail), + "additional_properties" => self.additional_properties.deref_any(tail), + "items" => self.items.deref_any(tail), + "discriminator" => self.discriminator.deref_any(tail), + "read_only" => self.read_only.deref_any(tail), + "xml" => self.xml.deref_any(tail), + "external_docs" => self.external_docs.deref_any(tail), + "example" => self.example.deref_any(tail), + _ => eyre::bail!("not found: {head}"), + } + } +} + +impl Schema { + fn validate(&self) -> eyre::Result<()> { + if let Some(_type) = &self._type { + match _type { + SchemaType::One(_type) => { + if _type == &Primitive::Array { + eyre::ensure!( + self.items.is_some(), + "array paramters must define their item types" + ); + } + if let Some(default) = &self.default { + eyre::ensure!( + _type.matches_value(default), + "param's default must match its type" + ); + } + if let Some(_enum) = &self._enum { + for variant in _enum { + eyre::ensure!( + _type.matches_value(variant), + "schema enum variant must match its type" + ); + } + } + } + SchemaType::List(_) => { + eyre::bail!("sum types not supported"); + } + } + } else { + eyre::ensure!( + self.default.is_none(), + "cannot have default when no type is specified" + ); + } + if let Some(items) = &self.items { + if let MaybeRef::Value { value } = &**items { + value.validate()?; + } + } + if let Some(required) = &self.required { + let properties = self.properties.as_ref().ok_or_else(|| { + eyre::eyre!("required properties listed but no properties present") + })?; + for i in required { + eyre::ensure!( + properties.contains_key(i), + "property \"{i}\" required, but is not defined" + ); + } + } + if let Some(properties) = &self.properties { + for (_, schema) in properties { + if let MaybeRef::Value { value } = schema { + value.validate().wrap_err("schema properties")?; + } + } + } + if let Some(additional_properties) = &self.additional_properties { + if let MaybeRef::Value { value } = &**additional_properties { + value.validate().wrap_err("schema additional properties")?; + } + } + if self.exclusive_maximum.is_some() { + eyre::ensure!( + self.maximum.is_some(), + "presence of `exclusiveMaximum` requires `maximum` be there too" + ); + } + if self.exclusive_minimum.is_some() { + eyre::ensure!( + self.minimum.is_some(), + "presence of `exclusiveMinimum` requires `minimum` be there too" + ); + } + if let Some(multiple_of) = self.multiple_of { + eyre::ensure!(multiple_of > 0, "multipleOf must be greater than 0"); + } + Ok(()) + } +} + +#[derive(serde::Deserialize, Debug, PartialEq)] +#[serde(untagged)] +pub enum SchemaType { + One(Primitive), + List(Vec), +} + +impl JsonDeref for SchemaType { + fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { + match self { + SchemaType::One(i) => i.deref_any(path), + SchemaType::List(list) => list.deref_any(path), + } + } +} + +#[derive(serde::Deserialize, Debug, PartialEq)] +#[serde(rename_all(deserialize = "camelCase"))] +pub enum Primitive { + Array, + Boolean, + Integer, + Number, + Null, + Object, + String, +} + +impl JsonDeref for Primitive { + fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { + if path.is_empty() { + Ok(self) + } else { + Err(eyre::eyre!("not found")) + } + } +} + +impl Primitive { + fn matches_value(&self, value: &serde_json::Value) -> bool { + match (self, value) { + (Primitive::String, serde_json::Value::String(_)) + | (Primitive::Number, serde_json::Value::Number(_)) + | (Primitive::Integer, serde_json::Value::Number(_)) + | (Primitive::Boolean, serde_json::Value::Bool(_)) + | (Primitive::Array, serde_json::Value::Array(_)) => true, + _ => false, + } + } +} + +#[derive(serde::Deserialize, Debug, PartialEq)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct Xml { + pub name: Option, + pub namespace: Option, + pub prefix: Option, + pub attribute: Option, + pub wrapped: Option, +} + +impl JsonDeref for Xml { + fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { + if path.is_empty() { + return Ok(self); + } + let (head, tail) = path.split_once("/").unwrap_or((path, "")); + match head { + "name" => self.name.deref_any(tail), + "namespace" => self.namespace.deref_any(tail), + "prefix" => self.prefix.deref_any(tail), + "attribute" => self.attribute.deref_any(tail), + "wrapped" => self.wrapped.deref_any(tail), + _ => eyre::bail!("not found: {head}"), + } + } +} + +#[derive(serde::Deserialize, Debug, PartialEq)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct SecurityScheme { + #[serde(flatten)] + pub _type: SecurityType, + pub description: Option, +} + +impl JsonDeref for SecurityScheme { + fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { + if path.is_empty() { + return Ok(self); + } + let (head, tail) = path.split_once("/").unwrap_or((path, "")); + match head { + "type" => self._type.deref_any(tail), + "description" => self.description.deref_any(tail), + _ => eyre::bail!("not found: {head}"), + } + } +} + +#[derive(serde::Deserialize, Debug, PartialEq)] +#[serde(rename_all(deserialize = "camelCase"), tag = "type")] +pub enum SecurityType { + Basic, + ApiKey { + name: String, + #[serde(rename = "in")] + _in: KeyIn, + }, + OAuth2 { + #[serde(flatten)] + flow: OAuth2Flow, + scopes: BTreeMap, + }, +} + +impl JsonDeref for SecurityType { + fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { + if path.is_empty() { + return Ok(self); + } + let (head, tail) = path.split_once("/").unwrap_or((path, "")); + match self { + SecurityType::Basic => eyre::bail!("not found: {head}"), + SecurityType::ApiKey { name, _in } => match head { + "name" => name.deref_any(tail), + "in" => _in.deref_any(tail), + _ => eyre::bail!("not found: {head}"), + }, + SecurityType::OAuth2 { flow, scopes } => match head { + "flow" => flow.deref_any(tail), + "scopes" => scopes.deref_any(tail), + _ => eyre::bail!("not found: {head}"), + }, + } + } +} + +#[derive(serde::Deserialize, Debug, PartialEq)] +#[serde(rename_all(deserialize = "camelCase"))] +pub enum KeyIn { + Query, + Header, +} + +impl JsonDeref for KeyIn { + fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { + if path.is_empty() { + Ok(self) + } else { + eyre::bail!("not found") + } + } +} + +#[derive(serde::Deserialize, Debug, PartialEq)] +#[serde(rename_all(deserialize = "camelCase"), tag = "flow")] +pub enum OAuth2Flow { + Implicit { + authorization_url: Url, + }, + Password { + token_url: Url, + }, + Application { + token_url: Url, + }, + AccessCode { + authorization_url: Url, + token_url: Url, + }, +} + +impl JsonDeref for OAuth2Flow { + fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { + if path.is_empty() { + return Ok(self); + } + let (head, tail) = path.split_once("/").unwrap_or((path, "")); + match self { + OAuth2Flow::Implicit { authorization_url } => match head { + "authorizationUrl" => authorization_url.deref_any(tail), + _ => eyre::bail!("not found: {head}"), + }, + OAuth2Flow::Password { token_url } => match head { + "tokenUrl" => token_url.deref_any(tail), + _ => eyre::bail!("not found: {head}"), + }, + OAuth2Flow::Application { token_url } => match head { + "tokenUrl" => token_url.deref_any(tail), + _ => eyre::bail!("not found: {head}"), + }, + OAuth2Flow::AccessCode { + authorization_url, + token_url, + } => match head { + "authorizationUrl" => authorization_url.deref_any(tail), + "tokenUrl" => token_url.deref_any(tail), + _ => eyre::bail!("not found: {head}"), + }, + } + } +} + +#[derive(serde::Deserialize, Debug, PartialEq)] +#[serde(untagged)] +pub enum MaybeRef { + Ref { + #[serde(rename = "$ref")] + _ref: String, + }, + Value { + #[serde(flatten)] + value: T, + }, +} + +impl JsonDeref for MaybeRef { + fn deref_any(&self, path: &str) -> eyre::Result<&dyn std::any::Any> { + if path.is_empty() { + return Ok(self); + } + let (head, tail) = path.split_once("/").unwrap_or((path, "")); + match self { + MaybeRef::Ref { _ref } => match head { + "$ref" => _ref.deref_any(tail), + _ => eyre::bail!("not found: {head}"), + }, + MaybeRef::Value { value } => value.deref_any(path), + } + } +} + +impl MaybeRef { + pub fn deref<'a>(&'a self, spec: &'a OpenApiV2) -> eyre::Result<&'a T> { + match self { + MaybeRef::Ref { _ref } => spec.deref(_ref), + MaybeRef::Value { value } => Ok(value), + } + } +} diff --git a/generator/src/structs.rs b/generator/src/structs.rs new file mode 100644 index 0000000..0b4de08 --- /dev/null +++ b/generator/src/structs.rs @@ -0,0 +1,654 @@ +use crate::openapi::*; +use eyre::WrapErr; +use heck::ToPascalCase; +use std::fmt::Write; + +pub fn create_structs(spec: &OpenApiV2) -> eyre::Result { + let mut s = String::new(); + s.push_str("use crate::StructureError;"); + s.push_str("use std::collections::BTreeMap;"); + 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); + } + + let tys = create_response_struct(spec, name, response)?; + s.push_str(&tys); + } + } + for (_, item) in &spec.paths { + let strukt = create_query_structs_for_path(spec, item)?; + s.push_str(&strukt); + + let strukt = create_response_structs(spec, item)?; + s.push_str(&strukt); + } + Ok(s) +} + +pub fn create_struct_for_definition( + spec: &OpenApiV2, + name: &str, + schema: &Schema, +) -> eyre::Result { + if matches!(schema._type, Some(SchemaType::One(Primitive::Array))) { + return Ok(String::new()); + } + + if schema._type == Some(SchemaType::One(Primitive::String)) { + if let Some(_enum) = &schema._enum { + return create_enum(name, schema.description.as_deref(), _enum, false); + } + } + + let mut subtypes = Vec::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 let MaybeRef::Value { value } = &prop_schema { + crate::schema_subtype_name(spec, name, prop_name, value, &mut field_ty)?; + crate::schema_subtypes(spec, name, prop_name, value, &mut subtypes)?; + } + 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" { + 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" { + fields.push_str("#[serde(with = \"time::serde::rfc3339::option\")]\n"); + } + if let MaybeRef::Value { value } = &prop_schema { + if let Some(desc) = &value.description { + for line in desc.lines() { + fields.push_str("/// "); + fields.push_str(line); + fields.push_str("\n/// \n"); + } + if fields.ends_with("/// \n") { + fields.truncate(fields.len() - 5); + } + } + } + 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: BTreeMap,\n"); + } + + let mut out = format!("{docs}#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]\npub struct {name} {{\n{fields}}}\n\n"); + for subtype in subtypes { + out.push_str(&subtype); + } + Ok(out) +} + +pub fn create_enum( + name: &str, + desc: Option<&str>, + _enum: &[serde_json::Value], + imp_as_str: bool, +) -> eyre::Result { + let mut variants = String::new(); + let mut imp = String::new(); + imp.push_str("match self {"); + let docs = create_struct_docs_str(desc)?; + for variant in _enum { + match variant { + serde_json::Value::String(s) => { + let variant_name = s.to_pascal_case(); + variants.push_str("#[serde(rename = \""); + variants.push_str(s); + variants.push_str("\")]"); + variants.push_str(&variant_name); + variants.push_str(",\n"); + + writeln!(&mut imp, "{name}::{variant_name} => \"{s}\",")?; + } + x => eyre::bail!("cannot create enum variant. expected string, got {x:?}"), + } + } + imp.push_str("}"); + + let strukt = format!( + " +{docs} +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum {name} {{ + {variants} +}}" + ); + let out = if imp_as_str { + let imp = format!( + "\n\nimpl {name} {{ + fn as_str(&self) -> &'static str {{ + {imp} + }} + }}" + ); + format!("{strukt} {imp}") + } else { + strukt + }; + Ok(out) +} + +fn create_struct_docs(schema: &Schema) -> eyre::Result { + create_struct_docs_str(schema.description.as_deref()) +} + +fn create_struct_docs_str(description: Option<&str>) -> eyre::Result { + let doc = match 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"); + } + if out.ends_with("/// \n") { + out.truncate(out.len() - 5); + } + out + } + None => String::new(), + }; + Ok(doc) +} + +pub fn create_query_structs_for_path(spec: &OpenApiV2, item: &PathItem) -> eyre::Result { + let mut s = String::new(); + if let Some(op) = &item.get { + s.push_str(&create_query_struct(spec, op).wrap_err("GET")?); + } + if let Some(op) = &item.put { + s.push_str(&create_query_struct(spec, op).wrap_err("PUT")?); + } + if let Some(op) = &item.post { + s.push_str(&create_query_struct(spec, op).wrap_err("POST")?); + } + if let Some(op) = &item.delete { + s.push_str(&create_query_struct(spec, op).wrap_err("DELETE")?); + } + if let Some(op) = &item.options { + s.push_str(&create_query_struct(spec, op).wrap_err("OPTIONS")?); + } + if let Some(op) = &item.head { + s.push_str(&create_query_struct(spec, op).wrap_err("HEAD")?); + } + if let Some(op) = &item.patch { + s.push_str(&create_query_struct(spec, op).wrap_err("PATCH")?); + } + Ok(s) +} + +pub fn query_struct_name(op: &Operation) -> eyre::Result { + 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, op: &Operation) -> eyre::Result { + let params = match &op.parameters { + Some(params) => params, + None => return Ok(String::new()), + }; + + let op_name = query_struct_name(op)?; + let mut enums = Vec::new(); + let mut fields = String::new(); + let mut imp = String::new(); + for param in params { + let param = param.deref(spec)?; + if let ParameterIn::Query { param: query_param } = ¶m._in { + let field_name = crate::sanitize_ident(¶m.name); + let ty = match &query_param { + NonBodyParameter { + _type: ParameterType::String, + _enum: Some(_enum), + .. + } => { + let name = format!("{op_name}{}", param.name.to_pascal_case()); + let enum_def = create_enum(&name, None, _enum, true)?; + enums.push(enum_def); + name + } + NonBodyParameter { + _type: ParameterType::Array, + items: + Some(Items { + _type: ParameterType::String, + _enum: Some(_enum), + .. + }), + .. + } => { + let name = format!("{op_name}{}", param.name.to_pascal_case()); + let enum_def = create_enum(&name, None, _enum, true)?; + enums.push(enum_def); + format!("Vec<{name}>") + } + _ => crate::methods::param_type(query_param, true)?, + }; + if let Some(desc) = ¶m.description { + for line in desc.lines() { + fields.push_str("/// "); + fields.push_str(line); + fields.push_str("\n/// \n"); + } + if fields.ends_with("/// \n") { + fields.truncate(fields.len() - 5); + } + } + fields.push_str("pub "); + fields.push_str(&field_name); + fields.push_str(": "); + if query_param.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(); + if query_param.required { + writeln!(&mut handler, "let {field_name} = &self.{field_name};")?; + } else { + writeln!( + &mut handler, + "if let Some({field_name}) = &self.{field_name} {{" + )?; + } + match &query_param._type { + ParameterType::String => { + if let Some(_enum) = &query_param._enum { + writeln!( + &mut handler, + "write!(f, \"{}={{}}&\", {}.as_str())?;", + param.name, field_name, + )?; + } else { + match query_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 = query_param + .collection_format + .unwrap_or(CollectionFormat::Csv); + let item = query_param + .items + .as_ref() + .ok_or_else(|| eyre::eyre!("array must have item type defined"))?; + let item_pusher = match item._type { + ParameterType::String => { + if let Some(_enum) = &item._enum { + "write!(f, \"{}\", item.as_str())?;" + } else { + match query_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 !query_param.required { + writeln!(&mut handler, "}}")?; + } + imp.push_str(&handler); + } + } + + let result = if fields.is_empty() { + String::new() + } else { + let mut out = 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(()) + }} +}} +" + ); + + for _enum in enums { + out.push_str(&_enum); + } + + out + }; + + Ok(result) +} + +fn simple_query_array( + param: &Parameter, + item_pusher: &str, + name: &str, + sep: &str, +) -> eyre::Result { + 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) +} + +fn create_header_struct( + name: &str, + headers: &std::collections::BTreeMap, +) -> eyre::Result { + 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::().map_err(|_| StructureError::HeaderParseFailed)") + } + Some("double") | _ => { + imp.push_str("s.parse::()).map_err(|_| StructureError::HeaderParseFailed)") + } + }, + ParameterType::Integer => match header.format.as_deref() { + Some("int64") => { + imp.push_str("s.parse::().map_err(|_| StructureError::HeaderParseFailed)") + } + Some("int32") | _ => { + imp.push_str("s.parse::().map_err(|_| StructureError::HeaderParseFailed)") + } + }, + ParameterType::Boolean => { + imp.push_str("s.parse::().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::>())", + ParameterType::Number => match items.format.as_deref() { + Some("float") => "s.parse::()).collect::, _>>().map_err(|_| StructureError::HeaderParseFailed)", + Some("double") | _ => "s.parse::()).collect::, _>>().map_err(|_| StructureError::HeaderParseFailed)", + }, + ParameterType::Integer => match items.format.as_deref() { + Some("int64") => "s.parse::()).collect::, _>>().map_err(|_| StructureError::HeaderParseFailed)", + Some("int32") | _ => "s.parse::()).collect::, _>>().map_err(|_| StructureError::HeaderParseFailed)", + }, + ParameterType::Boolean => "s.parse::()).collect::, _>>().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 {{ + {imp} + Ok(Self {{ {imp_ret} }}) + }} +}} +" + )) +} + +pub fn header_type(header: &Header) -> eyre::Result { + crate::methods::param_type_inner( + &header._type, + header.format.as_deref(), + header.items.as_ref(), + true, + ) +} + +pub fn create_response_structs(spec: &OpenApiV2, item: &PathItem) -> eyre::Result { + let mut s = String::new(); + if let Some(op) = &item.get { + s.push_str(&create_response_structs_for_op(spec, op).wrap_err("GET")?); + } + if let Some(op) = &item.put { + s.push_str(&create_response_structs_for_op(spec, op).wrap_err("PUT")?); + } + if let Some(op) = &item.post { + s.push_str(&create_response_structs_for_op(spec, op).wrap_err("POST")?); + } + if let Some(op) = &item.delete { + s.push_str(&create_response_structs_for_op(spec, op).wrap_err("DELETE")?); + } + if let Some(op) = &item.options { + s.push_str(&create_response_structs_for_op(spec, op).wrap_err("OPTIONS")?); + } + if let Some(op) = &item.head { + s.push_str(&create_response_structs_for_op(spec, op).wrap_err("HEAD")?); + } + if let Some(op) = &item.patch { + s.push_str(&create_response_structs_for_op(spec, op).wrap_err("PATCH")?); + } + Ok(s) +} + +pub fn create_response_structs_for_op(spec: &OpenApiV2, op: &Operation) -> eyre::Result { + let mut out = String::new(); + let op_name = op + .operation_id + .as_deref() + .ok_or_else(|| eyre::eyre!("no operation id"))? + .to_pascal_case(); + for (_, response) in &op.responses.http_codes { + let response = response.deref(spec)?; + let tys = create_response_struct(spec, &op_name, response)?; + out.push_str(&tys); + } + Ok(out) +} + +pub fn create_response_struct( + spec: &OpenApiV2, + name: &str, + res: &Response, +) -> eyre::Result { + let mut types = Vec::new(); + if let Some(MaybeRef::Value { value }) = &res.schema { + crate::schema_subtypes(spec, name, "Response", value, &mut types)?; + } + let mut out = String::new(); + for ty in types { + out.push_str(&ty); + } + Ok(out) +} diff --git a/src/admin.rs b/src/admin.rs deleted file mode 100644 index 560d6c6..0000000 --- a/src/admin.rs +++ /dev/null @@ -1,502 +0,0 @@ -use super::*; - -use std::collections::BTreeMap; -use std::fmt::Write; - -impl Forgejo { - pub async fn admin_get_crons(&self, query: CronQuery) -> Result, ForgejoError> { - self.get(&query.path()).await - } - - pub async fn admin_run_cron(&self, name: &str) -> Result<(), ForgejoError> { - self.post_unit(&format!("admin/cron/{name}"), &()).await - } - - pub async fn admin_get_emails( - &self, - query: EmailListQuery, - ) -> Result, ForgejoError> { - self.get(&query.path()).await - } - - pub async fn admin_search_emails( - &self, - query: EmailSearchQuery, - ) -> Result, ForgejoError> { - self.get(&query.path()).await - } - - pub async fn admin_get_hooks(&self, query: HookQuery) -> Result, ForgejoError> { - self.get(&query.path()).await - } - - pub async fn admin_create_hook(&self, opt: CreateHookOption) -> Result { - self.post("admin/hooks", &opt).await - } - - pub async fn admin_get_hook(&self, id: u64) -> Result, ForgejoError> { - self.get_opt(&format!("admin/hooks/{id}")).await - } - - pub async fn admin_delete_hook(&self, id: u64) -> Result<(), ForgejoError> { - self.delete(&format!("admin/hooks/{id}")).await - } - - pub async fn admin_edit_hook( - &self, - id: u64, - opt: EditHookOption, - ) -> Result { - self.patch(&format!("admin/hooks/{id}"), &opt).await - } - - pub async fn admin_get_orgs( - &self, - query: AdminOrganizationQuery, - ) -> Result, ForgejoError> { - self.get(&query.path()).await - } - - pub async fn admin_unadopted_repos( - &self, - query: UnadoptedRepoQuery, - ) -> Result, ForgejoError> { - self.get(&query.path()).await - } - - pub async fn admin_adopt(&self, owner: &str, repo: &str) -> Result<(), ForgejoError> { - self.post(&format!("admin/unadopted/{owner}/{repo}"), &()) - .await - } - - pub async fn admin_delete_unadopted( - &self, - owner: &str, - repo: &str, - ) -> Result<(), ForgejoError> { - self.delete(&format!("admin/unadopted/{owner}/{repo}")) - .await - } - - pub async fn admin_users(&self, query: AdminUserQuery) -> Result, ForgejoError> { - self.get(&query.path()).await - } - - pub async fn admin_create_user(&self, opt: CreateUserOption) -> Result { - self.post("admin/users", &opt).await - } - - pub async fn admin_delete_user(&self, user: &str, purge: bool) -> Result<(), ForgejoError> { - self.delete(&format!("admin/users/{user}?purge={purge}")) - .await - } - - pub async fn admin_edit_user( - &self, - user: &str, - opt: CreateUserOption, - ) -> Result { - self.patch(&format!("admin/users/{user}"), &opt).await - } - - pub async fn admin_add_key( - &self, - user: &str, - opt: CreateKeyOption, - ) -> Result { - self.post(&format!("admin/users/{user}/keys"), &opt).await - } - - pub async fn admin_delete_key(&self, user: &str, id: u64) -> Result<(), ForgejoError> { - self.delete(&format!("admin/users/{user}/keys/{id}")).await - } - - pub async fn admin_create_org( - &self, - owner: &str, - opt: CreateOrgOption, - ) -> Result { - self.post(&format!("admin/users/{owner}/orgs"), &opt).await - } - - pub async fn admin_rename_user( - &self, - user: &str, - opt: RenameUserOption, - ) -> Result<(), ForgejoError> { - self.post_unit(&format!("admin/users/{user}/rename"), &opt) - .await - } - - pub async fn admin_create_repo( - &self, - owner: &str, - opt: CreateRepoOption, - ) -> Result { - self.post(&format!("admin/users/{owner}/repos"), &opt).await - } -} - -#[derive(serde::Deserialize, Debug, PartialEq)] -pub struct Cron { - pub exec_times: u64, - pub name: String, - #[serde(with = "time::serde::rfc3339")] - pub next: time::OffsetDateTime, - #[serde(with = "time::serde::rfc3339")] - pub prev: time::OffsetDateTime, - pub schedule: String, -} - -#[derive(Default, Debug)] -pub struct CronQuery { - pub page: Option, - pub limit: Option, -} - -impl CronQuery { - fn path(&self) -> String { - let mut s = String::from("admin/cron?"); - if let Some(page) = self.page { - s.push_str("page="); - s.write_fmt(format_args!("{page}")) - .expect("writing to string can't fail"); - s.push('&'); - } - if let Some(limit) = self.limit { - s.push_str("limit="); - s.write_fmt(format_args!("{limit}")) - .expect("writing to string can't fail"); - s.push('&'); - } - s - } -} - -#[derive(serde::Deserialize, Debug, PartialEq)] -pub struct Email { - pub email: String, - pub primary: bool, - pub user_id: u64, - pub username: String, - pub verified: bool, -} - -#[derive(Default, Debug)] -pub struct EmailListQuery { - pub page: Option, - pub limit: Option, -} - -impl EmailListQuery { - fn path(&self) -> String { - let mut s = String::from("admin/emails?"); - if let Some(page) = self.page { - s.push_str("page="); - s.write_fmt(format_args!("{page}")) - .expect("writing to string can't fail"); - s.push('&'); - } - if let Some(limit) = self.limit { - s.push_str("limit="); - s.write_fmt(format_args!("{limit}")) - .expect("writing to string can't fail"); - s.push('&'); - } - s - } -} - -#[derive(Default, Debug)] -pub struct EmailSearchQuery { - pub query: String, - pub page: Option, - pub limit: Option, -} - -impl EmailSearchQuery { - fn path(&self) -> String { - let mut s = String::from("admin/emails/search?"); - if !self.query.is_empty() { - s.push_str("q="); - s.push_str(&self.query); - s.push('&'); - } - if let Some(page) = self.page { - s.push_str("page="); - s.write_fmt(format_args!("{page}")) - .expect("writing to string can't fail"); - s.push('&'); - } - if let Some(limit) = self.limit { - s.push_str("limit="); - s.write_fmt(format_args!("{limit}")) - .expect("writing to string can't fail"); - s.push('&'); - } - s - } -} - -#[derive(serde::Deserialize, Debug, PartialEq)] -pub struct Hook { - pub active: bool, - pub authorization_header: String, - pub branch_filter: String, - pub config: std::collections::BTreeMap, - #[serde(with = "time::serde::rfc3339")] - pub created_at: time::OffsetDateTime, - pub events: Vec, - pub id: u64, - #[serde(rename = "type")] - pub _type: HookType, - #[serde(with = "time::serde::rfc3339")] - pub updated_at: time::OffsetDateTime, -} - -#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)] -#[non_exhaustive] -#[serde(rename_all = "lowercase")] -pub enum HookType { - Forgejo, - Dingtalk, - Discord, - Gitea, - Gogs, - Msteams, - Slack, - Telegram, - Feishu, - Wechatwork, - Packagist, -} - -#[derive(Default, Debug)] -pub struct HookQuery { - pub page: Option, - pub limit: Option, -} - -impl HookQuery { - fn path(&self) -> String { - let mut s = String::from("admin/hooks?"); - if let Some(page) = self.page { - s.push_str("page="); - s.write_fmt(format_args!("{page}")) - .expect("writing to string can't fail"); - s.push('&'); - } - if let Some(limit) = self.limit { - s.push_str("limit="); - s.write_fmt(format_args!("{limit}")) - .expect("writing to string can't fail"); - s.push('&'); - } - s - } -} - -#[derive(serde::Serialize, Debug, PartialEq)] -pub struct CreateHookOption { - pub active: Option, - pub authorization_header: Option, - pub branch_filter: Option, - pub config: CreateHookOptionConfig, - pub events: Vec, - #[serde(rename = "type")] - pub _type: HookType, -} - -#[derive(serde::Serialize, Debug, PartialEq)] -pub struct CreateHookOptionConfig { - pub content_type: String, - pub url: Url, - #[serde(flatten)] - pub other: BTreeMap, -} - -#[derive(serde::Serialize, Debug, PartialEq, Default)] -pub struct EditHookOption { - pub active: Option, - pub authorization_header: Option, - pub branch_filter: Option, - pub config: Option>, - pub events: Option>, -} - -#[derive(Default, Debug)] -pub struct AdminOrganizationQuery { - pub page: Option, - pub limit: Option, -} - -impl AdminOrganizationQuery { - fn path(&self) -> String { - let mut s = String::from("admin/orgs?"); - if let Some(page) = self.page { - s.push_str("page="); - s.write_fmt(format_args!("{page}")) - .expect("writing to string can't fail"); - s.push('&'); - } - if let Some(limit) = self.limit { - s.push_str("limit="); - s.write_fmt(format_args!("{limit}")) - .expect("writing to string can't fail"); - s.push('&'); - } - s - } -} - -#[derive(Default, Debug)] -pub struct UnadoptedRepoQuery { - pub page: Option, - pub limit: Option, - pub pattern: String, -} - -impl UnadoptedRepoQuery { - fn path(&self) -> String { - let mut s = String::from("admin/unadopted?"); - if let Some(page) = self.page { - s.push_str("page="); - s.write_fmt(format_args!("{page}")) - .expect("writing to string can't fail"); - s.push('&'); - } - if let Some(limit) = self.limit { - s.push_str("limit="); - s.write_fmt(format_args!("{limit}")) - .expect("writing to string can't fail"); - s.push('&'); - } - if !self.pattern.is_empty() { - s.push_str("pattern="); - s.push_str(&self.pattern); - s.push('&'); - } - s - } -} - -#[derive(Default, Debug)] -pub struct AdminUserQuery { - pub source_id: Option, - pub login_name: String, - pub page: Option, - pub limit: Option, -} - -impl AdminUserQuery { - fn path(&self) -> String { - let mut s = String::from("admin/users?"); - if let Some(source_id) = self.source_id { - s.push_str("source_id="); - s.write_fmt(format_args!("{source_id}")) - .expect("writing to string can't fail"); - s.push('&'); - } - if !self.login_name.is_empty() { - s.push_str("login_name="); - s.push_str(&self.login_name); - s.push('&'); - } - if let Some(page) = self.page { - s.push_str("page="); - s.write_fmt(format_args!("{page}")) - .expect("writing to string can't fail"); - s.push('&'); - } - if let Some(limit) = self.limit { - s.push_str("limit="); - s.write_fmt(format_args!("{limit}")) - .expect("writing to string can't fail"); - s.push('&'); - } - s - } -} - -#[derive(serde::Serialize, Debug, PartialEq)] -pub struct CreateUserOption { - #[serde(with = "time::serde::rfc3339::option")] - pub created_at: Option, - pub email: String, - pub full_name: Option, - pub login_name: Option, - pub must_change_password: bool, - pub password: String, - pub restricted: bool, - pub send_notify: bool, - pub source_id: Option, - pub username: String, - pub visibility: String, -} - -#[derive(serde::Serialize, Debug, PartialEq, Default)] -pub struct EditUserOption { - pub active: Option, - pub admin: Option, - pub allow_create_organization: Option, - pub allow_git_hook: Option, - pub allow_import_local: Option, - pub description: Option, - pub email: Option, - pub full_name: Option, - pub location: Option, - pub login_name: Option, - pub max_repo_creation: Option, - pub must_change_password: Option, - pub password: Option, - pub prohibit_login: Option, - pub restricted: Option, - pub source_id: Option, - pub visibility: Option, - pub website: Option, -} - -#[derive(serde::Serialize, Debug, PartialEq)] -pub struct CreateKeyOption { - pub key: String, - pub read_only: Option, - pub title: String, -} - -#[derive(serde::Deserialize, Debug, PartialEq)] -pub struct PublicKey { - #[serde(with = "time::serde::rfc3339")] - pub created_at: time::OffsetDateTime, - pub fingerprint: String, - pub id: u64, - pub key: String, - pub key_type: String, - pub read_only: Option, - pub title: String, - pub url: Option, - pub user: User, -} - -#[derive(serde::Serialize, Debug, PartialEq)] -pub struct CreateOrgOption { - pub description: Option, - pub full_name: Option, - pub location: Option, - pub repo_admin_change_team_access: Option, - pub username: String, - pub visibility: OrgVisibility, - pub website: Option, -} - -#[derive(serde::Serialize, Debug, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum OrgVisibility { - Public, - Limited, - Private, -} - -#[derive(serde::Serialize, Debug, PartialEq)] -pub struct RenameUserOption { - pub new_username: String, -} diff --git a/src/generated/methods.rs b/src/generated/methods.rs new file mode 100644 index 0000000..0f6a97b --- /dev/null +++ b/src/generated/methods.rs @@ -0,0 +1,7226 @@ +use super::structs::*; +use crate::ForgejoError; +use std::collections::BTreeMap; + +impl crate::Forgejo { + /// Returns the Person actor for a user + /// + /// - `user-id`: user ID of the user + pub async fn activitypub_person(&self, user_id: u32) -> Result { + let request = self + .get(&format!("activitypub/user-id/{user_id}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Send to the inbox + /// + /// - `user-id`: user ID of the user + pub async fn activitypub_person_inbox(&self, user_id: u32) -> Result<(), ForgejoError> { + let request = self + .post(&format!("activitypub/user-id/{user_id}/inbox")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List cron tasks + /// + pub async fn admin_cron_list( + &self, + query: AdminCronListQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("admin/cron?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Run cron task + /// + /// - `task`: task to run + pub async fn admin_cron_run(&self, task: &str) -> Result<(), ForgejoError> { + let request = self.post(&format!("admin/cron/{task}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List all emails + /// + pub async fn admin_get_all_emails( + &self, + query: AdminGetAllEmailsQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("admin/emails?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Search all emails + /// + pub async fn admin_search_emails( + &self, + query: AdminSearchEmailsQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("admin/emails/search?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List system's webhooks + /// + pub async fn admin_list_hooks( + &self, + query: AdminListHooksQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("admin/hooks?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create a hook + /// + /// - `body`: See [`CreateHookOption`] + pub async fn admin_create_hook(&self, body: CreateHookOption) -> Result { + let request = self.post("admin/hooks").json(&body).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a hook + /// + /// - `id`: id of the hook to get + pub async fn admin_get_hook(&self, id: u64) -> Result { + let request = self.get(&format!("admin/hooks/{id}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a hook + /// + /// - `id`: id of the hook to delete + pub async fn admin_delete_hook(&self, id: u64) -> Result<(), ForgejoError> { + let request = self.delete(&format!("admin/hooks/{id}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Update a hook + /// + /// - `id`: id of the hook to update + /// - `body`: See [`EditHookOption`] + pub async fn admin_edit_hook( + &self, + id: u64, + body: EditHookOption, + ) -> Result { + let request = self + .patch(&format!("admin/hooks/{id}")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List all organizations + /// + pub async fn admin_get_all_orgs( + &self, + query: AdminGetAllOrgsQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("admin/orgs?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List unadopted repositories + /// + pub async fn admin_unadopted_list( + &self, + query: AdminUnadoptedListQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("admin/unadopted?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Adopt unadopted files as a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn admin_adopt_repository( + &self, + owner: &str, + repo: &str, + ) -> Result<(), ForgejoError> { + let request = self + .post(&format!("admin/unadopted/{owner}/{repo}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete unadopted files + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn admin_delete_unadopted_repository( + &self, + owner: &str, + repo: &str, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("admin/unadopted/{owner}/{repo}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Search users according filter conditions + /// + pub async fn admin_search_users( + &self, + query: AdminSearchUsersQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("admin/users?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create a user + /// + /// - `body`: See [`CreateUserOption`] + pub async fn admin_create_user(&self, body: CreateUserOption) -> Result { + let request = self.post("admin/users").json(&body).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a user + /// + /// - `username`: username of user to delete + pub async fn admin_delete_user( + &self, + username: &str, + query: AdminDeleteUserQuery, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("admin/users/{username}?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Edit an existing user + /// + /// - `username`: username of user to edit + /// - `body`: See [`EditUserOption`] + pub async fn admin_edit_user( + &self, + username: &str, + body: EditUserOption, + ) -> Result { + let request = self + .patch(&format!("admin/users/{username}")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Add a public key on behalf of a user + /// + /// - `username`: username of the user + /// - `key`: See [`CreateKeyOption`] + pub async fn admin_create_public_key( + &self, + username: &str, + key: CreateKeyOption, + ) -> Result { + let request = self + .post(&format!("admin/users/{username}/keys")) + .json(&key) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a user's public key + /// + /// - `username`: username of user + /// - `id`: id of the key to delete + pub async fn admin_delete_user_public_key( + &self, + username: &str, + id: u64, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("admin/users/{username}/keys/{id}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create an organization + /// + /// - `username`: username of the user that will own the created organization + /// - `organization`: See [`CreateOrgOption`] + pub async fn admin_create_org( + &self, + username: &str, + organization: CreateOrgOption, + ) -> Result { + let request = self + .post(&format!("admin/users/{username}/orgs")) + .json(&organization) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Rename a user + /// + /// - `username`: existing username of user + /// - `body`: See [`RenameUserOption`] + pub async fn admin_rename_user( + &self, + username: &str, + body: RenameUserOption, + ) -> Result<(), ForgejoError> { + let request = self + .post(&format!("admin/users/{username}/rename")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create a repository on behalf of a user + /// + /// - `username`: username of the user. This user will own the created repository + /// - `repository`: See [`CreateRepoOption`] + pub async fn admin_create_repo( + &self, + username: &str, + repository: CreateRepoOption, + ) -> Result { + let request = self + .post(&format!("admin/users/{username}/repos")) + .json(&repository) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Returns a list of all gitignore templates + pub async fn list_gitignores_templates(&self) -> Result, ForgejoError> { + let request = self.get("gitignore/templates").build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Returns information about a gitignore template + /// + /// - `name`: name of the template + pub async fn get_gitignore_template_info( + &self, + name: &str, + ) -> Result { + let request = self.get(&format!("gitignore/templates/{name}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Returns a list of all label templates + pub async fn list_label_templates(&self) -> Result, ForgejoError> { + let request = self.get("label/templates").build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Returns all labels in a template + /// + /// - `name`: name of the template + pub async fn get_label_template_info( + &self, + name: &str, + ) -> Result, ForgejoError> { + let request = self.get(&format!("label/templates/{name}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Returns a list of all license templates + pub async fn list_license_templates( + &self, + ) -> Result, ForgejoError> { + let request = self.get("licenses").build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Returns information about a license template + /// + /// - `name`: name of the license + pub async fn get_license_template_info( + &self, + name: &str, + ) -> Result { + let request = self.get(&format!("licenses/{name}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Render a markdown document as HTML + /// + /// - `body`: See [`MarkdownOption`] + pub async fn render_markdown(&self, body: MarkdownOption) -> Result { + let request = self.post("markdown").json(&body).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.text().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Render raw markdown as HTML + /// + /// - `body`: Request body to render + + /// See [`String`] + pub async fn render_markdown_raw(&self, body: String) -> Result { + let request = self.post("markdown/raw").body(body).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.text().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Render a markup document as HTML + /// + /// - `body`: See [`MarkupOption`] + pub async fn render_markup(&self, body: MarkupOption) -> Result { + let request = self.post("markup").json(&body).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.text().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Returns the nodeinfo of the Gitea application + pub async fn get_node_info(&self) -> Result { + let request = self.get("nodeinfo").build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List users's notification threads + /// + pub async fn notify_get_list( + &self, + query: NotifyGetListQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("notifications?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Mark notification threads as read, pinned or unread + /// + pub async fn notify_read_list( + &self, + query: NotifyReadListQuery, + ) -> Result, ForgejoError> { + let request = self.put(&format!("notifications?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 205 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Check if unread notifications exist + pub async fn notify_new_available(&self) -> Result { + let request = self.get("notifications/new").build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get notification thread by ID + /// + /// - `id`: id of notification thread + pub async fn notify_get_thread(&self, id: &str) -> Result { + let request = self.get(&format!("notifications/threads/{id}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Mark notification thread as read by ID + /// + /// - `id`: id of notification thread + pub async fn notify_read_thread( + &self, + id: &str, + query: NotifyReadThreadQuery, + ) -> Result { + let request = self + .patch(&format!("notifications/threads/{id}?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 205 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create a repository in an organization + /// + /// - `org`: name of organization + /// - `body`: See [`CreateRepoOption`] + pub async fn create_org_repo_deprecated( + &self, + org: &str, + body: CreateRepoOption, + ) -> Result { + let request = self.post(&format!("org/{org}/repos")).json(&body).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get list of organizations + /// + pub async fn org_get_all( + &self, + query: OrgGetAllQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("orgs?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create an organization + /// + /// - `organization`: See [`CreateOrgOption`] + pub async fn org_create( + &self, + organization: CreateOrgOption, + ) -> Result { + let request = self.post("orgs").json(&organization).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get an organization + /// + /// - `org`: name of the organization to get + pub async fn org_get(&self, org: &str) -> Result { + let request = self.get(&format!("orgs/{org}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete an organization + /// + /// - `org`: organization that is to be deleted + pub async fn org_delete(&self, org: &str) -> Result<(), ForgejoError> { + let request = self.delete(&format!("orgs/{org}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Edit an organization + /// + /// - `org`: name of the organization to edit + /// - `body`: See [`EditOrgOption`] + pub async fn org_edit( + &self, + org: &str, + body: EditOrgOption, + ) -> Result { + let request = self.patch(&format!("orgs/{org}")).json(&body).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List an organization's actions secrets + /// + /// - `org`: name of the organization + pub async fn org_list_actions_secrets( + &self, + org: &str, + query: OrgListActionsSecretsQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("orgs/{org}/actions/secrets?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create or Update a secret value in an organization + /// + /// - `org`: name of organization + /// - `secretname`: name of the secret + /// - `body`: See [`CreateOrUpdateSecretOption`] + pub async fn update_org_secret( + &self, + org: &str, + secretname: &str, + body: CreateOrUpdateSecretOption, + ) -> Result<(), ForgejoError> { + let request = self + .put(&format!("orgs/{org}/actions/secrets/{secretname}")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(()), + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a secret in an organization + /// + /// - `org`: name of organization + /// - `secretname`: name of the secret + pub async fn delete_org_secret(&self, org: &str, secretname: &str) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("orgs/{org}/actions/secrets/{secretname}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List an organization's activity feeds + /// + /// - `org`: name of the org + pub async fn org_list_activity_feeds( + &self, + org: &str, + query: OrgListActivityFeedsQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("orgs/{org}/activities/feeds?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Update Avatar + /// + /// - `org`: name of the organization + /// - `body`: See [`UpdateUserAvatarOption`] + pub async fn org_update_avatar( + &self, + org: &str, + body: UpdateUserAvatarOption, + ) -> Result<(), ForgejoError> { + let request = self + .post(&format!("orgs/{org}/avatar")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete Avatar + /// + /// - `org`: name of the organization + pub async fn org_delete_avatar(&self, org: &str) -> Result<(), ForgejoError> { + let request = self.delete(&format!("orgs/{org}/avatar")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Blocks a user from the organization + /// + /// - `org`: name of the org + /// - `username`: username of the user + pub async fn org_block_user(&self, org: &str, username: &str) -> Result<(), ForgejoError> { + let request = self.put(&format!("orgs/{org}/block/{username}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List an organization's webhooks + /// + /// - `org`: name of the organization + pub async fn org_list_hooks( + &self, + org: &str, + query: OrgListHooksQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("orgs/{org}/hooks?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create a hook + /// + /// - `org`: name of the organization + /// - `body`: See [`CreateHookOption`] + pub async fn org_create_hook( + &self, + org: &str, + body: CreateHookOption, + ) -> Result { + let request = self + .post(&format!("orgs/{org}/hooks")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a hook + /// + /// - `org`: name of the organization + /// - `id`: id of the hook to get + pub async fn org_get_hook(&self, org: &str, id: u64) -> Result { + let request = self.get(&format!("orgs/{org}/hooks/{id}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a hook + /// + /// - `org`: name of the organization + /// - `id`: id of the hook to delete + pub async fn org_delete_hook(&self, org: &str, id: u64) -> Result<(), ForgejoError> { + let request = self.delete(&format!("orgs/{org}/hooks/{id}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Update a hook + /// + /// - `org`: name of the organization + /// - `id`: id of the hook to update + /// - `body`: See [`EditHookOption`] + pub async fn org_edit_hook( + &self, + org: &str, + id: u64, + body: EditHookOption, + ) -> Result { + let request = self + .patch(&format!("orgs/{org}/hooks/{id}")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List an organization's labels + /// + /// - `org`: name of the organization + pub async fn org_list_labels( + &self, + org: &str, + query: OrgListLabelsQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("orgs/{org}/labels?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create a label for an organization + /// + /// - `org`: name of the organization + /// - `body`: See [`CreateLabelOption`] + pub async fn org_create_label( + &self, + org: &str, + body: CreateLabelOption, + ) -> Result { + let request = self + .post(&format!("orgs/{org}/labels")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a single label + /// + /// - `org`: name of the organization + /// - `id`: id of the label to get + pub async fn org_get_label(&self, org: &str, id: u64) -> Result { + let request = self.get(&format!("orgs/{org}/labels/{id}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a label + /// + /// - `org`: name of the organization + /// - `id`: id of the label to delete + pub async fn org_delete_label(&self, org: &str, id: u64) -> Result<(), ForgejoError> { + let request = self.delete(&format!("orgs/{org}/labels/{id}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Update a label + /// + /// - `org`: name of the organization + /// - `id`: id of the label to edit + /// - `body`: See [`EditLabelOption`] + pub async fn org_edit_label( + &self, + org: &str, + id: u64, + body: EditLabelOption, + ) -> Result { + let request = self + .patch(&format!("orgs/{org}/labels/{id}")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List the organization's blocked users + /// + /// - `org`: name of the org + pub async fn org_list_blocked_users( + &self, + org: &str, + query: OrgListBlockedUsersQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("orgs/{org}/list_blocked?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List an organization's members + /// + /// - `org`: name of the organization + pub async fn org_list_members( + &self, + org: &str, + query: OrgListMembersQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("orgs/{org}/members?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Check if a user is a member of an organization + /// + /// - `org`: name of the organization + /// - `username`: username of the user + pub async fn org_is_member(&self, org: &str, username: &str) -> Result<(), ForgejoError> { + let request = self + .get(&format!("orgs/{org}/members/{username}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Remove a member from an organization + /// + /// - `org`: name of the organization + /// - `username`: username of the user + pub async fn org_delete_member(&self, org: &str, username: &str) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("orgs/{org}/members/{username}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List an organization's public members + /// + /// - `org`: name of the organization + pub async fn org_list_public_members( + &self, + org: &str, + query: OrgListPublicMembersQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("orgs/{org}/public_members?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Check if a user is a public member of an organization + /// + /// - `org`: name of the organization + /// - `username`: username of the user + pub async fn org_is_public_member( + &self, + org: &str, + username: &str, + ) -> Result<(), ForgejoError> { + let request = self + .get(&format!("orgs/{org}/public_members/{username}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Publicize a user's membership + /// + /// - `org`: name of the organization + /// - `username`: username of the user + pub async fn org_publicize_member( + &self, + org: &str, + username: &str, + ) -> Result<(), ForgejoError> { + let request = self + .put(&format!("orgs/{org}/public_members/{username}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Conceal a user's membership + /// + /// - `org`: name of the organization + /// - `username`: username of the user + pub async fn org_conceal_member(&self, org: &str, username: &str) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("orgs/{org}/public_members/{username}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List an organization's repos + /// + /// - `org`: name of the organization + pub async fn org_list_repos( + &self, + org: &str, + query: OrgListReposQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("orgs/{org}/repos?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create a repository in an organization + /// + /// - `org`: name of organization + /// - `body`: See [`CreateRepoOption`] + pub async fn create_org_repo( + &self, + org: &str, + body: CreateRepoOption, + ) -> Result { + let request = self + .post(&format!("orgs/{org}/repos")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List an organization's teams + /// + /// - `org`: name of the organization + pub async fn org_list_teams( + &self, + org: &str, + query: OrgListTeamsQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("orgs/{org}/teams?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create a team + /// + /// - `org`: name of the organization + /// - `body`: See [`CreateTeamOption`] + pub async fn org_create_team( + &self, + org: &str, + body: CreateTeamOption, + ) -> Result { + let request = self + .post(&format!("orgs/{org}/teams")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Search for teams within an organization + /// + /// - `org`: name of the organization + pub async fn team_search( + &self, + org: &str, + query: TeamSearchQuery, + ) -> Result { + let request = self + .get(&format!("orgs/{org}/teams/search?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Unblock a user from the organization + /// + /// - `org`: name of the org + /// - `username`: username of the user + pub async fn org_unblock_user(&self, org: &str, username: &str) -> Result<(), ForgejoError> { + let request = self + .put(&format!("orgs/{org}/unblock/{username}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Gets all packages of an owner + /// + /// - `owner`: owner of the packages + pub async fn list_packages( + &self, + owner: &str, + query: ListPackagesQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("packages/{owner}?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Gets a package + /// + /// - `owner`: owner of the package + /// - `type`: type of the package + /// - `name`: name of the package + /// - `version`: version of the package + pub async fn get_package( + &self, + owner: &str, + r#type: &str, + name: &str, + version: &str, + ) -> Result { + let request = self + .get(&format!("packages/{owner}/{type}/{name}/{version}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a package + /// + /// - `owner`: owner of the package + /// - `type`: type of the package + /// - `name`: name of the package + /// - `version`: version of the package + pub async fn delete_package( + &self, + owner: &str, + r#type: &str, + name: &str, + version: &str, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("packages/{owner}/{type}/{name}/{version}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Gets all files of a package + /// + /// - `owner`: owner of the package + /// - `type`: type of the package + /// - `name`: name of the package + /// - `version`: version of the package + pub async fn list_package_files( + &self, + owner: &str, + r#type: &str, + name: &str, + version: &str, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("packages/{owner}/{type}/{name}/{version}/files")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Search for issues across the repositories that the user has access to + /// + pub async fn issue_search_issues( + &self, + query: IssueSearchIssuesQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("repos/issues/search?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Migrate a remote git repository + /// + /// - `body`: See [`MigrateRepoOptions`] + pub async fn repo_migrate(&self, body: MigrateRepoOptions) -> Result { + let request = self.post("repos/migrate").json(&body).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Search for repositories + /// + pub async fn repo_search(&self, query: RepoSearchQuery) -> Result { + let request = self.get(&format!("repos/search?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_get(&self, owner: &str, repo: &str) -> Result { + let request = self.get(&format!("repos/{owner}/{repo}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a repository + /// + /// - `owner`: owner of the repo to delete + /// - `repo`: name of the repo to delete + pub async fn repo_delete(&self, owner: &str, repo: &str) -> Result<(), ForgejoError> { + let request = self.delete(&format!("repos/{owner}/{repo}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Edit a repository's properties. Only fields that are set will be changed. + /// + /// - `owner`: owner of the repo to edit + /// - `repo`: name of the repo to edit + /// - `body`: Properties of a repo that you can edit + + /// See [`EditRepoOption`] + pub async fn repo_edit( + &self, + owner: &str, + repo: &str, + body: EditRepoOption, + ) -> Result { + let request = self + .patch(&format!("repos/{owner}/{repo}")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create or Update a secret value in a repository + /// + /// - `owner`: owner of the repository + /// - `repo`: name of the repository + /// - `secretname`: name of the secret + /// - `body`: See [`CreateOrUpdateSecretOption`] + pub async fn update_repo_secret( + &self, + owner: &str, + repo: &str, + secretname: &str, + body: CreateOrUpdateSecretOption, + ) -> Result<(), ForgejoError> { + let request = self + .put(&format!( + "repos/{owner}/{repo}/actions/secrets/{secretname}" + )) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(()), + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a secret in a repository + /// + /// - `owner`: owner of the repository + /// - `repo`: name of the repository + /// - `secretname`: name of the secret + pub async fn delete_repo_secret( + &self, + owner: &str, + repo: &str, + secretname: &str, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!( + "repos/{owner}/{repo}/actions/secrets/{secretname}" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List a repository's activity feeds + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_list_activity_feeds( + &self, + owner: &str, + repo: &str, + query: RepoListActivityFeedsQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/activities/feeds?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get an archive of a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `archive`: the git reference for download with attached archive format (e.g. master.zip) + pub async fn repo_get_archive( + &self, + owner: &str, + repo: &str, + archive: &str, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/archive/{archive}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.bytes().await?[..].to_vec()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Return all users that have write access and can be assigned to issues + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_get_assignees( + &self, + owner: &str, + repo: &str, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/assignees")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Update avatar + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `body`: See [`UpdateRepoAvatarOption`] + pub async fn repo_update_avatar( + &self, + owner: &str, + repo: &str, + body: UpdateRepoAvatarOption, + ) -> Result<(), ForgejoError> { + let request = self + .post(&format!("repos/{owner}/{repo}/avatar")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete avatar + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_delete_avatar(&self, owner: &str, repo: &str) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("repos/{owner}/{repo}/avatar")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List branch protections for a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_list_branch_protection( + &self, + owner: &str, + repo: &str, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/branch_protections")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create a branch protections for a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `body`: See [`CreateBranchProtectionOption`] + pub async fn repo_create_branch_protection( + &self, + owner: &str, + repo: &str, + body: CreateBranchProtectionOption, + ) -> Result { + let request = self + .post(&format!("repos/{owner}/{repo}/branch_protections")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a specific branch protection for the repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `name`: name of protected branch + pub async fn repo_get_branch_protection( + &self, + owner: &str, + repo: &str, + name: &str, + ) -> Result { + let request = self + .get(&format!("repos/{owner}/{repo}/branch_protections/{name}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a specific branch protection for the repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `name`: name of protected branch + pub async fn repo_delete_branch_protection( + &self, + owner: &str, + repo: &str, + name: &str, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("repos/{owner}/{repo}/branch_protections/{name}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Edit a branch protections for a repository. Only fields that are set will be changed + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `name`: name of protected branch + /// - `body`: See [`EditBranchProtectionOption`] + pub async fn repo_edit_branch_protection( + &self, + owner: &str, + repo: &str, + name: &str, + body: EditBranchProtectionOption, + ) -> Result { + let request = self + .patch(&format!("repos/{owner}/{repo}/branch_protections/{name}")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List a repository's branches + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_list_branches( + &self, + owner: &str, + repo: &str, + query: RepoListBranchesQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/branches?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create a branch + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `body`: See [`CreateBranchRepoOption`] + pub async fn repo_create_branch( + &self, + owner: &str, + repo: &str, + body: CreateBranchRepoOption, + ) -> Result { + let request = self + .post(&format!("repos/{owner}/{repo}/branches")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Retrieve a specific branch from a repository, including its effective branch protection + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `branch`: branch to get + pub async fn repo_get_branch( + &self, + owner: &str, + repo: &str, + branch: &str, + ) -> Result { + let request = self + .get(&format!("repos/{owner}/{repo}/branches/{branch}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a specific branch from a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `branch`: branch to delete + pub async fn repo_delete_branch( + &self, + owner: &str, + repo: &str, + branch: &str, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("repos/{owner}/{repo}/branches/{branch}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List a repository's collaborators + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_list_collaborators( + &self, + owner: &str, + repo: &str, + query: RepoListCollaboratorsQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/collaborators?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Check if a user is a collaborator of a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `collaborator`: username of the collaborator + pub async fn repo_check_collaborator( + &self, + owner: &str, + repo: &str, + collaborator: &str, + ) -> Result<(), ForgejoError> { + let request = self + .get(&format!( + "repos/{owner}/{repo}/collaborators/{collaborator}" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Add a collaborator to a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `collaborator`: username of the collaborator to add + /// - `body`: See [`AddCollaboratorOption`] + pub async fn repo_add_collaborator( + &self, + owner: &str, + repo: &str, + collaborator: &str, + body: AddCollaboratorOption, + ) -> Result<(), ForgejoError> { + let request = self + .put(&format!( + "repos/{owner}/{repo}/collaborators/{collaborator}" + )) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a collaborator from a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `collaborator`: username of the collaborator to delete + pub async fn repo_delete_collaborator( + &self, + owner: &str, + repo: &str, + collaborator: &str, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!( + "repos/{owner}/{repo}/collaborators/{collaborator}" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get repository permissions for a user + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `collaborator`: username of the collaborator + pub async fn repo_get_repo_permissions( + &self, + owner: &str, + repo: &str, + collaborator: &str, + ) -> Result { + let request = self + .get(&format!( + "repos/{owner}/{repo}/collaborators/{collaborator}/permission" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a list of all commits from a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_get_all_commits( + &self, + owner: &str, + repo: &str, + query: RepoGetAllCommitsQuery, + ) -> Result<(CommitListHeaders, Vec), 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().try_into()?, response.json().await?)), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a commit's combined status, by branch/tag/commit reference + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `ref`: name of branch/tag/commit + pub async fn repo_get_combined_status_by_ref( + &self, + owner: &str, + repo: &str, + r#ref: &str, + query: RepoGetCombinedStatusByRefQuery, + ) -> Result { + let request = self + .get(&format!( + "repos/{owner}/{repo}/commits/{ref}/status?{query}" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a commit's statuses, by branch/tag/commit reference + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `ref`: name of branch/tag/commit + pub async fn repo_list_statuses_by_ref( + &self, + owner: &str, + repo: &str, + r#ref: &str, + query: RepoListStatusesByRefQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!( + "repos/{owner}/{repo}/commits/{ref}/statuses?{query}" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Gets the metadata of all the entries of the root dir + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_get_contents_list( + &self, + owner: &str, + repo: &str, + query: RepoGetContentsListQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/contents?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Modify multiple files in a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `body`: See [`ChangeFilesOptions`] + pub async fn repo_change_files( + &self, + owner: &str, + repo: &str, + body: ChangeFilesOptions, + ) -> Result { + let request = self + .post(&format!("repos/{owner}/{repo}/contents")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Gets the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `filepath`: path of the dir, file, symlink or submodule in the repo + pub async fn repo_get_contents( + &self, + owner: &str, + repo: &str, + filepath: &str, + query: RepoGetContentsQuery, + ) -> Result { + let request = self + .get(&format!("repos/{owner}/{repo}/contents/{filepath}?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Update a file in a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `filepath`: path of the file to update + /// - `body`: See [`UpdateFileOptions`] + pub async fn repo_update_file( + &self, + owner: &str, + repo: &str, + filepath: &str, + body: UpdateFileOptions, + ) -> Result { + let request = self + .put(&format!("repos/{owner}/{repo}/contents/{filepath}")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create a file in a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `filepath`: path of the file to create + /// - `body`: See [`CreateFileOptions`] + pub async fn repo_create_file( + &self, + owner: &str, + repo: &str, + filepath: &str, + body: CreateFileOptions, + ) -> Result { + let request = self + .post(&format!("repos/{owner}/{repo}/contents/{filepath}")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a file in a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `filepath`: path of the file to delete + /// - `body`: See [`DeleteFileOptions`] + pub async fn repo_delete_file( + &self, + owner: &str, + repo: &str, + filepath: &str, + body: DeleteFileOptions, + ) -> Result { + let request = self + .delete(&format!("repos/{owner}/{repo}/contents/{filepath}")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Apply diff patch to repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `body`: See [`UpdateFileOptions`] + pub async fn repo_apply_diff_patch( + &self, + owner: &str, + repo: &str, + body: UpdateFileOptions, + ) -> Result { + let request = self + .post(&format!("repos/{owner}/{repo}/diffpatch")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get the EditorConfig definitions of a file in a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `filepath`: filepath of file to get + pub async fn repo_get_editor_config( + &self, + owner: &str, + repo: &str, + filepath: &str, + query: RepoGetEditorConfigQuery, + ) -> Result<(), ForgejoError> { + let request = self + .get(&format!( + "repos/{owner}/{repo}/editorconfig/{filepath}?{query}" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List a repository's forks + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn list_forks( + &self, + owner: &str, + repo: &str, + query: ListForksQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/forks?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Fork a repository + /// + /// - `owner`: owner of the repo to fork + /// - `repo`: name of the repo to fork + /// - `body`: See [`CreateForkOption`] + pub async fn create_fork( + &self, + owner: &str, + repo: &str, + body: CreateForkOption, + ) -> Result { + let request = self + .post(&format!("repos/{owner}/{repo}/forks")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 202 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Gets the blob of a repository. + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `sha`: sha of the commit + pub async fn get_blob( + &self, + owner: &str, + repo: &str, + sha: &str, + ) -> Result { + let request = self + .get(&format!("repos/{owner}/{repo}/git/blobs/{sha}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a single commit from a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `sha`: a git ref or commit sha + pub async fn repo_get_single_commit( + &self, + owner: &str, + repo: &str, + sha: &str, + query: RepoGetSingleCommitQuery, + ) -> Result { + let request = self + .get(&format!("repos/{owner}/{repo}/git/commits/{sha}?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a commit's diff or patch + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `sha`: SHA of the commit to get + /// - `diffType`: whether the output is diff or patch + pub async fn repo_download_commit_diff_or_patch( + &self, + owner: &str, + repo: &str, + sha: &str, + diff_type: &str, + ) -> Result { + let request = self + .get(&format!( + "repos/{owner}/{repo}/git/commits/{sha}.{diff_type}" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.text().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a note corresponding to a single commit from a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `sha`: a git ref or commit sha + pub async fn repo_get_note( + &self, + owner: &str, + repo: &str, + sha: &str, + query: RepoGetNoteQuery, + ) -> Result { + let request = self + .get(&format!("repos/{owner}/{repo}/git/notes/{sha}?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get specified ref or filtered repository's refs + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_list_all_git_refs( + &self, + owner: &str, + repo: &str, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/git/refs")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get specified ref or filtered repository's refs + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `ref`: part or full name of the ref + pub async fn repo_list_git_refs( + &self, + owner: &str, + repo: &str, + r#ref: &str, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/git/refs/{ref}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Gets the tag object of an annotated tag (not lightweight tags) + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `sha`: sha of the tag. The Git tags API only supports annotated tag objects, not lightweight tags. + pub async fn get_annotated_tag( + &self, + owner: &str, + repo: &str, + sha: &str, + ) -> Result { + let request = self + .get(&format!("repos/{owner}/{repo}/git/tags/{sha}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Gets the tree of a repository. + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `sha`: sha of the commit + pub async fn get_tree( + &self, + owner: &str, + repo: &str, + sha: &str, + query: GetTreeQuery, + ) -> Result { + let request = self + .get(&format!("repos/{owner}/{repo}/git/trees/{sha}?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List the hooks in a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_list_hooks( + &self, + owner: &str, + repo: &str, + query: RepoListHooksQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/hooks?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create a hook + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `body`: See [`CreateHookOption`] + pub async fn repo_create_hook( + &self, + owner: &str, + repo: &str, + body: CreateHookOption, + ) -> Result { + let request = self + .post(&format!("repos/{owner}/{repo}/hooks")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List the Git hooks in a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_list_git_hooks( + &self, + owner: &str, + repo: &str, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/hooks/git")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a Git hook + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: id of the hook to get + pub async fn repo_get_git_hook( + &self, + owner: &str, + repo: &str, + id: &str, + ) -> Result { + let request = self + .get(&format!("repos/{owner}/{repo}/hooks/git/{id}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a Git hook in a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: id of the hook to get + pub async fn repo_delete_git_hook( + &self, + owner: &str, + repo: &str, + id: &str, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("repos/{owner}/{repo}/hooks/git/{id}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Edit a Git hook in a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: id of the hook to get + /// - `body`: See [`EditGitHookOption`] + pub async fn repo_edit_git_hook( + &self, + owner: &str, + repo: &str, + id: &str, + body: EditGitHookOption, + ) -> Result { + let request = self + .patch(&format!("repos/{owner}/{repo}/hooks/git/{id}")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a hook + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: id of the hook to get + pub async fn repo_get_hook( + &self, + owner: &str, + repo: &str, + id: u64, + ) -> Result { + let request = self + .get(&format!("repos/{owner}/{repo}/hooks/{id}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a hook in a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: id of the hook to delete + pub async fn repo_delete_hook( + &self, + owner: &str, + repo: &str, + id: u64, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("repos/{owner}/{repo}/hooks/{id}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Edit a hook in a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: index of the hook + /// - `body`: See [`EditHookOption`] + pub async fn repo_edit_hook( + &self, + owner: &str, + repo: &str, + id: u64, + body: EditHookOption, + ) -> Result { + let request = self + .patch(&format!("repos/{owner}/{repo}/hooks/{id}")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Test a push webhook + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: id of the hook to test + pub async fn repo_test_hook( + &self, + owner: &str, + repo: &str, + id: u64, + query: RepoTestHookQuery, + ) -> Result<(), ForgejoError> { + let request = self + .post(&format!("repos/{owner}/{repo}/hooks/{id}/tests?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Returns the issue config for a repo + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_get_issue_config( + &self, + owner: &str, + repo: &str, + ) -> Result { + let request = self + .get(&format!("repos/{owner}/{repo}/issue_config")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Returns the validation information for a issue config + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_validate_issue_config( + &self, + owner: &str, + repo: &str, + ) -> Result { + let request = self + .get(&format!("repos/{owner}/{repo}/issue_config/validate")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get available issue templates for a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_get_issue_templates( + &self, + owner: &str, + repo: &str, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/issue_templates")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List a repository's issues + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn issue_list_issues( + &self, + owner: &str, + repo: &str, + query: IssueListIssuesQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/issues?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create an issue. If using deadline only the date will be taken into account, and time of day ignored. + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `body`: See [`CreateIssueOption`] + pub async fn issue_create_issue( + &self, + owner: &str, + repo: &str, + body: CreateIssueOption, + ) -> Result { + let request = self + .post(&format!("repos/{owner}/{repo}/issues")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List all comments in a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn issue_get_repo_comments( + &self, + owner: &str, + repo: &str, + query: IssueGetRepoCommentsQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/issues/comments?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a comment + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: id of the comment + pub async fn issue_get_comment( + &self, + owner: &str, + repo: &str, + id: u64, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/issues/comments/{id}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(Some(response.json().await?)), + 204 => Ok(Some(response.json().await?)), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a comment + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: id of comment to delete + pub async fn issue_delete_comment( + &self, + owner: &str, + repo: &str, + id: u64, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("repos/{owner}/{repo}/issues/comments/{id}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Edit a comment + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: id of the comment to edit + /// - `body`: See [`EditIssueCommentOption`] + pub async fn issue_edit_comment( + &self, + owner: &str, + repo: &str, + id: u64, + body: EditIssueCommentOption, + ) -> Result, ForgejoError> { + let request = self + .patch(&format!("repos/{owner}/{repo}/issues/comments/{id}")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(Some(response.json().await?)), + 204 => Ok(Some(response.json().await?)), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List comment's attachments + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: id of the comment + pub async fn issue_list_issue_comment_attachments( + &self, + owner: &str, + repo: &str, + id: u64, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/issues/comments/{id}/assets")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create a comment attachment + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: id of the comment + /// - `attachment`: attachment to upload + pub async fn issue_create_issue_comment_attachment( + &self, + owner: &str, + repo: &str, + id: u64, + attachment: Vec, + query: IssueCreateIssueCommentAttachmentQuery, + ) -> Result { + let request = self + .post(&format!( + "repos/{owner}/{repo}/issues/comments/{id}/assets?{query}" + )) + .multipart( + reqwest::multipart::Form::new().part( + "attachment", + reqwest::multipart::Part::bytes(attachment) + .file_name("file") + .mime_str("*/*") + .unwrap(), + ), + ) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a comment attachment + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: id of the comment + /// - `attachment_id`: id of the attachment to get + pub async fn issue_get_issue_comment_attachment( + &self, + owner: &str, + repo: &str, + id: u64, + attachment_id: u64, + ) -> Result { + let request = self + .get(&format!( + "repos/{owner}/{repo}/issues/comments/{id}/assets/{attachment_id}" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a comment attachment + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: id of the comment + /// - `attachment_id`: id of the attachment to delete + pub async fn issue_delete_issue_comment_attachment( + &self, + owner: &str, + repo: &str, + id: u64, + attachment_id: u64, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!( + "repos/{owner}/{repo}/issues/comments/{id}/assets/{attachment_id}" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Edit a comment attachment + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: id of the comment + /// - `attachment_id`: id of the attachment to edit + /// - `body`: See [`EditAttachmentOptions`] + pub async fn issue_edit_issue_comment_attachment( + &self, + owner: &str, + repo: &str, + id: u64, + attachment_id: u64, + body: EditAttachmentOptions, + ) -> Result { + let request = self + .patch(&format!( + "repos/{owner}/{repo}/issues/comments/{id}/assets/{attachment_id}" + )) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a list of reactions from a comment of an issue + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: id of the comment to edit + pub async fn issue_get_comment_reactions( + &self, + owner: &str, + repo: &str, + id: u64, + ) -> Result, ForgejoError> { + let request = self + .get(&format!( + "repos/{owner}/{repo}/issues/comments/{id}/reactions" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Add a reaction to a comment of an issue + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: id of the comment to edit + /// - `content`: See [`EditReactionOption`] + pub async fn issue_post_comment_reaction( + &self, + owner: &str, + repo: &str, + id: u64, + content: EditReactionOption, + ) -> Result { + let request = self + .post(&format!( + "repos/{owner}/{repo}/issues/comments/{id}/reactions" + )) + .json(&content) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Remove a reaction from a comment of an issue + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: id of the comment to edit + /// - `content`: See [`EditReactionOption`] + pub async fn issue_delete_comment_reaction( + &self, + owner: &str, + repo: &str, + id: u64, + content: EditReactionOption, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!( + "repos/{owner}/{repo}/issues/comments/{id}/reactions" + )) + .json(&content) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List a repo's pinned issues + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_list_pinned_issues( + &self, + owner: &str, + repo: &str, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/issues/pinned")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get an issue + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue to get + pub async fn issue_get_issue( + &self, + owner: &str, + repo: &str, + index: u64, + ) -> Result { + let request = self + .get(&format!("repos/{owner}/{repo}/issues/{index}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete an issue + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of issue to delete + pub async fn issue_delete( + &self, + owner: &str, + repo: &str, + index: u64, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("repos/{owner}/{repo}/issues/{index}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Edit an issue. If using deadline only the date will be taken into account, and time of day ignored. + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue to edit + /// - `body`: See [`EditIssueOption`] + pub async fn issue_edit_issue( + &self, + owner: &str, + repo: &str, + index: u64, + body: EditIssueOption, + ) -> Result { + let request = self + .patch(&format!("repos/{owner}/{repo}/issues/{index}")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List issue's attachments + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue + pub async fn issue_list_issue_attachments( + &self, + owner: &str, + repo: &str, + index: u64, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/issues/{index}/assets")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create an issue attachment + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue + /// - `attachment`: attachment to upload + pub async fn issue_create_issue_attachment( + &self, + owner: &str, + repo: &str, + index: u64, + attachment: Vec, + query: IssueCreateIssueAttachmentQuery, + ) -> Result { + let request = self + .post(&format!( + "repos/{owner}/{repo}/issues/{index}/assets?{query}" + )) + .multipart( + reqwest::multipart::Form::new().part( + "attachment", + reqwest::multipart::Part::bytes(attachment) + .file_name("file") + .mime_str("*/*") + .unwrap(), + ), + ) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get an issue attachment + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue + /// - `attachment_id`: id of the attachment to get + pub async fn issue_get_issue_attachment( + &self, + owner: &str, + repo: &str, + index: u64, + attachment_id: u64, + ) -> Result { + let request = self + .get(&format!( + "repos/{owner}/{repo}/issues/{index}/assets/{attachment_id}" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete an issue attachment + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue + /// - `attachment_id`: id of the attachment to delete + pub async fn issue_delete_issue_attachment( + &self, + owner: &str, + repo: &str, + index: u64, + attachment_id: u64, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!( + "repos/{owner}/{repo}/issues/{index}/assets/{attachment_id}" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Edit an issue attachment + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue + /// - `attachment_id`: id of the attachment to edit + /// - `body`: See [`EditAttachmentOptions`] + pub async fn issue_edit_issue_attachment( + &self, + owner: &str, + repo: &str, + index: u64, + attachment_id: u64, + body: EditAttachmentOptions, + ) -> Result { + let request = self + .patch(&format!( + "repos/{owner}/{repo}/issues/{index}/assets/{attachment_id}" + )) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List issues that are blocked by this issue + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue + pub async fn issue_list_blocks( + &self, + owner: &str, + repo: &str, + index: &str, + query: IssueListBlocksQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!( + "repos/{owner}/{repo}/issues/{index}/blocks?{query}" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Block the issue given in the body by the issue in path + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue + /// - `body`: See [`IssueMeta`] + pub async fn issue_create_issue_blocking( + &self, + owner: &str, + repo: &str, + index: &str, + body: IssueMeta, + ) -> Result { + let request = self + .post(&format!("repos/{owner}/{repo}/issues/{index}/blocks")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Unblock the issue given in the body by the issue in path + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue + /// - `body`: See [`IssueMeta`] + pub async fn issue_remove_issue_blocking( + &self, + owner: &str, + repo: &str, + index: &str, + body: IssueMeta, + ) -> Result { + let request = self + .delete(&format!("repos/{owner}/{repo}/issues/{index}/blocks")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List all comments on an issue + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue + pub async fn issue_get_comments( + &self, + owner: &str, + repo: &str, + index: u64, + query: IssueGetCommentsQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!( + "repos/{owner}/{repo}/issues/{index}/comments?{query}" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Add a comment to an issue + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue + /// - `body`: See [`CreateIssueCommentOption`] + pub async fn issue_create_comment( + &self, + owner: &str, + repo: &str, + index: u64, + body: CreateIssueCommentOption, + ) -> Result { + let request = self + .post(&format!("repos/{owner}/{repo}/issues/{index}/comments")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a comment + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: this parameter is ignored + /// - `id`: id of comment to delete + pub async fn issue_delete_comment_deprecated( + &self, + owner: &str, + repo: &str, + index: u32, + id: u64, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!( + "repos/{owner}/{repo}/issues/{index}/comments/{id}" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Edit a comment + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: this parameter is ignored + /// - `id`: id of the comment to edit + /// - `body`: See [`EditIssueCommentOption`] + pub async fn issue_edit_comment_deprecated( + &self, + owner: &str, + repo: &str, + index: u32, + id: u64, + body: EditIssueCommentOption, + ) -> Result, ForgejoError> { + let request = self + .patch(&format!( + "repos/{owner}/{repo}/issues/{index}/comments/{id}" + )) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(Some(response.json().await?)), + 204 => Ok(Some(response.json().await?)), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Set an issue deadline. If set to null, the deadline is deleted. If using deadline only the date will be taken into account, and time of day ignored. + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue to create or update a deadline on + /// - `body`: See [`EditDeadlineOption`] + pub async fn issue_edit_issue_deadline( + &self, + owner: &str, + repo: &str, + index: u64, + body: EditDeadlineOption, + ) -> Result { + let request = self + .post(&format!("repos/{owner}/{repo}/issues/{index}/deadline")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List an issue's dependencies, i.e all issues that block this issue. + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue + pub async fn issue_list_issue_dependencies( + &self, + owner: &str, + repo: &str, + index: &str, + query: IssueListIssueDependenciesQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!( + "repos/{owner}/{repo}/issues/{index}/dependencies?{query}" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Make the issue in the url depend on the issue in the form. + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue + /// - `body`: See [`IssueMeta`] + pub async fn issue_create_issue_dependencies( + &self, + owner: &str, + repo: &str, + index: &str, + body: IssueMeta, + ) -> Result { + let request = self + .post(&format!("repos/{owner}/{repo}/issues/{index}/dependencies")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Remove an issue dependency + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue + /// - `body`: See [`IssueMeta`] + pub async fn issue_remove_issue_dependencies( + &self, + owner: &str, + repo: &str, + index: &str, + body: IssueMeta, + ) -> Result { + let request = self + .delete(&format!("repos/{owner}/{repo}/issues/{index}/dependencies")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get an issue's labels + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue + pub async fn issue_get_labels( + &self, + owner: &str, + repo: &str, + index: u64, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/issues/{index}/labels")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Replace an issue's labels + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue + /// - `body`: See [`IssueLabelsOption`] + pub async fn issue_replace_labels( + &self, + owner: &str, + repo: &str, + index: u64, + body: IssueLabelsOption, + ) -> Result, ForgejoError> { + let request = self + .put(&format!("repos/{owner}/{repo}/issues/{index}/labels")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Add a label to an issue + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue + /// - `body`: See [`IssueLabelsOption`] + pub async fn issue_add_label( + &self, + owner: &str, + repo: &str, + index: u64, + body: IssueLabelsOption, + ) -> Result, ForgejoError> { + let request = self + .post(&format!("repos/{owner}/{repo}/issues/{index}/labels")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Remove all labels from an issue + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue + /// - `body`: See [`DeleteLabelsOption`] + pub async fn issue_clear_labels( + &self, + owner: &str, + repo: &str, + index: u64, + body: DeleteLabelsOption, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("repos/{owner}/{repo}/issues/{index}/labels")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Remove a label from an issue + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue + /// - `id`: id of the label to remove + /// - `body`: See [`DeleteLabelsOption`] + pub async fn issue_remove_label( + &self, + owner: &str, + repo: &str, + index: u64, + id: u64, + body: DeleteLabelsOption, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("repos/{owner}/{repo}/issues/{index}/labels/{id}")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Pin an Issue + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of issue to pin + pub async fn pin_issue(&self, owner: &str, repo: &str, index: u64) -> Result<(), ForgejoError> { + let request = self + .post(&format!("repos/{owner}/{repo}/issues/{index}/pin")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Unpin an Issue + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of issue to unpin + pub async fn unpin_issue( + &self, + owner: &str, + repo: &str, + index: u64, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("repos/{owner}/{repo}/issues/{index}/pin")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Moves the Pin to the given Position + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of issue + /// - `position`: the new position + pub async fn move_issue_pin( + &self, + owner: &str, + repo: &str, + index: u64, + position: u64, + ) -> Result<(), ForgejoError> { + let request = self + .patch(&format!( + "repos/{owner}/{repo}/issues/{index}/pin/{position}" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a list reactions of an issue + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue + pub async fn issue_get_issue_reactions( + &self, + owner: &str, + repo: &str, + index: u64, + query: IssueGetIssueReactionsQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!( + "repos/{owner}/{repo}/issues/{index}/reactions?{query}" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Add a reaction to an issue + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue + /// - `content`: See [`EditReactionOption`] + pub async fn issue_post_issue_reaction( + &self, + owner: &str, + repo: &str, + index: u64, + content: EditReactionOption, + ) -> Result { + let request = self + .post(&format!("repos/{owner}/{repo}/issues/{index}/reactions")) + .json(&content) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Remove a reaction from an issue + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue + /// - `content`: See [`EditReactionOption`] + pub async fn issue_delete_issue_reaction( + &self, + owner: &str, + repo: &str, + index: u64, + content: EditReactionOption, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("repos/{owner}/{repo}/issues/{index}/reactions")) + .json(&content) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete an issue's existing stopwatch. + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue to stop the stopwatch on + pub async fn issue_delete_stop_watch( + &self, + owner: &str, + repo: &str, + index: u64, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!( + "repos/{owner}/{repo}/issues/{index}/stopwatch/delete" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Start stopwatch on an issue. + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue to create the stopwatch on + pub async fn issue_start_stop_watch( + &self, + owner: &str, + repo: &str, + index: u64, + ) -> Result<(), ForgejoError> { + let request = self + .post(&format!( + "repos/{owner}/{repo}/issues/{index}/stopwatch/start" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Stop an issue's existing stopwatch. + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue to stop the stopwatch on + pub async fn issue_stop_stop_watch( + &self, + owner: &str, + repo: &str, + index: u64, + ) -> Result<(), ForgejoError> { + let request = self + .post(&format!( + "repos/{owner}/{repo}/issues/{index}/stopwatch/stop" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get users who subscribed on an issue. + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue + pub async fn issue_subscriptions( + &self, + owner: &str, + repo: &str, + index: u64, + query: IssueSubscriptionsQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!( + "repos/{owner}/{repo}/issues/{index}/subscriptions?{query}" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Check if user is subscribed to an issue + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue + pub async fn issue_check_subscription( + &self, + owner: &str, + repo: &str, + index: u64, + ) -> Result { + let request = self + .get(&format!( + "repos/{owner}/{repo}/issues/{index}/subscriptions/check" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Subscribe user to issue + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue + /// - `user`: user to subscribe + pub async fn issue_add_subscription( + &self, + owner: &str, + repo: &str, + index: u64, + user: &str, + ) -> Result<(), ForgejoError> { + let request = self + .put(&format!( + "repos/{owner}/{repo}/issues/{index}/subscriptions/{user}" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(()), + 201 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Unsubscribe user from issue + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue + /// - `user`: user witch unsubscribe + pub async fn issue_delete_subscription( + &self, + owner: &str, + repo: &str, + index: u64, + user: &str, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!( + "repos/{owner}/{repo}/issues/{index}/subscriptions/{user}" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(()), + 201 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List all comments and events on an issue + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue + pub async fn issue_get_comments_and_timeline( + &self, + owner: &str, + repo: &str, + index: u64, + query: IssueGetCommentsAndTimelineQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!( + "repos/{owner}/{repo}/issues/{index}/timeline?{query}" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List an issue's tracked times + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue + pub async fn issue_tracked_times( + &self, + owner: &str, + repo: &str, + index: u64, + query: IssueTrackedTimesQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!( + "repos/{owner}/{repo}/issues/{index}/times?{query}" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Add tracked time to a issue + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue + /// - `body`: See [`AddTimeOption`] + pub async fn issue_add_time( + &self, + owner: &str, + repo: &str, + index: u64, + body: AddTimeOption, + ) -> Result { + let request = self + .post(&format!("repos/{owner}/{repo}/issues/{index}/times")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Reset a tracked time of an issue + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue to add tracked time to + pub async fn issue_reset_time( + &self, + owner: &str, + repo: &str, + index: u64, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("repos/{owner}/{repo}/issues/{index}/times")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete specific tracked time + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the issue + /// - `id`: id of time to delete + pub async fn issue_delete_time( + &self, + owner: &str, + repo: &str, + index: u64, + id: u64, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("repos/{owner}/{repo}/issues/{index}/times/{id}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List a repository's keys + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_list_keys( + &self, + owner: &str, + repo: &str, + query: RepoListKeysQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/keys?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Add a key to a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `body`: See [`CreateKeyOption`] + pub async fn repo_create_key( + &self, + owner: &str, + repo: &str, + body: CreateKeyOption, + ) -> Result { + let request = self + .post(&format!("repos/{owner}/{repo}/keys")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a repository's key by id + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: id of the key to get + pub async fn repo_get_key( + &self, + owner: &str, + repo: &str, + id: u64, + ) -> Result { + let request = self + .get(&format!("repos/{owner}/{repo}/keys/{id}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a key from a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: id of the key to delete + pub async fn repo_delete_key( + &self, + owner: &str, + repo: &str, + id: u64, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("repos/{owner}/{repo}/keys/{id}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get all of a repository's labels + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn issue_list_labels( + &self, + owner: &str, + repo: &str, + query: IssueListLabelsQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/labels?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create a label + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `body`: See [`CreateLabelOption`] + pub async fn issue_create_label( + &self, + owner: &str, + repo: &str, + body: CreateLabelOption, + ) -> Result { + let request = self + .post(&format!("repos/{owner}/{repo}/labels")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a single label + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: id of the label to get + pub async fn issue_get_label( + &self, + owner: &str, + repo: &str, + id: u64, + ) -> Result { + let request = self + .get(&format!("repos/{owner}/{repo}/labels/{id}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a label + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: id of the label to delete + pub async fn issue_delete_label( + &self, + owner: &str, + repo: &str, + id: u64, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("repos/{owner}/{repo}/labels/{id}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Update a label + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: id of the label to edit + /// - `body`: See [`EditLabelOption`] + pub async fn issue_edit_label( + &self, + owner: &str, + repo: &str, + id: u64, + body: EditLabelOption, + ) -> Result { + let request = self + .patch(&format!("repos/{owner}/{repo}/labels/{id}")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get languages and number of bytes of code written + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_get_languages( + &self, + owner: &str, + repo: &str, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/languages")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a file or it's LFS object from a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `filepath`: filepath of the file to get + pub async fn repo_get_raw_file_or_lfs( + &self, + owner: &str, + repo: &str, + filepath: &str, + query: RepoGetRawFileOrLfsQuery, + ) -> Result<(), ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/media/{filepath}?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get all of a repository's opened milestones + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn issue_get_milestones_list( + &self, + owner: &str, + repo: &str, + query: IssueGetMilestonesListQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/milestones?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create a milestone + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `body`: See [`CreateMilestoneOption`] + pub async fn issue_create_milestone( + &self, + owner: &str, + repo: &str, + body: CreateMilestoneOption, + ) -> Result { + let request = self + .post(&format!("repos/{owner}/{repo}/milestones")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a milestone + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: the milestone to get, identified by ID and if not available by name + pub async fn issue_get_milestone( + &self, + owner: &str, + repo: &str, + id: &str, + ) -> Result { + let request = self + .get(&format!("repos/{owner}/{repo}/milestones/{id}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a milestone + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: the milestone to delete, identified by ID and if not available by name + pub async fn issue_delete_milestone( + &self, + owner: &str, + repo: &str, + id: &str, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("repos/{owner}/{repo}/milestones/{id}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Update a milestone + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: the milestone to edit, identified by ID and if not available by name + /// - `body`: See [`EditMilestoneOption`] + pub async fn issue_edit_milestone( + &self, + owner: &str, + repo: &str, + id: &str, + body: EditMilestoneOption, + ) -> Result { + let request = self + .patch(&format!("repos/{owner}/{repo}/milestones/{id}")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Sync a mirrored repository + /// + /// - `owner`: owner of the repo to sync + /// - `repo`: name of the repo to sync + pub async fn repo_mirror_sync(&self, owner: &str, repo: &str) -> Result<(), ForgejoError> { + let request = self + .post(&format!("repos/{owner}/{repo}/mirror-sync")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Returns if new Issue Pins are allowed + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_new_pin_allowed( + &self, + owner: &str, + repo: &str, + ) -> Result { + let request = self + .get(&format!("repos/{owner}/{repo}/new_pin_allowed")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List users's notification threads on a specific repo + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn notify_get_repo_list( + &self, + owner: &str, + repo: &str, + query: NotifyGetRepoListQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/notifications?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Mark notification threads as read, pinned or unread on a specific repo + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn notify_read_repo_list( + &self, + owner: &str, + repo: &str, + query: NotifyReadRepoListQuery, + ) -> Result, ForgejoError> { + let request = self + .put(&format!("repos/{owner}/{repo}/notifications?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 205 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List a repo's pull requests + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_list_pull_requests( + &self, + owner: &str, + repo: &str, + query: RepoListPullRequestsQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/pulls?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create a pull request + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `body`: See [`CreatePullRequestOption`] + pub async fn repo_create_pull_request( + &self, + owner: &str, + repo: &str, + body: CreatePullRequestOption, + ) -> Result { + let request = self + .post(&format!("repos/{owner}/{repo}/pulls")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List a repo's pinned pull requests + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_list_pinned_pull_requests( + &self, + owner: &str, + repo: &str, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/pulls/pinned")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a pull request + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the pull request to get + pub async fn repo_get_pull_request( + &self, + owner: &str, + repo: &str, + index: u64, + ) -> Result { + let request = self + .get(&format!("repos/{owner}/{repo}/pulls/{index}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Update a pull request. If using deadline only the date will be taken into account, and time of day ignored. + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the pull request to edit + /// - `body`: See [`EditPullRequestOption`] + pub async fn repo_edit_pull_request( + &self, + owner: &str, + repo: &str, + index: u64, + body: EditPullRequestOption, + ) -> Result { + let request = self + .patch(&format!("repos/{owner}/{repo}/pulls/{index}")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a pull request diff or patch + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the pull request to get + /// - `diffType`: whether the output is diff or patch + pub async fn repo_download_pull_diff_or_patch( + &self, + owner: &str, + repo: &str, + index: u64, + diff_type: &str, + query: RepoDownloadPullDiffOrPatchQuery, + ) -> Result { + let request = self + .get(&format!( + "repos/{owner}/{repo}/pulls/{index}.{diff_type}?{query}" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.text().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get commits for a pull request + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the pull request to get + pub async fn repo_get_pull_request_commits( + &self, + owner: &str, + repo: &str, + index: u64, + query: RepoGetPullRequestCommitsQuery, + ) -> Result<(CommitListHeaders, Vec), ForgejoError> { + let request = self + .get(&format!( + "repos/{owner}/{repo}/pulls/{index}/commits?{query}" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok((response.headers().try_into()?, response.json().await?)), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get changed files for a pull request + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the pull request to get + pub async fn repo_get_pull_request_files( + &self, + owner: &str, + repo: &str, + index: u64, + query: RepoGetPullRequestFilesQuery, + ) -> Result<(ChangedFileListHeaders, Vec), 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().try_into()?, response.json().await?)), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Check if a pull request has been merged + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the pull request + pub async fn repo_pull_request_is_merged( + &self, + owner: &str, + repo: &str, + index: u64, + ) -> Result<(), ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/pulls/{index}/merge")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Merge a pull request + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the pull request to merge + /// - `body`: See [`MergePullRequestOption`] + pub async fn repo_merge_pull_request( + &self, + owner: &str, + repo: &str, + index: u64, + body: MergePullRequestOption, + ) -> Result<(), ForgejoError> { + let request = self + .post(&format!("repos/{owner}/{repo}/pulls/{index}/merge")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Cancel the scheduled auto merge for the given pull request + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the pull request to merge + pub async fn repo_cancel_scheduled_auto_merge( + &self, + owner: &str, + repo: &str, + index: u64, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("repos/{owner}/{repo}/pulls/{index}/merge")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// create review requests for a pull request + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the pull request + /// - `body`: See [`PullReviewRequestOptions`] + pub async fn repo_create_pull_review_requests( + &self, + owner: &str, + repo: &str, + index: u64, + body: PullReviewRequestOptions, + ) -> Result, ForgejoError> { + let request = self + .post(&format!( + "repos/{owner}/{repo}/pulls/{index}/requested_reviewers" + )) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// cancel review requests for a pull request + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the pull request + /// - `body`: See [`PullReviewRequestOptions`] + pub async fn repo_delete_pull_review_requests( + &self, + owner: &str, + repo: &str, + index: u64, + body: PullReviewRequestOptions, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!( + "repos/{owner}/{repo}/pulls/{index}/requested_reviewers" + )) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List all reviews for a pull request + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the pull request + pub async fn repo_list_pull_reviews( + &self, + owner: &str, + repo: &str, + index: u64, + query: RepoListPullReviewsQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!( + "repos/{owner}/{repo}/pulls/{index}/reviews?{query}" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create a review to an pull request + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the pull request + /// - `body`: See [`CreatePullReviewOptions`] + pub async fn repo_create_pull_review( + &self, + owner: &str, + repo: &str, + index: u64, + body: CreatePullReviewOptions, + ) -> Result { + let request = self + .post(&format!("repos/{owner}/{repo}/pulls/{index}/reviews")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a specific review for a pull request + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the pull request + /// - `id`: id of the review + pub async fn repo_get_pull_review( + &self, + owner: &str, + repo: &str, + index: u64, + id: u64, + ) -> Result { + let request = self + .get(&format!("repos/{owner}/{repo}/pulls/{index}/reviews/{id}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Submit a pending review to an pull request + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the pull request + /// - `id`: id of the review + /// - `body`: See [`SubmitPullReviewOptions`] + pub async fn repo_submit_pull_review( + &self, + owner: &str, + repo: &str, + index: u64, + id: u64, + body: SubmitPullReviewOptions, + ) -> Result { + let request = self + .post(&format!("repos/{owner}/{repo}/pulls/{index}/reviews/{id}")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a specific review from a pull request + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the pull request + /// - `id`: id of the review + pub async fn repo_delete_pull_review( + &self, + owner: &str, + repo: &str, + index: u64, + id: u64, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("repos/{owner}/{repo}/pulls/{index}/reviews/{id}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a specific review for a pull request + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the pull request + /// - `id`: id of the review + pub async fn repo_get_pull_review_comments( + &self, + owner: &str, + repo: &str, + index: u64, + id: u64, + ) -> Result, ForgejoError> { + let request = self + .get(&format!( + "repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Dismiss a review for a pull request + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the pull request + /// - `id`: id of the review + /// - `body`: See [`DismissPullReviewOptions`] + pub async fn repo_dismiss_pull_review( + &self, + owner: &str, + repo: &str, + index: u64, + id: u64, + body: DismissPullReviewOptions, + ) -> Result { + let request = self + .post(&format!( + "repos/{owner}/{repo}/pulls/{index}/reviews/{id}/dismissals" + )) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Cancel to dismiss a review for a pull request + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the pull request + /// - `id`: id of the review + pub async fn repo_un_dismiss_pull_review( + &self, + owner: &str, + repo: &str, + index: u64, + id: u64, + ) -> Result { + let request = self + .post(&format!( + "repos/{owner}/{repo}/pulls/{index}/reviews/{id}/undismissals" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Merge PR's baseBranch into headBranch + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `index`: index of the pull request to get + pub async fn repo_update_pull_request( + &self, + owner: &str, + repo: &str, + index: u64, + query: RepoUpdatePullRequestQuery, + ) -> Result<(), ForgejoError> { + let request = self + .post(&format!( + "repos/{owner}/{repo}/pulls/{index}/update?{query}" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get all push mirrors of the repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_list_push_mirrors( + &self, + owner: &str, + repo: &str, + query: RepoListPushMirrorsQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/push_mirrors?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// add a push mirror to the repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `body`: See [`CreatePushMirrorOption`] + pub async fn repo_add_push_mirror( + &self, + owner: &str, + repo: &str, + body: CreatePushMirrorOption, + ) -> Result { + let request = self + .post(&format!("repos/{owner}/{repo}/push_mirrors")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Sync all push mirrored repository + /// + /// - `owner`: owner of the repo to sync + /// - `repo`: name of the repo to sync + pub async fn repo_push_mirror_sync(&self, owner: &str, repo: &str) -> Result<(), ForgejoError> { + let request = self + .post(&format!("repos/{owner}/{repo}/push_mirrors-sync")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get push mirror of the repository by remoteName + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `name`: remote name of push mirror + pub async fn repo_get_push_mirror_by_remote_name( + &self, + owner: &str, + repo: &str, + name: &str, + ) -> Result { + let request = self + .get(&format!("repos/{owner}/{repo}/push_mirrors/{name}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// deletes a push mirror from a repository by remoteName + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `name`: remote name of the pushMirror + pub async fn repo_delete_push_mirror( + &self, + owner: &str, + repo: &str, + name: &str, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("repos/{owner}/{repo}/push_mirrors/{name}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a file from a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `filepath`: filepath of the file to get + pub async fn repo_get_raw_file( + &self, + owner: &str, + repo: &str, + filepath: &str, + query: RepoGetRawFileQuery, + ) -> Result<(), ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/raw/{filepath}?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List a repo's releases + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_list_releases( + &self, + owner: &str, + repo: &str, + query: RepoListReleasesQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/releases?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create a release + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `body`: See [`CreateReleaseOption`] + pub async fn repo_create_release( + &self, + owner: &str, + repo: &str, + body: CreateReleaseOption, + ) -> Result { + let request = self + .post(&format!("repos/{owner}/{repo}/releases")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Gets the most recent non-prerelease, non-draft release of a repository, sorted by created_at + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_get_latest_release( + &self, + owner: &str, + repo: &str, + ) -> Result { + let request = self + .get(&format!("repos/{owner}/{repo}/releases/latest")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a release by tag name + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `tag`: tag name of the release to get + pub async fn repo_get_release_by_tag( + &self, + owner: &str, + repo: &str, + tag: &str, + ) -> Result { + let request = self + .get(&format!("repos/{owner}/{repo}/releases/tags/{tag}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a release by tag name + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `tag`: tag name of the release to delete + pub async fn repo_delete_release_by_tag( + &self, + owner: &str, + repo: &str, + tag: &str, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("repos/{owner}/{repo}/releases/tags/{tag}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a release + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: id of the release to get + pub async fn repo_get_release( + &self, + owner: &str, + repo: &str, + id: u64, + ) -> Result { + let request = self + .get(&format!("repos/{owner}/{repo}/releases/{id}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a release + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: id of the release to delete + pub async fn repo_delete_release( + &self, + owner: &str, + repo: &str, + id: u64, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("repos/{owner}/{repo}/releases/{id}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Update a release + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: id of the release to edit + /// - `body`: See [`EditReleaseOption`] + pub async fn repo_edit_release( + &self, + owner: &str, + repo: &str, + id: u64, + body: EditReleaseOption, + ) -> Result { + let request = self + .patch(&format!("repos/{owner}/{repo}/releases/{id}")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List release's attachments + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: id of the release + pub async fn repo_list_release_attachments( + &self, + owner: &str, + repo: &str, + id: u64, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/releases/{id}/assets")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create a release attachment + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: id of the release + /// - `attachment`: attachment to upload + pub async fn repo_create_release_attachment( + &self, + owner: &str, + repo: &str, + id: u64, + attachment: Vec, + query: RepoCreateReleaseAttachmentQuery, + ) -> Result { + let request = self + .post(&format!( + "repos/{owner}/{repo}/releases/{id}/assets?{query}" + )) + .multipart( + reqwest::multipart::Form::new().part( + "attachment", + reqwest::multipart::Part::bytes(attachment) + .file_name("file") + .mime_str("*/*") + .unwrap(), + ), + ) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a release attachment + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: id of the release + /// - `attachment_id`: id of the attachment to get + pub async fn repo_get_release_attachment( + &self, + owner: &str, + repo: &str, + id: u64, + attachment_id: u64, + ) -> Result { + let request = self + .get(&format!( + "repos/{owner}/{repo}/releases/{id}/assets/{attachment_id}" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a release attachment + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: id of the release + /// - `attachment_id`: id of the attachment to delete + pub async fn repo_delete_release_attachment( + &self, + owner: &str, + repo: &str, + id: u64, + attachment_id: u64, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!( + "repos/{owner}/{repo}/releases/{id}/assets/{attachment_id}" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Edit a release attachment + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `id`: id of the release + /// - `attachment_id`: id of the attachment to edit + /// - `body`: See [`EditAttachmentOptions`] + pub async fn repo_edit_release_attachment( + &self, + owner: &str, + repo: &str, + id: u64, + attachment_id: u64, + body: EditAttachmentOptions, + ) -> Result { + let request = self + .patch(&format!( + "repos/{owner}/{repo}/releases/{id}/assets/{attachment_id}" + )) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Return all users that can be requested to review in this repo + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_get_reviewers( + &self, + owner: &str, + repo: &str, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/reviewers")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get signing-key.gpg for given repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_signing_key(&self, owner: &str, repo: &str) -> Result { + let request = self + .get(&format!("repos/{owner}/{repo}/signing-key.gpg")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.text().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List a repo's stargazers + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_list_stargazers( + &self, + owner: &str, + repo: &str, + query: RepoListStargazersQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/stargazers?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a commit's statuses + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `sha`: sha of the commit + pub async fn repo_list_statuses( + &self, + owner: &str, + repo: &str, + sha: &str, + query: RepoListStatusesQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/statuses/{sha}?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create a commit status + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `sha`: sha of the commit + /// - `body`: See [`CreateStatusOption`] + pub async fn repo_create_status( + &self, + owner: &str, + repo: &str, + sha: &str, + body: CreateStatusOption, + ) -> Result { + let request = self + .post(&format!("repos/{owner}/{repo}/statuses/{sha}")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List a repo's watchers + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_list_subscribers( + &self, + owner: &str, + repo: &str, + query: RepoListSubscribersQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/subscribers?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Check if the current user is watching a repo + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn user_current_check_subscription( + &self, + owner: &str, + repo: &str, + ) -> Result { + let request = self + .get(&format!("repos/{owner}/{repo}/subscription")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Watch a repo + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn user_current_put_subscription( + &self, + owner: &str, + repo: &str, + ) -> Result { + let request = self + .put(&format!("repos/{owner}/{repo}/subscription")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Unwatch a repo + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn user_current_delete_subscription( + &self, + owner: &str, + repo: &str, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("repos/{owner}/{repo}/subscription")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List a repository's tags + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_list_tags( + &self, + owner: &str, + repo: &str, + query: RepoListTagsQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/tags?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create a new git tag in a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `body`: See [`CreateTagOption`] + pub async fn repo_create_tag( + &self, + owner: &str, + repo: &str, + body: CreateTagOption, + ) -> Result { + let request = self + .post(&format!("repos/{owner}/{repo}/tags")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get the tag of a repository by tag name + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `tag`: name of tag + pub async fn repo_get_tag( + &self, + owner: &str, + repo: &str, + tag: &str, + ) -> Result { + let request = self + .get(&format!("repos/{owner}/{repo}/tags/{tag}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a repository's tag by name + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `tag`: name of tag to delete + pub async fn repo_delete_tag( + &self, + owner: &str, + repo: &str, + tag: &str, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("repos/{owner}/{repo}/tags/{tag}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List a repository's teams + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_list_teams( + &self, + owner: &str, + repo: &str, + ) -> Result, ForgejoError> { + let request = self.get(&format!("repos/{owner}/{repo}/teams")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Check if a team is assigned to a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `team`: team name + pub async fn repo_check_team( + &self, + owner: &str, + repo: &str, + team: &str, + ) -> Result { + let request = self + .get(&format!("repos/{owner}/{repo}/teams/{team}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Add a team to a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `team`: team name + pub async fn repo_add_team( + &self, + owner: &str, + repo: &str, + team: &str, + ) -> Result<(), ForgejoError> { + let request = self + .put(&format!("repos/{owner}/{repo}/teams/{team}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a team from a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `team`: team name + pub async fn repo_delete_team( + &self, + owner: &str, + repo: &str, + team: &str, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("repos/{owner}/{repo}/teams/{team}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List a repo's tracked times + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_tracked_times( + &self, + owner: &str, + repo: &str, + query: RepoTrackedTimesQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/times?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List a user's tracked times in a repo + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `user`: username of user + pub async fn user_tracked_times( + &self, + owner: &str, + repo: &str, + user: &str, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/times/{user}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get list of topics that a repository has + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_list_topics( + &self, + owner: &str, + repo: &str, + query: RepoListTopicsQuery, + ) -> Result { + let request = self + .get(&format!("repos/{owner}/{repo}/topics?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Replace list of topics for a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `body`: See [`RepoTopicOptions`] + pub async fn repo_update_topics( + &self, + owner: &str, + repo: &str, + body: RepoTopicOptions, + ) -> Result<(), ForgejoError> { + let request = self + .put(&format!("repos/{owner}/{repo}/topics")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Add a topic to a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `topic`: name of the topic to add + pub async fn repo_add_topic( + &self, + owner: &str, + repo: &str, + topic: &str, + ) -> Result<(), ForgejoError> { + let request = self + .put(&format!("repos/{owner}/{repo}/topics/{topic}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a topic from a repository + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `topic`: name of the topic to delete + pub async fn repo_delete_topic( + &self, + owner: &str, + repo: &str, + topic: &str, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("repos/{owner}/{repo}/topics/{topic}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Transfer a repo ownership + /// + /// - `owner`: owner of the repo to transfer + /// - `repo`: name of the repo to transfer + /// - `body`: Transfer Options + + /// See [`TransferRepoOption`] + pub async fn repo_transfer( + &self, + owner: &str, + repo: &str, + body: TransferRepoOption, + ) -> Result { + let request = self + .post(&format!("repos/{owner}/{repo}/transfer")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 202 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Accept a repo transfer + /// + /// - `owner`: owner of the repo to transfer + /// - `repo`: name of the repo to transfer + pub async fn accept_repo_transfer( + &self, + owner: &str, + repo: &str, + ) -> Result { + let request = self + .post(&format!("repos/{owner}/{repo}/transfer/accept")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 202 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Reject a repo transfer + /// + /// - `owner`: owner of the repo to transfer + /// - `repo`: name of the repo to transfer + pub async fn reject_repo_transfer( + &self, + owner: &str, + repo: &str, + ) -> Result { + let request = self + .post(&format!("repos/{owner}/{repo}/transfer/reject")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create a wiki page + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `body`: See [`CreateWikiPageOptions`] + pub async fn repo_create_wiki_page( + &self, + owner: &str, + repo: &str, + body: CreateWikiPageOptions, + ) -> Result { + let request = self + .post(&format!("repos/{owner}/{repo}/wiki/new")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a wiki page + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `pageName`: name of the page + pub async fn repo_get_wiki_page( + &self, + owner: &str, + repo: &str, + page_name: &str, + ) -> Result { + let request = self + .get(&format!("repos/{owner}/{repo}/wiki/page/{page_name}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a wiki page + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `pageName`: name of the page + pub async fn repo_delete_wiki_page( + &self, + owner: &str, + repo: &str, + page_name: &str, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("repos/{owner}/{repo}/wiki/page/{page_name}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Edit a wiki page + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `pageName`: name of the page + /// - `body`: See [`CreateWikiPageOptions`] + pub async fn repo_edit_wiki_page( + &self, + owner: &str, + repo: &str, + page_name: &str, + body: CreateWikiPageOptions, + ) -> Result { + let request = self + .patch(&format!("repos/{owner}/{repo}/wiki/page/{page_name}")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get all wiki pages + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn repo_get_wiki_pages( + &self, + owner: &str, + repo: &str, + query: RepoGetWikiPagesQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("repos/{owner}/{repo}/wiki/pages?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get revisions of a wiki page + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + /// - `pageName`: name of the page + pub async fn repo_get_wiki_page_revisions( + &self, + owner: &str, + repo: &str, + page_name: &str, + query: RepoGetWikiPageRevisionsQuery, + ) -> Result { + let request = self + .get(&format!( + "repos/{owner}/{repo}/wiki/revisions/{page_name}?{query}" + )) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create a repository using a template + /// + /// - `template_owner`: name of the template repository owner + /// - `template_repo`: name of the template repository + /// - `body`: See [`GenerateRepoOption`] + pub async fn generate_repo( + &self, + template_owner: &str, + template_repo: &str, + body: GenerateRepoOption, + ) -> Result { + let request = self + .post(&format!("repos/{template_owner}/{template_repo}/generate")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a repository by id + /// + /// - `id`: id of the repo to get + pub async fn repo_get_by_id(&self, id: u64) -> Result { + let request = self.get(&format!("repositories/{id}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get instance's global settings for api + pub async fn get_general_api_settings(&self) -> Result { + let request = self.get("settings/api").build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get instance's global settings for Attachment + pub async fn get_general_attachment_settings( + &self, + ) -> Result { + let request = self.get("settings/attachment").build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get instance's global settings for repositories + pub async fn get_general_repository_settings( + &self, + ) -> Result { + let request = self.get("settings/repository").build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get instance's global settings for ui + pub async fn get_general_ui_settings(&self) -> Result { + let request = self.get("settings/ui").build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get default signing-key.gpg + pub async fn get_signing_key(&self) -> Result { + let request = self.get("signing-key.gpg").build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.text().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a team + /// + /// - `id`: id of the team to get + pub async fn org_get_team(&self, id: u64) -> Result { + let request = self.get(&format!("teams/{id}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a team + /// + /// - `id`: id of the team to delete + pub async fn org_delete_team(&self, id: u64) -> Result<(), ForgejoError> { + let request = self.delete(&format!("teams/{id}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Edit a team + /// + /// - `id`: id of the team to edit + /// - `body`: See [`EditTeamOption`] + pub async fn org_edit_team(&self, id: u32, body: EditTeamOption) -> Result { + let request = self.patch(&format!("teams/{id}")).json(&body).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List a team's activity feeds + /// + /// - `id`: id of the team + pub async fn org_list_team_activity_feeds( + &self, + id: u64, + query: OrgListTeamActivityFeedsQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("teams/{id}/activities/feeds?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List a team's members + /// + /// - `id`: id of the team + pub async fn org_list_team_members( + &self, + id: u64, + query: OrgListTeamMembersQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("teams/{id}/members?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List a particular member of team + /// + /// - `id`: id of the team + /// - `username`: username of the member to list + pub async fn org_list_team_member( + &self, + id: u64, + username: &str, + ) -> Result { + let request = self + .get(&format!("teams/{id}/members/{username}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Add a team member + /// + /// - `id`: id of the team + /// - `username`: username of the user to add + pub async fn org_add_team_member(&self, id: u64, username: &str) -> Result<(), ForgejoError> { + let request = self + .put(&format!("teams/{id}/members/{username}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Remove a team member + /// + /// - `id`: id of the team + /// - `username`: username of the user to remove + pub async fn org_remove_team_member( + &self, + id: u64, + username: &str, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("teams/{id}/members/{username}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List a team's repos + /// + /// - `id`: id of the team + pub async fn org_list_team_repos( + &self, + id: u64, + query: OrgListTeamReposQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("teams/{id}/repos?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List a particular repo of team + /// + /// - `id`: id of the team + /// - `org`: organization that owns the repo to list + /// - `repo`: name of the repo to list + pub async fn org_list_team_repo( + &self, + id: u64, + org: &str, + repo: &str, + ) -> Result { + let request = self + .get(&format!("teams/{id}/repos/{org}/{repo}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Add a repository to a team + /// + /// - `id`: id of the team + /// - `org`: organization that owns the repo to add + /// - `repo`: name of the repo to add + pub async fn org_add_team_repository( + &self, + id: u64, + org: &str, + repo: &str, + ) -> Result<(), ForgejoError> { + let request = self + .put(&format!("teams/{id}/repos/{org}/{repo}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Remove a repository from a team + /// + /// - `id`: id of the team + /// - `org`: organization that owns the repo to remove + /// - `repo`: name of the repo to remove + pub async fn org_remove_team_repository( + &self, + id: u64, + org: &str, + repo: &str, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("teams/{id}/repos/{org}/{repo}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// search topics via keyword + /// + pub async fn topic_search( + &self, + query: TopicSearchQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("topics/search?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get the authenticated user + pub async fn user_get_current(&self) -> Result { + let request = self.get("user").build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create or Update a secret value in a user scope + /// + /// - `secretname`: name of the secret + /// - `body`: See [`CreateOrUpdateSecretOption`] + pub async fn update_user_secret( + &self, + secretname: &str, + body: CreateOrUpdateSecretOption, + ) -> Result<(), ForgejoError> { + let request = self + .put(&format!("user/actions/secrets/{secretname}")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(()), + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a secret in a user scope + /// + /// - `secretname`: name of the secret + pub async fn delete_user_secret(&self, secretname: &str) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("user/actions/secrets/{secretname}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List the authenticated user's oauth2 applications + /// + pub async fn user_get_oauth2_applications( + &self, + query: UserGetOAuth2ApplicationsQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("user/applications/oauth2?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// creates a new OAuth2 application + /// + /// - `body`: See [`CreateOAuth2ApplicationOptions`] + pub async fn user_create_oauth2_application( + &self, + body: CreateOAuth2ApplicationOptions, + ) -> Result { + let request = self.post("user/applications/oauth2").json(&body).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// get an OAuth2 Application + /// + /// - `id`: Application ID to be found + pub async fn user_get_oauth2_application( + &self, + id: u64, + ) -> Result { + let request = self + .get(&format!("user/applications/oauth2/{id}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// delete an OAuth2 Application + /// + /// - `id`: token to be deleted + pub async fn user_delete_oauth2_application(&self, id: u64) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("user/applications/oauth2/{id}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// update an OAuth2 Application, this includes regenerating the client secret + /// + /// - `id`: application to be updated + /// - `body`: See [`CreateOAuth2ApplicationOptions`] + pub async fn user_update_oauth2_application( + &self, + id: u64, + body: CreateOAuth2ApplicationOptions, + ) -> Result { + let request = self + .patch(&format!("user/applications/oauth2/{id}")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Update Avatar + /// + /// - `body`: See [`UpdateUserAvatarOption`] + pub async fn user_update_avatar( + &self, + body: UpdateUserAvatarOption, + ) -> Result<(), ForgejoError> { + let request = self.post("user/avatar").json(&body).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete Avatar + pub async fn user_delete_avatar(&self) -> Result<(), ForgejoError> { + let request = self.delete("user/avatar").build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Blocks a user from the doer. + /// + /// - `username`: username of the user + pub async fn user_block_user(&self, username: &str) -> Result<(), ForgejoError> { + let request = self.put(&format!("user/block/{username}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List the authenticated user's email addresses + pub async fn user_list_emails(&self) -> Result, ForgejoError> { + let request = self.get("user/emails").build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Add email addresses + /// + /// - `body`: See [`CreateEmailOption`] + pub async fn user_add_email( + &self, + body: CreateEmailOption, + ) -> Result, ForgejoError> { + let request = self.post("user/emails").json(&body).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete email addresses + /// + /// - `body`: See [`DeleteEmailOption`] + pub async fn user_delete_email(&self, body: DeleteEmailOption) -> Result<(), ForgejoError> { + let request = self.delete("user/emails").json(&body).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List the authenticated user's followers + /// + pub async fn user_current_list_followers( + &self, + query: UserCurrentListFollowersQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("user/followers?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List the users that the authenticated user is following + /// + pub async fn user_current_list_following( + &self, + query: UserCurrentListFollowingQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("user/following?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Check whether a user is followed by the authenticated user + /// + /// - `username`: username of followed user + pub async fn user_current_check_following(&self, username: &str) -> Result<(), ForgejoError> { + let request = self.get(&format!("user/following/{username}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Follow a user + /// + /// - `username`: username of user to follow + pub async fn user_current_put_follow(&self, username: &str) -> Result<(), ForgejoError> { + let request = self.put(&format!("user/following/{username}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Unfollow a user + /// + /// - `username`: username of user to unfollow + pub async fn user_current_delete_follow(&self, username: &str) -> Result<(), ForgejoError> { + let request = self.delete(&format!("user/following/{username}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a Token to verify + pub async fn get_verification_token(&self) -> Result { + let request = self.get("user/gpg_key_token").build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.text().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Verify a GPG key + pub async fn user_verify_gpg_key(&self) -> Result { + let request = self.post("user/gpg_key_verify").build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List the authenticated user's GPG keys + /// + pub async fn user_current_list_gpg_keys( + &self, + query: UserCurrentListGpgKeysQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("user/gpg_keys?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create a GPG key + /// + /// - `Form`: See [`CreateGPGKeyOption`] + pub async fn user_current_post_gpg_key( + &self, + form: CreateGPGKeyOption, + ) -> Result { + let request = self.post("user/gpg_keys").json(&form).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a GPG key + /// + /// - `id`: id of key to get + pub async fn user_current_get_gpg_key(&self, id: u64) -> Result { + let request = self.get(&format!("user/gpg_keys/{id}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Remove a GPG key + /// + /// - `id`: id of key to delete + pub async fn user_current_delete_gpg_key(&self, id: u64) -> Result<(), ForgejoError> { + let request = self.delete(&format!("user/gpg_keys/{id}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List the authenticated user's webhooks + /// + pub async fn user_list_hooks( + &self, + query: UserListHooksQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("user/hooks?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create a hook + /// + /// - `body`: See [`CreateHookOption`] + pub async fn user_create_hook(&self, body: CreateHookOption) -> Result { + let request = self.post("user/hooks").json(&body).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a hook + /// + /// - `id`: id of the hook to get + pub async fn user_get_hook(&self, id: u64) -> Result { + let request = self.get(&format!("user/hooks/{id}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a hook + /// + /// - `id`: id of the hook to delete + pub async fn user_delete_hook(&self, id: u64) -> Result<(), ForgejoError> { + let request = self.delete(&format!("user/hooks/{id}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Update a hook + /// + /// - `id`: id of the hook to update + /// - `body`: See [`EditHookOption`] + pub async fn user_edit_hook( + &self, + id: u64, + body: EditHookOption, + ) -> Result { + let request = self + .patch(&format!("user/hooks/{id}")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List the authenticated user's public keys + /// + pub async fn user_current_list_keys( + &self, + query: UserCurrentListKeysQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("user/keys?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create a public key + /// + /// - `body`: See [`CreateKeyOption`] + pub async fn user_current_post_key( + &self, + body: CreateKeyOption, + ) -> Result { + let request = self.post("user/keys").json(&body).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a public key + /// + /// - `id`: id of key to get + pub async fn user_current_get_key(&self, id: u64) -> Result { + let request = self.get(&format!("user/keys/{id}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Delete a public key + /// + /// - `id`: id of key to delete + pub async fn user_current_delete_key(&self, id: u64) -> Result<(), ForgejoError> { + let request = self.delete(&format!("user/keys/{id}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List the authenticated user's blocked users + /// + pub async fn user_list_blocked_users( + &self, + query: UserListBlockedUsersQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("user/list_blocked?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List the current user's organizations + /// + pub async fn org_list_current_user_orgs( + &self, + query: OrgListCurrentUserOrgsQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("user/orgs?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List the repos that the authenticated user owns + /// + pub async fn user_current_list_repos( + &self, + query: UserCurrentListReposQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("user/repos?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create a repository + /// + /// - `body`: See [`CreateRepoOption`] + pub async fn create_current_user_repo( + &self, + body: CreateRepoOption, + ) -> Result { + let request = self.post("user/repos").json(&body).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get user settings + pub async fn get_user_settings(&self) -> Result, ForgejoError> { + let request = self.get("user/settings").build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Update user settings + /// + /// - `body`: See [`UserSettingsOptions`] + pub async fn update_user_settings( + &self, + body: UserSettingsOptions, + ) -> Result, ForgejoError> { + let request = self.patch("user/settings").json(&body).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// The repos that the authenticated user has starred + /// + pub async fn user_current_list_starred( + &self, + query: UserCurrentListStarredQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("user/starred?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Whether the authenticated is starring the repo + /// + /// - `owner`: owner of the repo + /// - `repo`: name of the repo + pub async fn user_current_check_starring( + &self, + owner: &str, + repo: &str, + ) -> Result<(), ForgejoError> { + let request = self.get(&format!("user/starred/{owner}/{repo}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Star the given repo + /// + /// - `owner`: owner of the repo to star + /// - `repo`: name of the repo to star + pub async fn user_current_put_star(&self, owner: &str, repo: &str) -> Result<(), ForgejoError> { + let request = self.put(&format!("user/starred/{owner}/{repo}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Unstar the given repo + /// + /// - `owner`: owner of the repo to unstar + /// - `repo`: name of the repo to unstar + pub async fn user_current_delete_star( + &self, + owner: &str, + repo: &str, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("user/starred/{owner}/{repo}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get list of all existing stopwatches + /// + pub async fn user_get_stop_watches( + &self, + query: UserGetStopWatchesQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("user/stopwatches?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List repositories watched by the authenticated user + /// + pub async fn user_current_list_subscriptions( + &self, + query: UserCurrentListSubscriptionsQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("user/subscriptions?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List all the teams a user belongs to + /// + pub async fn user_list_teams( + &self, + query: UserListTeamsQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("user/teams?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List the current user's tracked times + /// + pub async fn user_current_tracked_times( + &self, + query: UserCurrentTrackedTimesQuery, + ) -> Result, ForgejoError> { + let request = self.get(&format!("user/times?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Unblocks a user from the doer. + /// + /// - `username`: username of the user + pub async fn user_unblock_user(&self, username: &str) -> Result<(), ForgejoError> { + let request = self.put(&format!("user/unblock/{username}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Search for users + /// + pub async fn user_search( + &self, + query: UserSearchQuery, + ) -> Result { + let request = self.get(&format!("users/search?{query}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a user + /// + /// - `username`: username of user to get + pub async fn user_get(&self, username: &str) -> Result { + let request = self.get(&format!("users/{username}")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List a user's activity feeds + /// + /// - `username`: username of user + pub async fn user_list_activity_feeds( + &self, + username: &str, + query: UserListActivityFeedsQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("users/{username}/activities/feeds?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List the given user's followers + /// + /// - `username`: username of user + pub async fn user_list_followers( + &self, + username: &str, + query: UserListFollowersQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("users/{username}/followers?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List the users that the given user is following + /// + /// - `username`: username of user + pub async fn user_list_following( + &self, + username: &str, + query: UserListFollowingQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("users/{username}/following?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Check if one user is following another user + /// + /// - `username`: username of following user + /// - `target`: username of followed user + pub async fn user_check_following( + &self, + username: &str, + target: &str, + ) -> Result<(), ForgejoError> { + let request = self + .get(&format!("users/{username}/following/{target}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List the given user's GPG keys + /// + /// - `username`: username of user + pub async fn user_list_gpg_keys( + &self, + username: &str, + query: UserListGpgKeysQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("users/{username}/gpg_keys?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get a user's heatmap + /// + /// - `username`: username of user to get + pub async fn user_get_heatmap_data( + &self, + username: &str, + ) -> Result, ForgejoError> { + let request = self.get(&format!("users/{username}/heatmap")).build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List the given user's public keys + /// + /// - `username`: username of user + pub async fn user_list_keys( + &self, + username: &str, + query: UserListKeysQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("users/{username}/keys?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List a user's organizations + /// + /// - `username`: username of user + pub async fn org_list_user_orgs( + &self, + username: &str, + query: OrgListUserOrgsQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("users/{username}/orgs?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Get user permissions in organization + /// + /// - `username`: username of user + /// - `org`: name of the organization + pub async fn org_get_user_permissions( + &self, + username: &str, + org: &str, + ) -> Result { + let request = self + .get(&format!("users/{username}/orgs/{org}/permissions")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List the repos owned by the given user + /// + /// - `username`: username of user + pub async fn user_list_repos( + &self, + username: &str, + query: UserListReposQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("users/{username}/repos?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// The repos that the given user has starred + /// + /// - `username`: username of user + pub async fn user_list_starred( + &self, + username: &str, + query: UserListStarredQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("users/{username}/starred?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List the repositories watched by a user + /// + /// - `username`: username of the user + pub async fn user_list_subscriptions( + &self, + username: &str, + query: UserListSubscriptionsQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("users/{username}/subscriptions?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// List the authenticated user's access tokens + /// + /// - `username`: username of user + pub async fn user_get_tokens( + &self, + username: &str, + query: UserGetTokensQuery, + ) -> Result, ForgejoError> { + let request = self + .get(&format!("users/{username}/tokens?{query}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Create an access token + /// + /// - `username`: username of user + /// - `body`: See [`CreateAccessTokenOption`] + pub async fn user_create_token( + &self, + username: &str, + body: CreateAccessTokenOption, + ) -> Result { + let request = self + .post(&format!("users/{username}/tokens")) + .json(&body) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 201 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// delete an access token + /// + /// - `username`: username of user + /// - `token`: token to be deleted, identified by ID and if not available by name + pub async fn user_delete_access_token( + &self, + username: &str, + token: &str, + ) -> Result<(), ForgejoError> { + let request = self + .delete(&format!("users/{username}/tokens/{token}")) + .build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 204 => Ok(()), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } + + /// Returns the version of the Gitea application + pub async fn get_version(&self) -> Result { + let request = self.get("version").build()?; + let response = self.execute(request).await?; + match response.status().as_u16() { + 200 => Ok(response.json().await?), + _ => Err(ForgejoError::UnexpectedStatusCode(response.status())), + } + } +} diff --git a/src/generated/mod.rs b/src/generated/mod.rs new file mode 100644 index 0000000..fb6927b --- /dev/null +++ b/src/generated/mod.rs @@ -0,0 +1,2 @@ +pub mod methods; +pub mod structs; diff --git a/src/generated/structs.rs b/src/generated/structs.rs new file mode 100644 index 0000000..7b8b25f --- /dev/null +++ b/src/generated/structs.rs @@ -0,0 +1,5959 @@ +use crate::StructureError; +use std::collections::BTreeMap; +/// APIError is an api error with a message +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct APIError { + pub message: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub url: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct AccessToken { + pub id: Option, + pub name: Option, + pub scopes: Option>, + pub sha1: Option, + pub token_last_eight: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct Activity { + pub act_user: Option, + pub act_user_id: Option, + pub comment: Option, + pub comment_id: Option, + pub content: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub created: Option, + pub id: Option, + pub is_private: Option, + pub op_type: Option, + pub ref_name: Option, + pub repo: Option, + pub repo_id: Option, + pub user_id: Option, +} + +/// ActivityPub type +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct ActivityPub { + #[serde(rename = "@context")] + pub context: Option, +} + +/// AddCollaboratorOption options when adding a user as a collaborator of a repository +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct AddCollaboratorOption { + pub permission: Option, +} + +/// AddTimeOption options for adding time to an issue +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct AddTimeOption { + #[serde(with = "time::serde::rfc3339::option")] + pub created: Option, + /// time in seconds + pub time: u64, + /// User who spent the time (optional) + pub user_name: Option, +} + +/// AnnotatedTag represents an annotated tag +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct AnnotatedTag { + pub message: Option, + pub object: Option, + pub sha: Option, + pub tag: Option, + pub tagger: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub url: Option, + pub verification: Option, +} + +/// AnnotatedTagObject contains meta information of the tag object +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct AnnotatedTagObject { + pub sha: Option, + #[serde(rename = "type")] + pub r#type: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub url: Option, +} + +/// Attachment a generic attachment +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct Attachment { + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub browser_download_url: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub created_at: Option, + pub download_count: Option, + pub id: Option, + pub name: Option, + pub size: Option, + pub uuid: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct BlockedUser { + pub block_id: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub created_at: Option, +} + +/// Branch represents a repository branch +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct Branch { + pub commit: Option, + pub effective_branch_protection_name: Option, + pub enable_status_check: Option, + pub name: Option, + pub protected: Option, + pub required_approvals: Option, + pub status_check_contexts: Option>, + pub user_can_merge: Option, + pub user_can_push: Option, +} + +/// BranchProtection represents a branch protection for a repository +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct BranchProtection { + pub approvals_whitelist_teams: Option>, + pub approvals_whitelist_username: Option>, + pub block_on_official_review_requests: Option, + pub block_on_outdated_branch: Option, + pub block_on_rejected_reviews: Option, + /// Deprecated: true + pub branch_name: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub created_at: Option, + pub dismiss_stale_approvals: Option, + pub enable_approvals_whitelist: Option, + pub enable_merge_whitelist: Option, + pub enable_push: Option, + pub enable_push_whitelist: Option, + pub enable_status_check: Option, + pub merge_whitelist_teams: Option>, + pub merge_whitelist_usernames: Option>, + pub protected_file_patterns: Option, + pub push_whitelist_deploy_keys: Option, + pub push_whitelist_teams: Option>, + pub push_whitelist_usernames: Option>, + pub require_signed_commits: Option, + pub required_approvals: Option, + pub rule_name: Option, + pub status_check_contexts: Option>, + pub unprotected_file_patterns: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub updated_at: Option, +} + +/// ChangeFileOperation for creating, updating or deleting a file +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct ChangeFileOperation { + /// new or updated file content, must be base64 encoded + pub content: Option, + /// old path of the file to move + pub from_path: Option, + /// indicates what to do with the file + pub operation: ChangeFileOperationOperation, + /// path to the existing or new file + pub path: String, + /// sha is the SHA for the file that already exists, required for update or delete + pub sha: Option, +} + +/// indicates what to do with the file + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum ChangeFileOperationOperation { + #[serde(rename = "create")] + Create, + #[serde(rename = "update")] + Update, + #[serde(rename = "delete")] + Delete, +} +/// ChangeFilesOptions options for creating, updating or deleting multiple files +/// +/// Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used) +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct ChangeFilesOptions { + pub author: Option, + /// branch (optional) to base this file from. if not given, the default branch is used + pub branch: Option, + pub committer: Option, + pub dates: Option, + /// list of file operations + pub files: Vec, + /// message (optional) for the commit of this file. if not supplied, a default message will be used + pub message: Option, + /// new_branch (optional) will make a new branch from `branch` before creating the file + pub new_branch: Option, + /// Add a Signed-off-by trailer by the committer at the end of the commit log message. + pub signoff: Option, +} + +/// ChangedFile store information about files affected by the pull request +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct ChangedFile { + pub additions: Option, + pub changes: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub contents_url: Option, + pub deletions: Option, + pub filename: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub html_url: Option, + pub previous_filename: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub raw_url: Option, + pub status: Option, +} + +/// CombinedStatus holds the combined state of several statuses for a single commit +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CombinedStatus { + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub commit_url: Option, + pub repository: Option, + pub sha: Option, + pub state: Option, + pub statuses: Option>, + pub total_count: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub url: Option, +} + +/// Comment represents a comment on a commit or issue +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct Comment { + pub assets: Option>, + pub body: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub created_at: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub html_url: Option, + pub id: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub issue_url: Option, + pub original_author: Option, + pub original_author_id: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub pull_request_url: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub updated_at: Option, + pub user: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct Commit { + pub author: Option, + pub commit: Option, + pub committer: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub created: Option, + pub files: Option>, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub html_url: Option, + pub parents: Option>, + pub sha: Option, + pub stats: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub url: Option, +} + +/// CommitAffectedFiles store information about files affected by the commit +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CommitAffectedFiles { + pub filename: Option, + pub status: Option, +} + +/// CommitDateOptions store dates for GIT_AUTHOR_DATE and GIT_COMMITTER_DATE +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CommitDateOptions { + #[serde(with = "time::serde::rfc3339::option")] + pub author: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub committer: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CommitMeta { + #[serde(with = "time::serde::rfc3339::option")] + pub created: Option, + pub sha: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub url: Option, +} + +/// CommitStats is statistics for a RepoCommit +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CommitStats { + pub additions: Option, + pub deletions: Option, + pub total: Option, +} + +/// CommitStatus holds a single status of a single Commit +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CommitStatus { + pub context: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub created_at: Option, + pub creator: Option, + pub description: Option, + pub id: Option, + pub status: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub target_url: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub updated_at: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub url: Option, +} + +/// CommitStatusState holds the state of a CommitStatus +/// +/// It can be "pending", "success", "error" and "failure" +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CommitStatusState {} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CommitUser { + pub date: Option, + pub email: Option, + pub name: Option, +} + +/// ContentsResponse contains information about a repo's entry's (dir, file, symlink, submodule) metadata and content +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct ContentsResponse { + #[serde(rename = "_links")] + pub links: Option, + /// `content` is populated when `type` is `file`, otherwise null + pub content: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub download_url: Option, + /// `encoding` is populated when `type` is `file`, otherwise null + pub encoding: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub git_url: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub html_url: Option, + pub last_commit_sha: Option, + pub name: Option, + pub path: Option, + pub sha: Option, + pub size: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + /// `submodule_git_url` is populated when `type` is `submodule`, otherwise null + pub submodule_git_url: Option, + /// `target` is populated when `type` is `symlink`, otherwise null + pub target: Option, + /// `type` will be `file`, `dir`, `symlink`, or `submodule` + #[serde(rename = "type")] + pub r#type: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub url: Option, +} + +/// CreateAccessTokenOption options when create access token +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CreateAccessTokenOption { + pub name: String, + pub scopes: Option>, +} + +/// CreateBranchProtectionOption options for creating a branch protection +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CreateBranchProtectionOption { + pub approvals_whitelist_teams: Option>, + pub approvals_whitelist_username: Option>, + pub block_on_official_review_requests: Option, + pub block_on_outdated_branch: Option, + pub block_on_rejected_reviews: Option, + /// Deprecated: true + pub branch_name: Option, + pub dismiss_stale_approvals: Option, + pub enable_approvals_whitelist: Option, + pub enable_merge_whitelist: Option, + pub enable_push: Option, + pub enable_push_whitelist: Option, + pub enable_status_check: Option, + pub merge_whitelist_teams: Option>, + pub merge_whitelist_usernames: Option>, + pub protected_file_patterns: Option, + pub push_whitelist_deploy_keys: Option, + pub push_whitelist_teams: Option>, + pub push_whitelist_usernames: Option>, + pub require_signed_commits: Option, + pub required_approvals: Option, + pub rule_name: Option, + pub status_check_contexts: Option>, + pub unprotected_file_patterns: Option, +} + +/// CreateBranchRepoOption options when creating a branch in a repository +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CreateBranchRepoOption { + /// Name of the branch to create + pub new_branch_name: String, + /// Deprecated: true + /// + /// Name of the old branch to create from + pub old_branch_name: Option, + /// Name of the old branch/tag/commit to create from + pub old_ref_name: Option, +} + +/// CreateEmailOption options when creating email addresses +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CreateEmailOption { + /// email addresses to add + pub emails: Option>, +} + +/// CreateFileOptions options for creating files +/// +/// Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used) +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CreateFileOptions { + pub author: Option, + /// branch (optional) to base this file from. if not given, the default branch is used + pub branch: Option, + pub committer: Option, + /// content must be base64 encoded + pub content: String, + pub dates: Option, + /// message (optional) for the commit of this file. if not supplied, a default message will be used + pub message: Option, + /// new_branch (optional) will make a new branch from `branch` before creating the file + pub new_branch: Option, + /// Add a Signed-off-by trailer by the committer at the end of the commit log message. + pub signoff: Option, +} + +/// CreateForkOption options for creating a fork +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CreateForkOption { + /// name of the forked repository + pub name: Option, + /// organization name, if forking into an organization + pub organization: Option, +} + +/// CreateGPGKeyOption options create user GPG key +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CreateGPGKeyOption { + /// An armored GPG key to add + pub armored_public_key: String, + pub armored_signature: Option, +} + +/// CreateHookOption options when create a hook +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CreateHookOption { + pub active: Option, + pub authorization_header: Option, + pub branch_filter: Option, + pub config: CreateHookOptionConfig, + pub events: Option>, + #[serde(rename = "type")] + pub r#type: CreateHookOptionType, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum CreateHookOptionType { + #[serde(rename = "forgejo")] + Forgejo, + #[serde(rename = "dingtalk")] + Dingtalk, + #[serde(rename = "discord")] + Discord, + #[serde(rename = "gitea")] + Gitea, + #[serde(rename = "gogs")] + Gogs, + #[serde(rename = "msteams")] + Msteams, + #[serde(rename = "slack")] + Slack, + #[serde(rename = "telegram")] + Telegram, + #[serde(rename = "feishu")] + Feishu, + #[serde(rename = "wechatwork")] + Wechatwork, + #[serde(rename = "packagist")] + Packagist, +} +/// CreateHookOptionConfig has all config options in it +/// +/// required are "content_type" and "url" Required +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CreateHookOptionConfig { + #[serde(flatten)] + pub additional: BTreeMap, +} + +/// CreateIssueCommentOption options for creating a comment on an issue +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CreateIssueCommentOption { + pub body: String, + #[serde(with = "time::serde::rfc3339::option")] + pub updated_at: Option, +} + +/// CreateIssueOption options to create one issue +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CreateIssueOption { + /// deprecated + pub assignee: Option, + pub assignees: Option>, + pub body: Option, + pub closed: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub due_date: Option, + /// list of label ids + pub labels: Option>, + /// milestone id + pub milestone: Option, + #[serde(rename = "ref")] + pub r#ref: Option, + pub title: String, +} + +/// CreateKeyOption options when creating a key +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CreateKeyOption { + /// An armored SSH key to add + pub key: String, + /// Describe if the key has only read access or read/write + pub read_only: Option, + /// Title of the key to add + pub title: String, +} + +/// CreateLabelOption options for creating a label +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CreateLabelOption { + pub color: String, + pub description: Option, + pub exclusive: Option, + pub is_archived: Option, + pub name: String, +} + +/// CreateMilestoneOption options for creating a milestone +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CreateMilestoneOption { + pub description: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub due_on: Option, + pub state: Option, + pub title: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum CreateMilestoneOptionState { + #[serde(rename = "open")] + Open, + #[serde(rename = "closed")] + Closed, +} +/// CreateOAuth2ApplicationOptions holds options to create an oauth2 application +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CreateOAuth2ApplicationOptions { + pub confidential_client: Option, + pub name: Option, + pub redirect_uris: Option>, +} + +/// CreateOrUpdateSecretOption options when creating or updating secret +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CreateOrUpdateSecretOption { + /// Data of the secret to update + pub data: String, +} + +/// CreateOrgOption options for creating an organization +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CreateOrgOption { + pub description: Option, + pub email: Option, + pub full_name: Option, + pub location: Option, + pub repo_admin_change_team_access: Option, + pub username: String, + /// possible values are `public` (default), `limited` or `private` + pub visibility: Option, + pub website: Option, +} + +/// possible values are `public` (default), `limited` or `private` + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum CreateOrgOptionVisibility { + #[serde(rename = "public")] + Public, + #[serde(rename = "limited")] + Limited, + #[serde(rename = "private")] + Private, +} +/// CreatePullRequestOption options when creating a pull request +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CreatePullRequestOption { + pub assignee: Option, + pub assignees: Option>, + pub base: Option, + pub body: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub due_date: Option, + pub head: Option, + pub labels: Option>, + pub milestone: Option, + pub title: Option, +} + +/// CreatePullReviewComment represent a review comment for creation api +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CreatePullReviewComment { + pub body: Option, + /// if comment to new file line or 0 + pub new_position: Option, + /// if comment to old file line or 0 + pub old_position: Option, + /// the tree path + pub path: Option, +} + +/// CreatePullReviewOptions are options to create a pull review +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CreatePullReviewOptions { + pub body: Option, + pub comments: Option>, + pub commit_id: Option, + pub event: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CreatePushMirrorOption { + pub interval: Option, + pub remote_address: Option, + pub remote_password: Option, + pub remote_username: Option, + pub sync_on_commit: Option, +} + +/// CreateReleaseOption options when creating a release +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CreateReleaseOption { + pub body: Option, + pub draft: Option, + pub name: Option, + pub prerelease: Option, + pub tag_name: String, + pub target_commitish: Option, +} + +/// CreateRepoOption options when creating repository +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CreateRepoOption { + /// Whether the repository should be auto-initialized? + pub auto_init: Option, + /// DefaultBranch of the repository (used when initializes and in template) + pub default_branch: Option, + /// Description of the repository to create + pub description: Option, + /// Gitignores to use + pub gitignores: Option, + /// Label-Set to use + pub issue_labels: Option, + /// License to use + pub license: Option, + /// Name of the repository to create + pub name: String, + /// Whether the repository is private + pub private: Option, + /// Readme of the repository to create + pub readme: Option, + /// Whether the repository is template + pub template: Option, + /// TrustModel of the repository + pub trust_model: Option, +} + +/// TrustModel of the repository + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum CreateRepoOptionTrustModel { + #[serde(rename = "default")] + Default, + #[serde(rename = "collaborator")] + Collaborator, + #[serde(rename = "committer")] + Committer, + #[serde(rename = "collaboratorcommitter")] + Collaboratorcommitter, +} +/// CreateStatusOption holds the information needed to create a new CommitStatus for a Commit +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CreateStatusOption { + pub context: Option, + pub description: Option, + pub state: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub target_url: Option, +} + +/// CreateTagOption options when creating a tag +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CreateTagOption { + pub message: Option, + pub tag_name: String, + pub target: Option, +} + +/// CreateTeamOption options for creating a team +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CreateTeamOption { + pub can_create_org_repo: Option, + pub description: Option, + pub includes_all_repositories: Option, + pub name: String, + pub permission: Option, + pub units: Option>, + pub units_map: Option>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum CreateTeamOptionPermission { + #[serde(rename = "read")] + Read, + #[serde(rename = "write")] + Write, + #[serde(rename = "admin")] + Admin, +} +/// CreateUserOption create user options +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CreateUserOption { + #[serde(with = "time::serde::rfc3339::option")] + /// For explicitly setting the user creation timestamp. Useful when users are + /// + /// migrated from other systems. When omitted, the user's creation timestamp + /// + /// will be set to "now". + pub created_at: Option, + pub email: String, + pub full_name: Option, + pub login_name: Option, + pub must_change_password: Option, + pub password: Option, + pub restricted: Option, + pub send_notify: Option, + pub source_id: Option, + pub username: String, + pub visibility: Option, +} + +/// CreateWikiPageOptions form for creating wiki +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CreateWikiPageOptions { + /// content must be base64 encoded + pub content_base64: Option, + /// optional commit message summarizing the change + pub message: Option, + /// page title. leave empty to keep unchanged + pub title: Option, +} + +/// Cron represents a Cron task +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct Cron { + pub exec_times: Option, + pub name: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub next: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub prev: Option, + pub schedule: Option, +} + +/// DeleteEmailOption options when deleting email addresses +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct DeleteEmailOption { + /// email addresses to delete + pub emails: Option>, +} + +/// DeleteFileOptions options for deleting files (used for other File structs below) +/// +/// Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used) +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct DeleteFileOptions { + pub author: Option, + /// branch (optional) to base this file from. if not given, the default branch is used + pub branch: Option, + pub committer: Option, + pub dates: Option, + /// message (optional) for the commit of this file. if not supplied, a default message will be used + pub message: Option, + /// new_branch (optional) will make a new branch from `branch` before creating the file + pub new_branch: Option, + /// sha is the SHA for the file that already exists + pub sha: String, + /// Add a Signed-off-by trailer by the committer at the end of the commit log message. + pub signoff: Option, +} + +/// DeleteLabelOption options for deleting a label +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct DeleteLabelsOption { + #[serde(with = "time::serde::rfc3339::option")] + pub updated_at: Option, +} + +/// DeployKey a deploy key +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct DeployKey { + #[serde(with = "time::serde::rfc3339::option")] + pub created_at: Option, + pub fingerprint: Option, + pub id: Option, + pub key: Option, + pub key_id: Option, + pub read_only: Option, + pub repository: Option, + pub title: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub url: Option, +} + +/// DismissPullReviewOptions are options to dismiss a pull review +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct DismissPullReviewOptions { + pub message: Option, + pub priors: Option, +} + +/// EditAttachmentOptions options for editing attachments +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct EditAttachmentOptions { + pub name: Option, +} + +/// EditBranchProtectionOption options for editing a branch protection +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct EditBranchProtectionOption { + pub approvals_whitelist_teams: Option>, + pub approvals_whitelist_username: Option>, + pub block_on_official_review_requests: Option, + pub block_on_outdated_branch: Option, + pub block_on_rejected_reviews: Option, + pub dismiss_stale_approvals: Option, + pub enable_approvals_whitelist: Option, + pub enable_merge_whitelist: Option, + pub enable_push: Option, + pub enable_push_whitelist: Option, + pub enable_status_check: Option, + pub merge_whitelist_teams: Option>, + pub merge_whitelist_usernames: Option>, + pub protected_file_patterns: Option, + pub push_whitelist_deploy_keys: Option, + pub push_whitelist_teams: Option>, + pub push_whitelist_usernames: Option>, + pub require_signed_commits: Option, + pub required_approvals: Option, + pub status_check_contexts: Option>, + pub unprotected_file_patterns: Option, +} + +/// EditDeadlineOption options for creating a deadline +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct EditDeadlineOption { + #[serde(with = "time::serde::rfc3339")] + pub due_date: time::OffsetDateTime, +} + +/// EditGitHookOption options when modifying one Git hook +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct EditGitHookOption { + pub content: Option, +} + +/// EditHookOption options when modify one hook +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct EditHookOption { + pub active: Option, + pub authorization_header: Option, + pub branch_filter: Option, + pub config: Option>, + pub events: Option>, +} + +/// EditIssueCommentOption options for editing a comment +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct EditIssueCommentOption { + pub body: String, + #[serde(with = "time::serde::rfc3339::option")] + pub updated_at: Option, +} + +/// EditIssueOption options for editing an issue +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct EditIssueOption { + /// deprecated + pub assignee: Option, + pub assignees: Option>, + pub body: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub due_date: Option, + pub milestone: Option, + #[serde(rename = "ref")] + pub r#ref: Option, + pub state: Option, + pub title: Option, + pub unset_due_date: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub updated_at: Option, +} + +/// EditLabelOption options for editing a label +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct EditLabelOption { + pub color: Option, + pub description: Option, + pub exclusive: Option, + pub is_archived: Option, + pub name: Option, +} + +/// EditMilestoneOption options for editing a milestone +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct EditMilestoneOption { + pub description: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub due_on: Option, + pub state: Option, + pub title: Option, +} + +/// EditOrgOption options for editing an organization +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct EditOrgOption { + pub description: Option, + pub email: Option, + pub full_name: Option, + pub location: Option, + pub repo_admin_change_team_access: Option, + /// possible values are `public`, `limited` or `private` + pub visibility: Option, + pub website: Option, +} + +/// possible values are `public`, `limited` or `private` + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum EditOrgOptionVisibility { + #[serde(rename = "public")] + Public, + #[serde(rename = "limited")] + Limited, + #[serde(rename = "private")] + Private, +} +/// EditPullRequestOption options when modify pull request +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct EditPullRequestOption { + pub allow_maintainer_edit: Option, + pub assignee: Option, + pub assignees: Option>, + pub base: Option, + pub body: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub due_date: Option, + pub labels: Option>, + pub milestone: Option, + pub state: Option, + pub title: Option, + pub unset_due_date: Option, +} + +/// EditReactionOption contain the reaction type +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct EditReactionOption { + pub content: Option, +} + +/// EditReleaseOption options when editing a release +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct EditReleaseOption { + pub body: Option, + pub draft: Option, + pub name: Option, + pub prerelease: Option, + pub tag_name: Option, + pub target_commitish: Option, +} + +/// EditRepoOption options when editing a repository's properties +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct EditRepoOption { + /// either `true` to allow mark pr as merged manually, or `false` to prevent it. + pub allow_manual_merge: Option, + /// either `true` to allow merging pull requests with a merge commit, or `false` to prevent merging pull requests with merge commits. + pub allow_merge_commits: Option, + /// either `true` to allow rebase-merging pull requests, or `false` to prevent rebase-merging. + pub allow_rebase: Option, + /// either `true` to allow rebase with explicit merge commits (--no-ff), or `false` to prevent rebase with explicit merge commits. + pub allow_rebase_explicit: Option, + /// either `true` to allow updating pull request branch by rebase, or `false` to prevent it. + pub allow_rebase_update: Option, + /// either `true` to allow squash-merging pull requests, or `false` to prevent squash-merging. + pub allow_squash_merge: Option, + /// set to `true` to archive this repository. + pub archived: Option, + /// either `true` to enable AutodetectManualMerge, or `false` to prevent it. Note: In some special cases, misjudgments can occur. + pub autodetect_manual_merge: Option, + /// set to `true` to allow edits from maintainers by default + pub default_allow_maintainer_edit: Option, + /// sets the default branch for this repository. + pub default_branch: Option, + /// set to `true` to delete pr branch after merge by default + pub default_delete_branch_after_merge: Option, + /// set to a merge style to be used by this repository: "merge", "rebase", "rebase-merge", or "squash". + pub default_merge_style: Option, + /// a short description of the repository. + pub description: Option, + /// enable prune - remove obsolete remote-tracking references + pub enable_prune: Option, + pub external_tracker: Option, + pub external_wiki: Option, + /// either `true` to enable actions unit, or `false` to disable them. + pub has_actions: Option, + /// either `true` to enable issues for this repository or `false` to disable them. + pub has_issues: Option, + /// either `true` to enable packages unit, or `false` to disable them. + pub has_packages: Option, + /// either `true` to enable project unit, or `false` to disable them. + pub has_projects: Option, + /// either `true` to allow pull requests, or `false` to prevent pull request. + pub has_pull_requests: Option, + /// either `true` to enable releases unit, or `false` to disable them. + pub has_releases: Option, + /// either `true` to enable the wiki for this repository or `false` to disable it. + pub has_wiki: Option, + /// either `true` to ignore whitespace for conflicts, or `false` to not ignore whitespace. + pub ignore_whitespace_conflicts: Option, + pub internal_tracker: Option, + /// set to a string like `8h30m0s` to set the mirror interval time + pub mirror_interval: Option, + /// name of the repository + pub name: Option, + /// either `true` to make the repository private or `false` to make it public. + /// + /// Note: you will get a 422 error if the organization restricts changing repository visibility to organization + /// + /// owners and a non-owner tries to change the value of private. + pub private: Option, + /// either `true` to make this repository a template or `false` to make it a normal repository + pub template: Option, + /// a URL with more information about the repository. + pub website: Option, +} + +/// EditTeamOption options for editing a team +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct EditTeamOption { + pub can_create_org_repo: Option, + pub description: Option, + pub includes_all_repositories: Option, + pub name: String, + pub permission: Option, + pub units: Option>, + pub units_map: Option>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum EditTeamOptionPermission { + #[serde(rename = "read")] + Read, + #[serde(rename = "write")] + Write, + #[serde(rename = "admin")] + Admin, +} +/// EditUserOption edit user options +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct EditUserOption { + pub active: Option, + pub admin: Option, + pub allow_create_organization: Option, + pub allow_git_hook: Option, + pub allow_import_local: Option, + pub description: Option, + pub email: Option, + pub full_name: Option, + pub location: Option, + pub login_name: String, + pub max_repo_creation: Option, + pub must_change_password: Option, + pub password: Option, + pub prohibit_login: Option, + pub restricted: Option, + pub source_id: u64, + pub visibility: Option, + pub website: Option, +} + +/// Email an email address belonging to a user +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct Email { + pub email: Option, + pub primary: Option, + pub user_id: Option, + pub username: Option, + pub verified: Option, +} + +/// ExternalTracker represents settings for external tracker +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct ExternalTracker { + /// External Issue Tracker URL Format. Use the placeholders {user}, {repo} and {index} for the username, repository name and issue index. + pub external_tracker_format: Option, + /// External Issue Tracker issue regular expression + pub external_tracker_regexp_pattern: Option, + /// External Issue Tracker Number Format, either `numeric`, `alphanumeric`, or `regexp` + pub external_tracker_style: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + /// URL of external issue tracker. + pub external_tracker_url: Option, +} + +/// ExternalWiki represents setting for external wiki +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct ExternalWiki { + #[serde(deserialize_with = "crate::none_if_blank_url")] + /// URL of external wiki. + pub external_wiki_url: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct FileCommitResponse { + pub author: Option, + pub committer: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub created: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub html_url: Option, + pub message: Option, + pub parents: Option>, + pub sha: Option, + pub tree: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub url: Option, +} + +/// FileDeleteResponse contains information about a repo's file that was deleted +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct FileDeleteResponse { + pub commit: Option, + pub content: Option, + pub verification: Option, +} + +/// FileLinksResponse contains the links for a repo's file +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct FileLinksResponse { + pub git: Option, + pub html: Option, + #[serde(rename = "self")] + pub this: Option, +} + +/// FileResponse contains information about a repo's file +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct FileResponse { + pub commit: Option, + pub content: Option, + pub verification: Option, +} + +/// FilesResponse contains information about multiple files from a repo +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct FilesResponse { + pub commit: Option, + pub files: Option>, + pub verification: Option, +} + +/// GPGKey a user GPG key to sign commit and tag in repository +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct GPGKey { + pub can_certify: Option, + pub can_encrypt_comms: Option, + pub can_encrypt_storage: Option, + pub can_sign: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub created_at: Option, + pub emails: Option>, + #[serde(with = "time::serde::rfc3339::option")] + pub expires_at: Option, + pub id: Option, + pub key_id: Option, + pub primary_key_id: Option, + pub public_key: Option, + pub subkeys: Option>, + pub verified: Option, +} + +/// GPGKeyEmail an email attached to a GPGKey +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct GPGKeyEmail { + pub email: Option, + pub verified: Option, +} + +/// GeneralAPISettings contains global api settings exposed by it +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct GeneralAPISettings { + pub default_git_trees_per_page: Option, + pub default_max_blob_size: Option, + pub default_paging_num: Option, + pub max_response_items: Option, +} + +/// GeneralAttachmentSettings contains global Attachment settings exposed by API +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct GeneralAttachmentSettings { + pub allowed_types: Option, + pub enabled: Option, + pub max_files: Option, + pub max_size: Option, +} + +/// GeneralRepoSettings contains global repository settings exposed by API +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct GeneralRepoSettings { + pub http_git_disabled: Option, + pub lfs_disabled: Option, + pub migrations_disabled: Option, + pub mirrors_disabled: Option, + pub stars_disabled: Option, + pub time_tracking_disabled: Option, +} + +/// GeneralUISettings contains global ui settings exposed by API +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct GeneralUISettings { + pub allowed_reactions: Option>, + pub custom_emojis: Option>, + pub default_theme: Option, +} + +/// GenerateRepoOption options when creating repository using a template +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct GenerateRepoOption { + /// include avatar of the template repo + pub avatar: Option, + /// Default branch of the new repository + pub default_branch: Option, + /// Description of the repository to create + pub description: Option, + /// include git content of default branch in template repo + pub git_content: Option, + /// include git hooks in template repo + pub git_hooks: Option, + /// include labels in template repo + pub labels: Option, + /// Name of the repository to create + pub name: String, + /// The organization or person who will own the new repository + pub owner: String, + /// Whether the repository is private + pub private: Option, + /// include protected branches in template repo + pub protected_branch: Option, + /// include topics in template repo + pub topics: Option, + /// include webhooks in template repo + pub webhooks: Option, +} + +/// GitBlobResponse represents a git blob +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct GitBlobResponse { + pub content: Option, + pub encoding: Option, + pub sha: Option, + pub size: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub url: Option, +} + +/// GitEntry represents a git tree +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct GitEntry { + pub mode: Option, + pub path: Option, + pub sha: Option, + pub size: Option, + #[serde(rename = "type")] + pub r#type: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub url: Option, +} + +/// GitHook represents a Git repository hook +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct GitHook { + pub content: Option, + pub is_active: Option, + pub name: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct GitObject { + pub sha: Option, + #[serde(rename = "type")] + pub r#type: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub url: Option, +} + +/// GitTreeResponse returns a git tree +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct GitTreeResponse { + pub page: Option, + pub sha: Option, + pub total_count: Option, + pub tree: Option>, + pub truncated: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub url: Option, +} + +/// GitignoreTemplateInfo name and text of a gitignore template +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct GitignoreTemplateInfo { + pub name: Option, + pub source: Option, +} + +/// Hook a hook is a web hook when one repository changed +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct Hook { + pub active: Option, + pub authorization_header: Option, + pub branch_filter: Option, + pub config: Option>, + #[serde(with = "time::serde::rfc3339::option")] + pub created_at: Option, + pub events: Option>, + pub id: Option, + #[serde(rename = "type")] + pub r#type: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub updated_at: Option, +} + +/// Identity for a person's identity like an author or committer +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct Identity { + pub email: Option, + pub name: Option, +} + +/// InternalTracker represents settings for internal tracker +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct InternalTracker { + /// Let only contributors track time (Built-in issue tracker) + pub allow_only_contributors_to_track_time: Option, + /// Enable dependencies for issues and pull requests (Built-in issue tracker) + pub enable_issue_dependencies: Option, + /// Enable time tracking (Built-in issue tracker) + pub enable_time_tracker: Option, +} + +/// Issue represents an issue in a repository +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct Issue { + pub assets: Option>, + pub assignee: Option, + pub assignees: Option>, + pub body: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub closed_at: Option, + pub comments: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub created_at: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub due_date: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub html_url: Option, + pub id: Option, + pub is_locked: Option, + pub labels: Option>, + pub milestone: Option, + pub number: Option, + pub original_author: Option, + pub original_author_id: Option, + pub pin_order: Option, + pub pull_request: Option, + #[serde(rename = "ref")] + pub r#ref: Option, + pub repository: Option, + pub state: Option, + pub title: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub updated_at: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub url: Option, + pub user: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct IssueConfig { + pub blank_issues_enabled: Option, + pub contact_links: Option>, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct IssueConfigContactLink { + pub about: Option, + pub name: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub url: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct IssueConfigValidation { + pub message: Option, + pub valid: Option, +} + +/// IssueDeadline represents an issue deadline +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct IssueDeadline { + #[serde(with = "time::serde::rfc3339::option")] + pub due_date: Option, +} + +/// IssueFormField represents a form field +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct IssueFormField { + pub attributes: Option>, + pub id: Option, + #[serde(rename = "type")] + pub r#type: Option, + pub validations: Option>, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct IssueFormFieldType {} + +/// IssueLabelsOption a collection of labels +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct IssueLabelsOption { + /// list of label IDs + pub labels: Option>, + #[serde(with = "time::serde::rfc3339::option")] + pub updated_at: Option, +} + +/// IssueMeta basic issue information +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct IssueMeta { + pub index: Option, + pub owner: Option, + pub repo: Option, +} + +/// IssueTemplate represents an issue template for a repository +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct IssueTemplate { + pub about: Option, + pub body: Option>, + pub content: Option, + pub file_name: Option, + pub labels: Option>, + pub name: Option, + #[serde(rename = "ref")] + pub r#ref: Option, + pub title: Option, +} + +/// Label a label to an issue or a pr +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct Label { + pub color: Option, + pub description: Option, + pub exclusive: Option, + pub id: Option, + pub is_archived: Option, + pub name: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub url: Option, +} + +/// LabelTemplate info of a Label template +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct LabelTemplate { + pub color: Option, + pub description: Option, + pub exclusive: Option, + pub name: Option, +} + +/// LicensesInfo contains information about a License +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct LicenseTemplateInfo { + pub body: Option, + pub implementation: Option, + pub key: Option, + pub name: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub url: Option, +} + +/// LicensesListEntry is used for the API +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct LicensesTemplateListEntry { + pub key: Option, + pub name: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub url: Option, +} + +/// MarkdownOption markdown options +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct MarkdownOption { + /// Context to render + /// + /// + /// + /// in: body + #[serde(rename = "Context")] + pub context: Option, + /// Mode to render (comment, gfm, markdown) + /// + /// + /// + /// in: body + #[serde(rename = "Mode")] + pub mode: Option, + /// Text markdown to render + /// + /// + /// + /// in: body + #[serde(rename = "Text")] + pub text: Option, + /// Is it a wiki page ? + /// + /// + /// + /// in: body + #[serde(rename = "Wiki")] + pub wiki: Option, +} + +/// MarkupOption markup options +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct MarkupOption { + /// Context to render + /// + /// + /// + /// in: body + #[serde(rename = "Context")] + pub context: Option, + /// File path for detecting extension in file mode + /// + /// + /// + /// in: body + #[serde(rename = "FilePath")] + pub file_path: Option, + /// Mode to render (comment, gfm, markdown, file) + /// + /// + /// + /// in: body + #[serde(rename = "Mode")] + pub mode: Option, + /// Text markup to render + /// + /// + /// + /// in: body + #[serde(rename = "Text")] + pub text: Option, + /// Is it a wiki page ? + /// + /// + /// + /// in: body + #[serde(rename = "Wiki")] + pub wiki: Option, +} + +/// MergePullRequestForm form for merging Pull Request +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct MergePullRequestOption { + #[serde(rename = "Do")] + pub r#do: MergePullRequestOptionDo, + #[serde(rename = "MergeCommitID")] + pub merge_commit_id: Option, + #[serde(rename = "MergeMessageField")] + pub merge_message_field: Option, + #[serde(rename = "MergeTitleField")] + pub merge_title_field: Option, + pub delete_branch_after_merge: Option, + pub force_merge: Option, + pub head_commit_id: Option, + pub merge_when_checks_succeed: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum MergePullRequestOptionDo { + #[serde(rename = "merge")] + Merge, + #[serde(rename = "rebase")] + Rebase, + #[serde(rename = "rebase-merge")] + RebaseMerge, + #[serde(rename = "squash")] + Squash, + #[serde(rename = "manually-merged")] + ManuallyMerged, +} +/// MigrateRepoOptions options for migrating repository's +/// +/// this is used to interact with api v1 +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct MigrateRepoOptions { + pub auth_password: Option, + pub auth_token: Option, + pub auth_username: Option, + pub clone_addr: String, + pub description: Option, + pub issues: Option, + pub labels: Option, + pub lfs: Option, + pub lfs_endpoint: Option, + pub milestones: Option, + pub mirror: Option, + pub mirror_interval: Option, + pub private: Option, + pub pull_requests: Option, + pub releases: Option, + pub repo_name: String, + /// Name of User or Organisation who will own Repo after migration + pub repo_owner: Option, + pub service: Option, + /// deprecated (only for backwards compatibility) + pub uid: Option, + pub wiki: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum MigrateRepoOptionsService { + #[serde(rename = "git")] + Git, + #[serde(rename = "github")] + Github, + #[serde(rename = "gitea")] + Gitea, + #[serde(rename = "gitlab")] + Gitlab, + #[serde(rename = "gogs")] + Gogs, + #[serde(rename = "onedev")] + Onedev, + #[serde(rename = "gitbucket")] + Gitbucket, + #[serde(rename = "codebase")] + Codebase, +} +/// Milestone milestone is a collection of issues on one repository +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct Milestone { + #[serde(with = "time::serde::rfc3339::option")] + pub closed_at: Option, + pub closed_issues: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub created_at: Option, + pub description: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub due_on: Option, + pub id: Option, + pub open_issues: Option, + pub state: Option, + pub title: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub updated_at: Option, +} + +/// NewIssuePinsAllowed represents an API response that says if new Issue Pins are allowed +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct NewIssuePinsAllowed { + pub issues: Option, + pub pull_requests: Option, +} + +/// NodeInfo contains standardized way of exposing metadata about a server running one of the distributed social networks +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct NodeInfo { + pub metadata: Option>, + #[serde(rename = "openRegistrations")] + pub open_registrations: Option, + pub protocols: Option>, + pub services: Option, + pub software: Option, + pub usage: Option, + pub version: Option, +} + +/// NodeInfoServices contains the third party sites this server can connect to via their application API +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct NodeInfoServices { + pub inbound: Option>, + pub outbound: Option>, +} + +/// NodeInfoSoftware contains Metadata about server software in use +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct NodeInfoSoftware { + pub homepage: Option, + pub name: Option, + pub repository: Option, + pub version: Option, +} + +/// NodeInfoUsage contains usage statistics for this server +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct NodeInfoUsage { + #[serde(rename = "localComments")] + pub local_comments: Option, + #[serde(rename = "localPosts")] + pub local_posts: Option, + pub users: Option, +} + +/// NodeInfoUsageUsers contains statistics about the users of this server +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct NodeInfoUsageUsers { + #[serde(rename = "activeHalfyear")] + pub active_halfyear: Option, + #[serde(rename = "activeMonth")] + pub active_month: Option, + pub total: Option, +} + +/// Note contains information related to a git note +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct Note { + pub commit: Option, + pub message: Option, +} + +/// NotificationCount number of unread notifications +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct NotificationCount { + pub new: Option, +} + +/// NotificationSubject contains the notification subject (Issue/Pull/Commit) +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct NotificationSubject { + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub html_url: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub latest_comment_html_url: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub latest_comment_url: Option, + pub state: Option, + pub title: Option, + #[serde(rename = "type")] + pub r#type: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub url: Option, +} + +/// NotificationThread expose Notification on API +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct NotificationThread { + pub id: Option, + pub pinned: Option, + pub repository: Option, + pub subject: Option, + pub unread: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub updated_at: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub url: Option, +} + +/// NotifySubjectType represent type of notification subject +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct NotifySubjectType {} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct OAuth2Application { + pub client_id: Option, + pub client_secret: Option, + pub confidential_client: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub created: Option, + pub id: Option, + pub name: Option, + pub redirect_uris: Option>, +} + +/// Organization represents an organization +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct Organization { + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub avatar_url: Option, + pub description: Option, + pub email: Option, + pub full_name: Option, + pub id: Option, + pub location: Option, + pub name: Option, + pub repo_admin_change_team_access: Option, + /// deprecated + pub username: Option, + pub visibility: Option, + pub website: Option, +} + +/// OrganizationPermissions list different users permissions on an organization +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct OrganizationPermissions { + pub can_create_repository: Option, + pub can_read: Option, + pub can_write: Option, + pub is_admin: Option, + pub is_owner: Option, +} + +/// PRBranchInfo information about a branch +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct PRBranchInfo { + pub label: Option, + #[serde(rename = "ref")] + pub r#ref: Option, + pub repo: Option, + pub repo_id: Option, + pub sha: Option, +} + +/// Package represents a package +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct Package { + #[serde(with = "time::serde::rfc3339::option")] + pub created_at: Option, + pub creator: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub html_url: Option, + pub id: Option, + pub name: Option, + pub owner: Option, + pub repository: Option, + #[serde(rename = "type")] + pub r#type: Option, + pub version: Option, +} + +/// PackageFile represents a package file +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct PackageFile { + #[serde(rename = "Size")] + pub size: Option, + pub id: Option, + pub md5: Option, + pub name: Option, + pub sha1: Option, + pub sha256: Option, + pub sha512: Option, +} + +/// PayloadCommit represents a commit +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct PayloadCommit { + pub added: Option>, + pub author: Option, + pub committer: Option, + /// sha1 hash of the commit + pub id: Option, + pub message: Option, + pub modified: Option>, + pub removed: Option>, + #[serde(with = "time::serde::rfc3339::option")] + pub timestamp: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub url: Option, + pub verification: Option, +} + +/// PayloadCommitVerification represents the GPG verification of a commit +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct PayloadCommitVerification { + pub payload: Option, + pub reason: Option, + pub signature: Option, + pub signer: Option, + pub verified: Option, +} + +/// PayloadUser represents the author or committer of a commit +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct PayloadUser { + pub email: Option, + /// Full name of the commit author + pub name: Option, + pub username: Option, +} + +/// Permission represents a set of permissions +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct Permission { + pub admin: Option, + pub pull: Option, + pub push: Option, +} + +/// PublicKey publickey is a user key to push code to repository +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct PublicKey { + #[serde(with = "time::serde::rfc3339::option")] + pub created_at: Option, + pub fingerprint: Option, + pub id: Option, + pub key: Option, + pub key_type: Option, + pub read_only: Option, + pub title: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub url: Option, + pub user: Option, +} + +/// PullRequest represents a pull request +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct PullRequest { + pub allow_maintainer_edit: Option, + pub assignee: Option, + pub assignees: Option>, + pub base: Option, + pub body: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub closed_at: Option, + pub comments: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub created_at: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub diff_url: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub due_date: Option, + pub head: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub html_url: Option, + pub id: Option, + pub is_locked: Option, + pub labels: Option>, + pub merge_base: Option, + pub merge_commit_sha: Option, + pub mergeable: Option, + pub merged: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub merged_at: Option, + pub merged_by: Option, + pub milestone: Option, + pub number: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub patch_url: Option, + pub pin_order: Option, + pub requested_reviewers: Option>, + pub state: Option, + pub title: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub updated_at: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub url: Option, + pub user: Option, +} + +/// PullRequestMeta PR info if an issue is a PR +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct PullRequestMeta { + pub merged: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub merged_at: Option, +} + +/// PullReview represents a pull request review +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct PullReview { + pub body: Option, + pub comments_count: Option, + pub commit_id: Option, + pub dismissed: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub html_url: Option, + pub id: Option, + pub official: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub pull_request_url: Option, + pub stale: Option, + pub state: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub submitted_at: Option, + pub team: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub updated_at: Option, + pub user: Option, +} + +/// PullReviewComment represents a comment on a pull request review +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct PullReviewComment { + pub body: Option, + pub commit_id: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub created_at: Option, + pub diff_hunk: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub html_url: Option, + pub id: Option, + pub original_commit_id: Option, + pub original_position: Option, + pub path: Option, + pub position: Option, + pub pull_request_review_id: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub pull_request_url: Option, + pub resolver: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub updated_at: Option, + pub user: Option, +} + +/// PullReviewRequestOptions are options to add or remove pull review requests +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct PullReviewRequestOptions { + pub reviewers: Option>, + pub team_reviewers: Option>, +} + +/// PushMirror represents information of a push mirror +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct PushMirror { + pub created: Option, + pub interval: Option, + pub last_error: Option, + pub last_update: Option, + pub remote_address: Option, + pub remote_name: Option, + pub repo_name: Option, + pub sync_on_commit: Option, +} + +/// Reaction contain one reaction +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct Reaction { + pub content: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub created_at: Option, + pub user: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct Reference { + pub object: Option, + #[serde(rename = "ref")] + pub r#ref: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub url: Option, +} + +/// Release represents a repository release +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct Release { + pub assets: Option>, + pub author: Option, + pub body: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub created_at: Option, + pub draft: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub html_url: Option, + pub id: Option, + pub name: Option, + pub prerelease: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub published_at: Option, + pub tag_name: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub tarball_url: Option, + pub target_commitish: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub upload_url: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub url: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub zipball_url: Option, +} + +/// RenameUserOption options when renaming a user +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct RenameUserOption { + /// New username for this user. This name cannot be in use yet by any other user. + pub new_username: String, +} + +/// RepoCollaboratorPermission to get repository permission for a collaborator +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct RepoCollaboratorPermission { + pub permission: Option, + pub role_name: Option, + pub user: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct RepoCommit { + pub author: Option, + pub committer: Option, + pub message: Option, + pub tree: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub url: Option, + pub verification: Option, +} + +/// RepoTopicOptions a collection of repo topic names +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct RepoTopicOptions { + /// list of topic names + pub topics: Option>, +} + +/// RepoTransfer represents a pending repo transfer +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct RepoTransfer { + pub doer: Option, + pub recipient: Option, + pub teams: Option>, +} + +/// Repository represents a repository +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct Repository { + pub allow_merge_commits: Option, + pub allow_rebase: Option, + pub allow_rebase_explicit: Option, + pub allow_rebase_update: Option, + pub allow_squash_merge: Option, + pub archived: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub archived_at: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub avatar_url: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub clone_url: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub created_at: Option, + pub default_allow_maintainer_edit: Option, + pub default_branch: Option, + pub default_delete_branch_after_merge: Option, + pub default_merge_style: Option, + pub description: Option, + pub empty: Option, + pub external_tracker: Option, + pub external_wiki: Option, + pub fork: Option, + pub forks_count: Option, + pub full_name: Option, + pub has_actions: Option, + pub has_issues: Option, + pub has_packages: Option, + pub has_projects: Option, + pub has_pull_requests: Option, + pub has_releases: Option, + pub has_wiki: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub html_url: Option, + pub id: Option, + pub ignore_whitespace_conflicts: Option, + pub internal: Option, + pub internal_tracker: Option, + pub language: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub languages_url: Option, + pub link: Option, + pub mirror: Option, + pub mirror_interval: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub mirror_updated: Option, + pub name: Option, + pub open_issues_count: Option, + pub open_pr_counter: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub original_url: Option, + pub owner: Option, + pub parent: Option>, + pub permissions: Option, + pub private: Option, + pub release_counter: Option, + pub repo_transfer: Option, + pub size: Option, + pub ssh_url: Option, + pub stars_count: Option, + pub template: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub updated_at: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub url: Option, + pub watchers_count: Option, + pub website: Option, +} + +/// RepositoryMeta basic repository information +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct RepositoryMeta { + pub full_name: Option, + pub id: Option, + pub name: Option, + pub owner: Option, +} + +/// ReviewStateType review state type +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct ReviewStateType {} + +/// SearchResults results of a successful search +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct SearchResults { + pub data: Option>, + pub ok: Option, +} + +/// Secret represents a secret +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct Secret { + #[serde(with = "time::serde::rfc3339::option")] + pub created_at: Option, + /// the secret's name + pub name: Option, +} + +/// ServerVersion wraps the version of the server +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct ServerVersion { + pub version: Option, +} + +/// StateType issue state type +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct StateType {} + +/// StopWatch represent a running stopwatch +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct StopWatch { + #[serde(with = "time::serde::rfc3339::option")] + pub created: Option, + pub duration: Option, + pub issue_index: Option, + pub issue_title: Option, + pub repo_name: Option, + pub repo_owner_name: Option, + pub seconds: Option, +} + +/// SubmitPullReviewOptions are options to submit a pending pull review +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct SubmitPullReviewOptions { + pub body: Option, + pub event: Option, +} + +/// Tag represents a repository tag +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct Tag { + pub commit: Option, + pub id: Option, + pub message: Option, + pub name: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub tarball_url: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub zipball_url: Option, +} + +/// Team represents a team in an organization +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct Team { + pub can_create_org_repo: Option, + pub description: Option, + pub id: Option, + pub includes_all_repositories: Option, + pub name: Option, + pub organization: Option, + pub permission: Option, + pub units: Option>, + pub units_map: Option>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum TeamPermission { + #[serde(rename = "none")] + None, + #[serde(rename = "read")] + Read, + #[serde(rename = "write")] + Write, + #[serde(rename = "admin")] + Admin, + #[serde(rename = "owner")] + Owner, +} +/// TimeStamp defines a timestamp +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct TimeStamp {} + +/// TimelineComment represents a timeline comment (comment of any type) on a commit or issue +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct TimelineComment { + pub assignee: Option, + pub assignee_team: Option, + pub body: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub created_at: Option, + pub dependent_issue: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub html_url: Option, + pub id: Option, + #[serde(deserialize_with = "crate::none_if_blank_url")] + pub issue_url: Option, + pub label: Option