Merge pull request 'Auto-generate API' (#38) from autogen into main
Reviewed-on: https://codeberg.org/Cyborus/forgejo-api/pulls/38
This commit is contained in:
commit
96b92daf23
61
Cargo.lock
generated
61
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
workspace = { members = ["generator"] }
|
||||
[package]
|
||||
name = "forgejo-api"
|
||||
version = "0.1.0"
|
||||
|
|
13
generator/Cargo.toml
Normal file
13
generator/Cargo.toml
Normal file
|
@ -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"] }
|
287
generator/src/main.rs
Normal file
287
generator/src/main.rs
Normal file
|
@ -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<OpenApiV2> {
|
||||
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::<OpenApiV2>(&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<Schema>) -> eyre::Result<String> {
|
||||
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<String> {
|
||||
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<String, serde_json::Value>".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(name.to_owned())
|
||||
}
|
||||
SchemaType::List(_) => todo!(),
|
||||
}
|
||||
} else {
|
||||
Ok("serde_json::Value".into())
|
||||
}
|
||||
}
|
||||
|
||||
fn schema_is_string(spec: &OpenApiV2, schema: &MaybeRef<Schema>) -> eyre::Result<bool> {
|
||||
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<bool> {
|
||||
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<String, {additional_ty}>");
|
||||
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<String>,
|
||||
) -> 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(())
|
||||
}
|
612
generator/src/methods.rs
Normal file
612
generator/src/methods.rs
Normal file
|
@ -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<String> {
|
||||
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<String> {
|
||||
let mut s = String::new();
|
||||
if let Some(op) = &item.get {
|
||||
s.push_str(&create_get_method(spec, path, op).wrap_err("GET")?);
|
||||
}
|
||||
if let Some(op) = &item.put {
|
||||
s.push_str(&create_put_method(spec, path, op).wrap_err("PUT")?);
|
||||
}
|
||||
if let Some(op) = &item.post {
|
||||
s.push_str(&create_post_method(spec, path, op).wrap_err("POST")?);
|
||||
}
|
||||
if let Some(op) = &item.delete {
|
||||
s.push_str(&create_delete_method(spec, path, op).wrap_err("DELETE")?);
|
||||
}
|
||||
if let Some(op) = &item.options {
|
||||
s.push_str(&create_options_method(spec, path, op).wrap_err("OPTIONS")?);
|
||||
}
|
||||
if let Some(op) = &item.head {
|
||||
s.push_str(&create_head_method(spec, path, op).wrap_err("HEAD")?);
|
||||
}
|
||||
if let Some(op) = &item.patch {
|
||||
s.push_str(&create_patch_method(spec, path, op).wrap_err("PATCH")?);
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
fn create_get_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> {
|
||||
let doc = method_docs(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<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
let mut out = String::new();
|
||||
let mut prev = false;
|
||||
if let Some(summary) = &op.summary {
|
||||
write!(&mut out, "/// {summary}\n")?;
|
||||
prev = true;
|
||||
}
|
||||
if let Some(params) = &op.parameters {
|
||||
if prev {
|
||||
out.push_str("///\n");
|
||||
}
|
||||
for param in params {
|
||||
let param = 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<String> {
|
||||
let name = op
|
||||
.operation_id
|
||||
.as_deref()
|
||||
.ok_or_else(|| eyre::eyre!("operation did not have id"))?
|
||||
.to_snake_case()
|
||||
.replace("o_auth2", "oauth2");
|
||||
let args = fn_args_from_op(spec, op)?;
|
||||
let ty = fn_return_from_op(spec, op)?;
|
||||
Ok(format!(
|
||||
"pub async fn {name}({args}) -> Result<{ty}, ForgejoError>"
|
||||
))
|
||||
}
|
||||
|
||||
fn fn_args_from_op(spec: &OpenApiV2, op: &Operation) -> eyre::Result<String> {
|
||||
let mut args = "&self".to_string();
|
||||
let mut has_query = false;
|
||||
// let mut has_headers = false;
|
||||
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<u8>");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if has_query {
|
||||
let query_ty = crate::structs::query_struct_name(op)?;
|
||||
args.push_str(", query: ");
|
||||
args.push_str(&query_ty);
|
||||
}
|
||||
Ok(args)
|
||||
}
|
||||
|
||||
pub fn param_type(param: &NonBodyParameter, owned: bool) -> eyre::Result<String> {
|
||||
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<String> {
|
||||
let ty_name = match ty {
|
||||
ParameterType::String => match format.as_deref() {
|
||||
Some("date") => "time::Date",
|
||||
Some("date-time") => "time::OffsetDateTime",
|
||||
_ => {
|
||||
if owned {
|
||||
"String"
|
||||
} else {
|
||||
"&str"
|
||||
}
|
||||
}
|
||||
}
|
||||
.into(),
|
||||
ParameterType::Number => match format.as_deref() {
|
||||
Some("float") => "f32",
|
||||
Some("double") => "f64",
|
||||
_ => "f64",
|
||||
}
|
||||
.into(),
|
||||
ParameterType::Integer => match format.as_deref() {
|
||||
Some("int32") => "u32",
|
||||
Some("int64") => "u64",
|
||||
_ => "u32",
|
||||
}
|
||||
.into(),
|
||||
ParameterType::Boolean => "bool".into(),
|
||||
ParameterType::Array => {
|
||||
let item = items
|
||||
.as_ref()
|
||||
.ok_or_else(|| eyre::eyre!("array must have item type defined"))?;
|
||||
let item_ty_name = param_type_inner(
|
||||
&item._type,
|
||||
item.format.as_deref(),
|
||||
item.items.as_deref(),
|
||||
owned,
|
||||
)?;
|
||||
if owned {
|
||||
format!("Vec<{item_ty_name}>")
|
||||
} else {
|
||||
format!("&[{item_ty_name}]")
|
||||
}
|
||||
}
|
||||
ParameterType::File => {
|
||||
if owned {
|
||||
format!("Vec<u8>")
|
||||
} else {
|
||||
format!("&[u8]")
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(ty_name)
|
||||
}
|
||||
|
||||
fn fn_return_from_op(spec: &OpenApiV2, op: &Operation) -> eyre::Result<ResponseType> {
|
||||
let responses = op
|
||||
.responses
|
||||
.http_codes
|
||||
.iter()
|
||||
.filter(|(k, _)| k.starts_with("2"))
|
||||
.map(|(_, v)| response_ref_type_name(spec, v, op))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let mut iter = responses.into_iter();
|
||||
let mut response = iter
|
||||
.next()
|
||||
.ok_or_else(|| eyre::eyre!("must have at least one response type"))?;
|
||||
for next in iter {
|
||||
response = response.merge(next)?;
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn response_ref_type_name(
|
||||
spec: &OpenApiV2,
|
||||
response_ref: &MaybeRef<Response>,
|
||||
op: &Operation,
|
||||
) -> eyre::Result<ResponseType> {
|
||||
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<u8>".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<String> {
|
||||
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<String> {
|
||||
let mut has_query = false;
|
||||
// let mut has_headers = false;
|
||||
let mut body_method = String::new();
|
||||
if let Some(params) = &op.parameters {
|
||||
for param in params {
|
||||
let param = 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<bool> {
|
||||
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<String> {
|
||||
let mut out = String::new();
|
||||
loop {
|
||||
let (head, tail) = match path.split_once("{") {
|
||||
Some(i) => i,
|
||||
None => {
|
||||
out.push_str(path);
|
||||
break;
|
||||
}
|
||||
};
|
||||
path = tail;
|
||||
out.push_str(head);
|
||||
out.push('{');
|
||||
let (head, tail) = match path.split_once("}") {
|
||||
Some(i) => i,
|
||||
None => {
|
||||
eyre::bail!("unmatched bracket");
|
||||
}
|
||||
};
|
||||
path = tail;
|
||||
out.push_str(&head.to_snake_case());
|
||||
out.push('}');
|
||||
}
|
||||
if out.starts_with("/") {
|
||||
out.remove(0);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn create_method_response(spec: &OpenApiV2, op: &Operation) -> eyre::Result<String> {
|
||||
let mut has_empty = false;
|
||||
let mut only_empty = true;
|
||||
for (code, res) in &op.responses.http_codes {
|
||||
let response = response_ref_type_name(spec, res, 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<String>,
|
||||
body: Option<String>,
|
||||
kind: Option<ResponseKind>,
|
||||
}
|
||||
|
||||
impl ResponseType {
|
||||
fn merge(self, other: Self) -> eyre::Result<Self> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1447
generator/src/openapi.rs
Normal file
1447
generator/src/openapi.rs
Normal file
File diff suppressed because it is too large
Load diff
654
generator/src/structs.rs
Normal file
654
generator/src/structs.rs
Normal file
|
@ -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<String> {
|
||||
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<String> {
|
||||
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<url::Url>" {
|
||||
fields.push_str("#[serde(deserialize_with = \"crate::none_if_blank_url\")]\n");
|
||||
}
|
||||
if field_ty == "time::OffsetDateTime" {
|
||||
fields.push_str("#[serde(with = \"time::serde::rfc3339\")]\n");
|
||||
}
|
||||
if field_ty == "Option<time::OffsetDateTime>" {
|
||||
fields.push_str("#[serde(with = \"time::serde::rfc3339::option\")]\n");
|
||||
}
|
||||
if 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<String, ");
|
||||
fields.push_str(&prop_ty);
|
||||
fields.push_str(">,\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<String> {
|
||||
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<String> {
|
||||
create_struct_docs_str(schema.description.as_deref())
|
||||
}
|
||||
|
||||
fn create_struct_docs_str(description: Option<&str>) -> eyre::Result<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
let mut ty = op
|
||||
.operation_id
|
||||
.as_deref()
|
||||
.ok_or_else(|| eyre::eyre!("operation did not have id"))?
|
||||
.to_pascal_case()
|
||||
.replace("o_auth2", "oauth2");
|
||||
ty.push_str("Query");
|
||||
Ok(ty)
|
||||
}
|
||||
|
||||
fn create_query_struct(spec: &OpenApiV2, op: &Operation) -> eyre::Result<String> {
|
||||
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<String> {
|
||||
let mut out = String::new();
|
||||
|
||||
writeln!(
|
||||
&mut out,
|
||||
"
|
||||
if !{name}.is_empty() {{
|
||||
write!(f, \"{}=\")?;
|
||||
for (item, i) in {name}.iter().enumerate() {{
|
||||
{item_pusher}
|
||||
if i < {name}.len() - 1 {{
|
||||
write!(f, \"{sep}\")?;
|
||||
}}
|
||||
}}
|
||||
write!(f, \"&\")?;
|
||||
}}",
|
||||
param.name
|
||||
)?;
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn create_header_struct(
|
||||
name: &str,
|
||||
headers: &std::collections::BTreeMap<String, Header>,
|
||||
) -> eyre::Result<String> {
|
||||
let ty_name = format!("{name}Headers").to_pascal_case();
|
||||
let mut fields = String::new();
|
||||
let mut imp = String::new();
|
||||
let mut imp_ret = String::new();
|
||||
for (header_name, header) in headers {
|
||||
let ty = header_type(header)?;
|
||||
let field_name = crate::sanitize_ident(header_name);
|
||||
fields.push_str("pub ");
|
||||
fields.push_str(&field_name);
|
||||
fields.push_str(": Option<");
|
||||
fields.push_str(&ty);
|
||||
fields.push_str(">,\n");
|
||||
|
||||
write!(
|
||||
&mut imp,
|
||||
"
|
||||
let {field_name} = map.get(\"{header_name}\").map(|s| -> Result<_, _> {{
|
||||
let s = s.to_str().map_err(|_| StructureError::HeaderNotAscii)?;
|
||||
"
|
||||
)
|
||||
.unwrap();
|
||||
match &header._type {
|
||||
ParameterType::String => imp.push_str("Ok(s.to_string())"),
|
||||
ParameterType::Number => match header.format.as_deref() {
|
||||
Some("float") => {
|
||||
imp.push_str("s.parse::<f32>().map_err(|_| StructureError::HeaderParseFailed)")
|
||||
}
|
||||
Some("double") | _ => {
|
||||
imp.push_str("s.parse::<f64>()).map_err(|_| StructureError::HeaderParseFailed)")
|
||||
}
|
||||
},
|
||||
ParameterType::Integer => match header.format.as_deref() {
|
||||
Some("int64") => {
|
||||
imp.push_str("s.parse::<u64>().map_err(|_| StructureError::HeaderParseFailed)")
|
||||
}
|
||||
Some("int32") | _ => {
|
||||
imp.push_str("s.parse::<u32>().map_err(|_| StructureError::HeaderParseFailed)")
|
||||
}
|
||||
},
|
||||
ParameterType::Boolean => {
|
||||
imp.push_str("s.parse::<bool>().map_err(|_| StructureError::HeaderParseFailed)")
|
||||
}
|
||||
ParameterType::Array => {
|
||||
let sep = match header.collection_format {
|
||||
Some(CollectionFormat::Csv) | None => ",",
|
||||
Some(CollectionFormat::Ssv) => " ",
|
||||
Some(CollectionFormat::Tsv) => "\\t",
|
||||
Some(CollectionFormat::Pipes) => "|",
|
||||
Some(CollectionFormat::Multi) => {
|
||||
eyre::bail!("multi format not supported in headers")
|
||||
}
|
||||
};
|
||||
let items = header
|
||||
.items
|
||||
.as_ref()
|
||||
.ok_or_else(|| eyre::eyre!("items property must be set for arrays"))?;
|
||||
if items._type == ParameterType::String {
|
||||
imp.push_str("Ok(");
|
||||
}
|
||||
imp.push_str("s.split(\"");
|
||||
imp.push_str(sep);
|
||||
imp.push_str("\").map(|s| ");
|
||||
imp.push_str(match items._type {
|
||||
ParameterType::String => "s.to_string()).collect::<Vec<_>>())",
|
||||
ParameterType::Number => match items.format.as_deref() {
|
||||
Some("float") => "s.parse::<f32>()).collect::<Result<Vec<_>, _>>().map_err(|_| StructureError::HeaderParseFailed)",
|
||||
Some("double") | _ => "s.parse::<f64>()).collect::<Result<Vec<_>, _>>().map_err(|_| StructureError::HeaderParseFailed)",
|
||||
},
|
||||
ParameterType::Integer => match items.format.as_deref() {
|
||||
Some("int64") => "s.parse::<u64>()).collect::<Result<Vec<_>, _>>().map_err(|_| StructureError::HeaderParseFailed)",
|
||||
Some("int32") | _ => "s.parse::<u32>()).collect::<Result<Vec<_>, _>>().map_err(|_| StructureError::HeaderParseFailed)",
|
||||
},
|
||||
ParameterType::Boolean => "s.parse::<bool>()).collect::<Result<Vec<_>, _>>().map_err(|_| StructureError::HeaderParseFailed)",
|
||||
ParameterType::Array => eyre::bail!("nested arrays not supported in headers"),
|
||||
ParameterType::File => eyre::bail!("files not supported in headers"),
|
||||
});
|
||||
}
|
||||
ParameterType::File => eyre::bail!("files not supported in headers"),
|
||||
}
|
||||
imp.push_str("}).transpose()?;");
|
||||
|
||||
imp_ret.push_str(&field_name);
|
||||
imp_ret.push_str(", ");
|
||||
}
|
||||
|
||||
Ok(format!(
|
||||
"
|
||||
pub struct {ty_name} {{
|
||||
{fields}
|
||||
}}
|
||||
|
||||
impl TryFrom<&reqwest::header::HeaderMap> for {ty_name} {{
|
||||
type Error = StructureError;
|
||||
|
||||
fn try_from(map: &reqwest::header::HeaderMap) -> Result<Self, Self::Error> {{
|
||||
{imp}
|
||||
Ok(Self {{ {imp_ret} }})
|
||||
}}
|
||||
}}
|
||||
"
|
||||
))
|
||||
}
|
||||
|
||||
pub fn header_type(header: &Header) -> eyre::Result<String> {
|
||||
crate::methods::param_type_inner(
|
||||
&header._type,
|
||||
header.format.as_deref(),
|
||||
header.items.as_ref(),
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn create_response_structs(spec: &OpenApiV2, item: &PathItem) -> eyre::Result<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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)
|
||||
}
|
502
src/admin.rs
502
src/admin.rs
|
@ -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<Vec<Cron>, 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<Vec<Email>, ForgejoError> {
|
||||
self.get(&query.path()).await
|
||||
}
|
||||
|
||||
pub async fn admin_search_emails(
|
||||
&self,
|
||||
query: EmailSearchQuery,
|
||||
) -> Result<Vec<Email>, ForgejoError> {
|
||||
self.get(&query.path()).await
|
||||
}
|
||||
|
||||
pub async fn admin_get_hooks(&self, query: HookQuery) -> Result<Vec<Hook>, ForgejoError> {
|
||||
self.get(&query.path()).await
|
||||
}
|
||||
|
||||
pub async fn admin_create_hook(&self, opt: CreateHookOption) -> Result<Hook, ForgejoError> {
|
||||
self.post("admin/hooks", &opt).await
|
||||
}
|
||||
|
||||
pub async fn admin_get_hook(&self, id: u64) -> Result<Option<Hook>, 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<Hook, ForgejoError> {
|
||||
self.patch(&format!("admin/hooks/{id}"), &opt).await
|
||||
}
|
||||
|
||||
pub async fn admin_get_orgs(
|
||||
&self,
|
||||
query: AdminOrganizationQuery,
|
||||
) -> Result<Vec<Organization>, ForgejoError> {
|
||||
self.get(&query.path()).await
|
||||
}
|
||||
|
||||
pub async fn admin_unadopted_repos(
|
||||
&self,
|
||||
query: UnadoptedRepoQuery,
|
||||
) -> Result<Vec<String>, 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<Vec<User>, ForgejoError> {
|
||||
self.get(&query.path()).await
|
||||
}
|
||||
|
||||
pub async fn admin_create_user(&self, opt: CreateUserOption) -> Result<User, ForgejoError> {
|
||||
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<User, ForgejoError> {
|
||||
self.patch(&format!("admin/users/{user}"), &opt).await
|
||||
}
|
||||
|
||||
pub async fn admin_add_key(
|
||||
&self,
|
||||
user: &str,
|
||||
opt: CreateKeyOption,
|
||||
) -> Result<PublicKey, ForgejoError> {
|
||||
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<Organization, ForgejoError> {
|
||||
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<Repository, ForgejoError> {
|
||||
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<u32>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
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<u32>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
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<u32>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
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<String, String>,
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub created_at: time::OffsetDateTime,
|
||||
pub events: Vec<String>,
|
||||
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<u32>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
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<bool>,
|
||||
pub authorization_header: Option<String>,
|
||||
pub branch_filter: Option<String>,
|
||||
pub config: CreateHookOptionConfig,
|
||||
pub events: Vec<String>,
|
||||
#[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<String, String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Debug, PartialEq, Default)]
|
||||
pub struct EditHookOption {
|
||||
pub active: Option<bool>,
|
||||
pub authorization_header: Option<String>,
|
||||
pub branch_filter: Option<String>,
|
||||
pub config: Option<BTreeMap<String, String>>,
|
||||
pub events: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct AdminOrganizationQuery {
|
||||
pub page: Option<u32>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
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<u32>,
|
||||
pub limit: Option<u32>,
|
||||
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<u64>,
|
||||
pub login_name: String,
|
||||
pub page: Option<u32>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
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<time::OffsetDateTime>,
|
||||
pub email: String,
|
||||
pub full_name: Option<String>,
|
||||
pub login_name: Option<String>,
|
||||
pub must_change_password: bool,
|
||||
pub password: String,
|
||||
pub restricted: bool,
|
||||
pub send_notify: bool,
|
||||
pub source_id: Option<u64>,
|
||||
pub username: String,
|
||||
pub visibility: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Debug, PartialEq, Default)]
|
||||
pub struct EditUserOption {
|
||||
pub active: Option<bool>,
|
||||
pub admin: Option<bool>,
|
||||
pub allow_create_organization: Option<bool>,
|
||||
pub allow_git_hook: Option<bool>,
|
||||
pub allow_import_local: Option<bool>,
|
||||
pub description: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub full_name: Option<String>,
|
||||
pub location: Option<String>,
|
||||
pub login_name: Option<String>,
|
||||
pub max_repo_creation: Option<u64>,
|
||||
pub must_change_password: Option<bool>,
|
||||
pub password: Option<String>,
|
||||
pub prohibit_login: Option<bool>,
|
||||
pub restricted: Option<bool>,
|
||||
pub source_id: Option<u64>,
|
||||
pub visibility: Option<String>,
|
||||
pub website: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Debug, PartialEq)]
|
||||
pub struct CreateKeyOption {
|
||||
pub key: String,
|
||||
pub read_only: Option<bool>,
|
||||
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<bool>,
|
||||
pub title: String,
|
||||
pub url: Option<Url>,
|
||||
pub user: User,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Debug, PartialEq)]
|
||||
pub struct CreateOrgOption {
|
||||
pub description: Option<String>,
|
||||
pub full_name: Option<String>,
|
||||
pub location: Option<String>,
|
||||
pub repo_admin_change_team_access: Option<bool>,
|
||||
pub username: String,
|
||||
pub visibility: OrgVisibility,
|
||||
pub website: Option<Url>,
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
7226
src/generated/methods.rs
Normal file
7226
src/generated/methods.rs
Normal file
File diff suppressed because it is too large
Load diff
2
src/generated/mod.rs
Normal file
2
src/generated/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod methods;
|
||||
pub mod structs;
|
5959
src/generated/structs.rs
Normal file
5959
src/generated/structs.rs
Normal file
File diff suppressed because it is too large
Load diff
347
src/issue.rs
347
src/issue.rs
|
@ -1,347 +0,0 @@
|
|||
use super::*;
|
||||
|
||||
impl Forgejo {
|
||||
pub async fn get_repo_issues(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
query: IssueQuery,
|
||||
) -> Result<Vec<Issue>, ForgejoError> {
|
||||
self.get(&query.to_string(owner, repo)).await
|
||||
}
|
||||
|
||||
pub async fn create_issue(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
opts: CreateIssueOption,
|
||||
) -> Result<Issue, ForgejoError> {
|
||||
self.post(&format!("repos/{owner}/{repo}/issues"), &opts)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_issue(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
id: u64,
|
||||
) -> Result<Option<Issue>, ForgejoError> {
|
||||
self.get_opt(&format!("repos/{owner}/{repo}/issues/{id}"))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_issue(&self, owner: &str, repo: &str, id: u64) -> Result<(), ForgejoError> {
|
||||
self.delete(&format!("repos/{owner}/{repo}/issues/{id}"))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn edit_issue(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
id: u64,
|
||||
opts: EditIssueOption,
|
||||
) -> Result<Issue, ForgejoError> {
|
||||
self.patch(&format!("repos/{owner}/{repo}/issues/{id}"), &opts)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_repo_comments(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
query: RepoCommentQuery,
|
||||
) -> Result<Vec<Comment>, ForgejoError> {
|
||||
self.get(&query.to_string(owner, repo)).await
|
||||
}
|
||||
|
||||
pub async fn get_issue_comments(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
issue_id: u64,
|
||||
query: IssueCommentQuery,
|
||||
) -> Result<Vec<Comment>, ForgejoError> {
|
||||
self.get(&query.to_string(owner, repo, issue_id)).await
|
||||
}
|
||||
|
||||
pub async fn create_comment(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
issue_id: u64,
|
||||
opts: CreateIssueCommentOption,
|
||||
) -> Result<Comment, ForgejoError> {
|
||||
self.post(
|
||||
&format!("repos/{owner}/{repo}/issues/{issue_id}/comments"),
|
||||
&opts,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_comment(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
id: u64,
|
||||
) -> Result<Option<Comment>, ForgejoError> {
|
||||
self.get_opt(&format!("repos/{owner}/{repo}/issues/comments/{id}"))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_comment(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
id: u64,
|
||||
) -> Result<(), ForgejoError> {
|
||||
self.delete(&format!("repos/{owner}/{repo}/issues/comments/{id}"))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn edit_comment(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
id: u64,
|
||||
opts: EditIssueCommentOption,
|
||||
) -> Result<Comment, ForgejoError> {
|
||||
self.patch(&format!("repos/{owner}/{repo}/issues/comments/{id}"), &opts)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct Issue {
|
||||
pub assets: Vec<Attachment>,
|
||||
pub assignee: Option<User>,
|
||||
pub assignees: Option<Vec<User>>,
|
||||
pub body: String,
|
||||
#[serde(with = "time::serde::rfc3339::option")]
|
||||
pub closed_at: Option<time::OffsetDateTime>,
|
||||
pub comments: u64,
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub created_at: time::OffsetDateTime,
|
||||
#[serde(with = "time::serde::rfc3339::option")]
|
||||
pub due_date: Option<time::OffsetDateTime>,
|
||||
pub html_url: Url,
|
||||
pub id: u64,
|
||||
pub is_locked: bool,
|
||||
pub labels: Vec<Label>,
|
||||
pub milestone: Option<Milestone>,
|
||||
pub number: u64,
|
||||
pub original_author: String,
|
||||
pub original_author_id: u64,
|
||||
pub pin_order: u64,
|
||||
pub pull_request: Option<PullRequestMeta>,
|
||||
#[serde(rename = "ref")]
|
||||
pub _ref: String,
|
||||
pub repository: RepositoryMeta,
|
||||
pub state: State,
|
||||
pub title: String,
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub updated_at: time::OffsetDateTime,
|
||||
pub url: Url,
|
||||
pub user: User,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct Label {
|
||||
pub color: String,
|
||||
pub description: String,
|
||||
pub exclusive: bool,
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
pub url: Url,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct Attachment {
|
||||
pub browser_download_url: Url,
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub created_at: time::OffsetDateTime,
|
||||
pub download_count: u64,
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
pub size: u64,
|
||||
pub uuid: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Debug, PartialEq, Default)]
|
||||
pub struct EditAttachmentOption {
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone, Copy)]
|
||||
pub enum State {
|
||||
#[serde(rename = "open")]
|
||||
Open,
|
||||
#[serde(rename = "closed")]
|
||||
Closed,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub(crate) fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
State::Open => "open",
|
||||
State::Closed => "closed",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct Comment {
|
||||
pub assets: Vec<Attachment>,
|
||||
pub body: String,
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub created_at: time::OffsetDateTime,
|
||||
pub html_url: Url,
|
||||
pub id: u64,
|
||||
pub issue_url: Url,
|
||||
pub original_author: String,
|
||||
pub original_author_id: u64,
|
||||
#[serde(deserialize_with = "crate::none_if_blank_url")]
|
||||
pub pull_request_url: Option<Url>,
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub updated_at: time::OffsetDateTime,
|
||||
pub user: User,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct IssueQuery {
|
||||
pub state: Option<State>,
|
||||
pub labels: Vec<String>,
|
||||
pub query: Option<String>,
|
||||
pub _type: Option<IssueQueryType>,
|
||||
pub milestones: Vec<String>,
|
||||
pub since: Option<time::OffsetDateTime>,
|
||||
pub before: Option<time::OffsetDateTime>,
|
||||
pub created_by: Option<String>,
|
||||
pub assigned_by: Option<String>,
|
||||
pub mentioned_by: Option<String>,
|
||||
pub page: Option<u32>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
impl IssueQuery {
|
||||
fn to_string(&self, owner: &str, repo: &str) -> String {
|
||||
format!("repos/{owner}/{repo}/issues?state={}&labels={}&q={}&type={}&milestones={}&since={}&before={}&created_by={}&assigned_by={}&mentioned_by={}&page={}&limit={}",
|
||||
self.state.map(|s| s.as_str()).unwrap_or_default(),
|
||||
self.labels.join(","),
|
||||
self.query.as_deref().unwrap_or_default(),
|
||||
self._type.map(|t| t.as_str()).unwrap_or_default(),
|
||||
self.milestones.join(","),
|
||||
self.since.map(|t| t.format(&time::format_description::well_known::Rfc3339).unwrap()).unwrap_or_default(),
|
||||
self.before.map(|t| t.format(&time::format_description::well_known::Rfc3339).unwrap()).unwrap_or_default(),
|
||||
self.created_by.as_deref().unwrap_or_default(),
|
||||
self.assigned_by.as_deref().unwrap_or_default(),
|
||||
self.mentioned_by.as_deref().unwrap_or_default(),
|
||||
self.page.map(|page| page.to_string()).unwrap_or_default(),
|
||||
self.limit.map(|page| page.to_string()).unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
pub enum IssueQueryType {
|
||||
Issues,
|
||||
Pulls,
|
||||
}
|
||||
|
||||
impl IssueQueryType {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
IssueQueryType::Issues => "issues",
|
||||
IssueQueryType::Pulls => "pulls",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct IssueCommentQuery {
|
||||
pub since: Option<time::OffsetDateTime>,
|
||||
pub before: Option<time::OffsetDateTime>,
|
||||
}
|
||||
|
||||
impl IssueCommentQuery {
|
||||
fn to_string(&self, owner: &str, repo: &str, issue_id: u64) -> String {
|
||||
format!(
|
||||
"repos/{owner}/{repo}/issues/{issue_id}/comments?since={}&before={}",
|
||||
self.since
|
||||
.map(|t| t
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.unwrap())
|
||||
.unwrap_or_default(),
|
||||
self.before
|
||||
.map(|t| t
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.unwrap())
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct RepoCommentQuery {
|
||||
pub since: Option<time::OffsetDateTime>,
|
||||
pub before: Option<time::OffsetDateTime>,
|
||||
pub page: Option<u32>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
impl RepoCommentQuery {
|
||||
fn to_string(&self, owner: &str, repo: &str) -> String {
|
||||
format!(
|
||||
"repos/{owner}/{repo}/issues/comments?since={}&before={}&page={}&limit={}",
|
||||
self.since
|
||||
.map(|t| t
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.unwrap())
|
||||
.unwrap_or_default(),
|
||||
self.before
|
||||
.map(|t| t
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.unwrap())
|
||||
.unwrap_or_default(),
|
||||
self.page.map(|page| page.to_string()).unwrap_or_default(),
|
||||
self.limit.map(|page| page.to_string()).unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Debug, PartialEq, Default)]
|
||||
pub struct CreateIssueOption {
|
||||
pub assignees: Vec<String>,
|
||||
pub body: Option<String>,
|
||||
pub closed: Option<bool>,
|
||||
#[serde(with = "time::serde::rfc3339::option")]
|
||||
pub due_date: Option<time::OffsetDateTime>,
|
||||
pub labels: Vec<u64>,
|
||||
pub milestone: Option<u64>,
|
||||
pub _ref: Option<String>,
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Debug, PartialEq, Default)]
|
||||
pub struct EditIssueOption {
|
||||
pub assignees: Vec<String>,
|
||||
pub body: Option<String>,
|
||||
#[serde(with = "time::serde::rfc3339::option")]
|
||||
pub due_date: Option<time::OffsetDateTime>,
|
||||
pub labels: Vec<u64>,
|
||||
pub milestone: Option<u64>,
|
||||
pub _ref: Option<String>,
|
||||
pub state: Option<State>,
|
||||
pub title: Option<String>,
|
||||
pub unset_due_date: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Debug, PartialEq, Default)]
|
||||
pub struct CreateIssueCommentOption {
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Debug, PartialEq, Default)]
|
||||
pub struct EditIssueCommentOption {
|
||||
pub body: String,
|
||||
}
|
257
src/lib.rs
257
src/lib.rs
|
@ -1,5 +1,4 @@
|
|||
use reqwest::{Client, Request, StatusCode};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use soft_assert::*;
|
||||
use url::Url;
|
||||
use zeroize::Zeroize;
|
||||
|
@ -9,23 +8,9 @@ pub struct Forgejo {
|
|||
client: Client,
|
||||
}
|
||||
|
||||
mod admin;
|
||||
mod issue;
|
||||
mod misc;
|
||||
mod notification;
|
||||
mod organization;
|
||||
mod package;
|
||||
mod repository;
|
||||
mod user;
|
||||
mod generated;
|
||||
|
||||
pub use admin::*;
|
||||
pub use issue::*;
|
||||
pub use misc::*;
|
||||
pub use notification::*;
|
||||
pub use organization::*;
|
||||
pub use package::*;
|
||||
pub use repository::*;
|
||||
pub use user::*;
|
||||
pub use generated::structs;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ForgejoError {
|
||||
|
@ -38,15 +23,30 @@ pub enum ForgejoError {
|
|||
#[error("API key should be ascii")]
|
||||
KeyNotAscii,
|
||||
#[error("the response from forgejo was not properly structured")]
|
||||
BadStructure(#[source] serde_json::Error, String),
|
||||
BadStructure(#[from] StructureError),
|
||||
#[error("unexpected status code {} {}", .0.as_u16(), .0.canonical_reason().unwrap_or(""))]
|
||||
UnexpectedStatusCode(StatusCode),
|
||||
#[error("{} {}: {}", .0.as_u16(), .0.canonical_reason().unwrap_or(""), .1)]
|
||||
ApiError(StatusCode, String),
|
||||
#[error("{} {}{}", .0.as_u16(), .0.canonical_reason().unwrap_or(""), .1.as_ref().map(|s| format!(": {s}")).unwrap_or_default())]
|
||||
ApiError(StatusCode, Option<String>),
|
||||
#[error("the provided authorization was too long to accept")]
|
||||
AuthTooLong,
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum StructureError {
|
||||
#[error("{contents}")]
|
||||
Serde {
|
||||
e: serde_json::Error,
|
||||
contents: String,
|
||||
},
|
||||
#[error("failed to find header `{0}`")]
|
||||
HeaderMissing(&'static str),
|
||||
#[error("header was not ascii")]
|
||||
HeaderNotAscii,
|
||||
#[error("failed to parse header")]
|
||||
HeaderParseFailed,
|
||||
}
|
||||
|
||||
/// Method of authentication to connect to the Forgejo host with.
|
||||
pub enum Auth<'a> {
|
||||
/// Application Access Token. Grants access to scope enabled for the
|
||||
|
@ -134,218 +134,67 @@ impl Forgejo {
|
|||
Ok(Self { url, client })
|
||||
}
|
||||
|
||||
async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T, ForgejoError> {
|
||||
let url = self.url.join("api/v1/").unwrap().join(path).unwrap();
|
||||
let request = self.client.get(url).build()?;
|
||||
self.execute(request).await
|
||||
}
|
||||
|
||||
async fn get_opt<T: DeserializeOwned>(&self, path: &str) -> Result<Option<T>, ForgejoError> {
|
||||
let url = self.url.join("api/v1/").unwrap().join(path).unwrap();
|
||||
let request = self.client.get(url).build()?;
|
||||
self.execute_opt(request).await
|
||||
}
|
||||
|
||||
async fn get_str(&self, path: &str) -> Result<String, ForgejoError> {
|
||||
let url = self.url.join("api/v1/").unwrap().join(path).unwrap();
|
||||
let request = self.client.get(url).build()?;
|
||||
self.execute_str(request).await
|
||||
}
|
||||
|
||||
async fn get_exists(&self, path: &str) -> Result<bool, ForgejoError> {
|
||||
let url = self.url.join("api/v1/").unwrap().join(path).unwrap();
|
||||
let request = self.client.get(url).build()?;
|
||||
self.execute_exists(request).await
|
||||
}
|
||||
|
||||
async fn post<T: Serialize, U: DeserializeOwned>(
|
||||
pub async fn download_release_attachment(
|
||||
&self,
|
||||
path: &str,
|
||||
body: &T,
|
||||
) -> Result<U, ForgejoError> {
|
||||
let url = self.url.join("api/v1/").unwrap().join(path).unwrap();
|
||||
let request = self.client.post(url).json(body).build()?;
|
||||
self.execute(request).await
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
release: u64,
|
||||
attach: u64,
|
||||
) -> Result<bytes::Bytes, ForgejoError> {
|
||||
let release = self
|
||||
.repo_get_release_attachment(owner, repo, release, attach)
|
||||
.await?;
|
||||
let request = self
|
||||
.client
|
||||
.get(format!("/attachments/{}", release.uuid.unwrap()))
|
||||
.build()?;
|
||||
Ok(self.execute(request).await?.bytes().await?)
|
||||
}
|
||||
|
||||
async fn post_multipart<T: DeserializeOwned>(
|
||||
&self,
|
||||
path: &str,
|
||||
body: reqwest::multipart::Form,
|
||||
) -> Result<T, ForgejoError> {
|
||||
fn get(&self, path: &str) -> reqwest::RequestBuilder {
|
||||
let url = self.url.join("api/v1/").unwrap().join(path).unwrap();
|
||||
let request = self.client.post(url).multipart(body).build()?;
|
||||
self.execute(request).await
|
||||
self.client.get(url)
|
||||
}
|
||||
|
||||
async fn post_str_out<T: Serialize>(
|
||||
&self,
|
||||
path: &str,
|
||||
body: &T,
|
||||
) -> Result<String, ForgejoError> {
|
||||
fn put(&self, path: &str) -> reqwest::RequestBuilder {
|
||||
let url = self.url.join("api/v1/").unwrap().join(path).unwrap();
|
||||
let request = self.client.post(url).json(body).build()?;
|
||||
self.execute_str(request).await
|
||||
self.client.put(url)
|
||||
}
|
||||
|
||||
async fn post_unit<T: Serialize>(&self, path: &str, body: &T) -> Result<(), ForgejoError> {
|
||||
fn post(&self, path: &str) -> reqwest::RequestBuilder {
|
||||
let url = self.url.join("api/v1/").unwrap().join(path).unwrap();
|
||||
let request = self.client.post(url).json(body).build()?;
|
||||
self.execute_unit(request).await
|
||||
self.client.post(url)
|
||||
}
|
||||
|
||||
async fn post_raw(&self, path: &str, body: String) -> Result<String, ForgejoError> {
|
||||
fn delete(&self, path: &str) -> reqwest::RequestBuilder {
|
||||
let url = self.url.join("api/v1/").unwrap().join(path).unwrap();
|
||||
let request = self.client.post(url).body(body).build()?;
|
||||
self.execute_str(request).await
|
||||
self.client.delete(url)
|
||||
}
|
||||
|
||||
async fn delete(&self, path: &str) -> Result<(), ForgejoError> {
|
||||
fn patch(&self, path: &str) -> reqwest::RequestBuilder {
|
||||
let url = self.url.join("api/v1/").unwrap().join(path).unwrap();
|
||||
let request = self.client.delete(url).build()?;
|
||||
self.execute_unit(request).await
|
||||
self.client.patch(url)
|
||||
}
|
||||
|
||||
async fn patch<T: Serialize, U: DeserializeOwned>(
|
||||
&self,
|
||||
path: &str,
|
||||
body: &T,
|
||||
) -> Result<U, ForgejoError> {
|
||||
let url = self.url.join("api/v1/").unwrap().join(path).unwrap();
|
||||
let request = self.client.patch(url).json(body).build()?;
|
||||
self.execute(request).await
|
||||
}
|
||||
|
||||
async fn put<T: DeserializeOwned>(&self, path: &str) -> Result<T, ForgejoError> {
|
||||
let url = self.url.join("api/v1/").unwrap().join(path).unwrap();
|
||||
let request = self.client.put(url).build()?;
|
||||
self.execute(request).await
|
||||
}
|
||||
|
||||
async fn execute<T: DeserializeOwned>(&self, request: Request) -> Result<T, ForgejoError> {
|
||||
async fn execute(&self, request: Request) -> Result<reqwest::Response, ForgejoError> {
|
||||
let response = self.client.execute(request).await?;
|
||||
match response.status() {
|
||||
status if status.is_success() => {
|
||||
let body = response.text().await?;
|
||||
let out =
|
||||
serde_json::from_str(&body).map_err(|e| ForgejoError::BadStructure(e, body))?;
|
||||
Ok(out)
|
||||
status if status.is_success() => Ok(response),
|
||||
status if status.is_client_error() => {
|
||||
Err(ForgejoError::ApiError(status, maybe_err(response).await))
|
||||
}
|
||||
status if status.is_client_error() => Err(ForgejoError::ApiError(
|
||||
status,
|
||||
response
|
||||
.json::<ErrorMessage>()
|
||||
.await?
|
||||
.message
|
||||
.unwrap_or_else(|| String::from("[no message]")),
|
||||
)),
|
||||
status => Err(ForgejoError::UnexpectedStatusCode(status)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Like `execute`, but returns a `String`.
|
||||
async fn execute_opt_raw(
|
||||
&self,
|
||||
request: Request,
|
||||
) -> Result<Option<bytes::Bytes>, ForgejoError> {
|
||||
let response = self.client.execute(request).await?;
|
||||
match response.status() {
|
||||
status if status.is_success() => Ok(Some(response.bytes().await?)),
|
||||
StatusCode::NOT_FOUND => Ok(None),
|
||||
status if status.is_client_error() => Err(ForgejoError::ApiError(
|
||||
status,
|
||||
response
|
||||
.json::<ErrorMessage>()
|
||||
.await?
|
||||
.message
|
||||
.unwrap_or_else(|| String::from("[no message]")),
|
||||
)),
|
||||
status => Err(ForgejoError::UnexpectedStatusCode(status)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Like `execute`, but returns a `String`.
|
||||
async fn execute_str(&self, request: Request) -> Result<String, ForgejoError> {
|
||||
let response = self.client.execute(request).await?;
|
||||
match response.status() {
|
||||
status if status.is_success() => Ok(response.text().await?),
|
||||
status if status.is_client_error() => Err(ForgejoError::ApiError(
|
||||
status,
|
||||
response
|
||||
.json::<ErrorMessage>()
|
||||
.await?
|
||||
.message
|
||||
.unwrap_or_else(|| String::from("[no message]")),
|
||||
)),
|
||||
status => Err(ForgejoError::UnexpectedStatusCode(status)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Like `execute`, but returns unit.
|
||||
async fn execute_unit(&self, request: Request) -> Result<(), ForgejoError> {
|
||||
let response = self.client.execute(request).await?;
|
||||
match response.status() {
|
||||
status if status.is_success() => Ok(()),
|
||||
status if status.is_client_error() => Err(ForgejoError::ApiError(
|
||||
status,
|
||||
response
|
||||
.json::<ErrorMessage>()
|
||||
.await?
|
||||
.message
|
||||
.unwrap_or_else(|| String::from("[no message]")),
|
||||
)),
|
||||
status => Err(ForgejoError::UnexpectedStatusCode(status)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Like `execute`, but returns `Ok(None)` on 404.
|
||||
async fn execute_opt<T: DeserializeOwned>(
|
||||
&self,
|
||||
request: Request,
|
||||
) -> Result<Option<T>, ForgejoError> {
|
||||
let response = self.client.execute(request).await?;
|
||||
match response.status() {
|
||||
status if status.is_success() => {
|
||||
let body = response.text().await?;
|
||||
let out =
|
||||
serde_json::from_str(&body).map_err(|e| ForgejoError::BadStructure(e, body))?;
|
||||
Ok(out)
|
||||
}
|
||||
StatusCode::NOT_FOUND => Ok(None),
|
||||
status if status.is_client_error() => Err(ForgejoError::ApiError(
|
||||
status,
|
||||
response
|
||||
.json::<ErrorMessage>()
|
||||
.await?
|
||||
.message
|
||||
.unwrap_or_else(|| String::from("[no message]")),
|
||||
)),
|
||||
status => Err(ForgejoError::UnexpectedStatusCode(status)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Like `execute`, but returns `false` on 404.
|
||||
async fn execute_exists(&self, request: Request) -> Result<bool, ForgejoError> {
|
||||
let response = self.client.execute(request).await?;
|
||||
match response.status() {
|
||||
status if status.is_success() => Ok(true),
|
||||
StatusCode::NOT_FOUND => Ok(false),
|
||||
status if status.is_client_error() => Err(ForgejoError::ApiError(
|
||||
status,
|
||||
response
|
||||
.json::<ErrorMessage>()
|
||||
.await?
|
||||
.message
|
||||
.unwrap_or_else(|| String::from("[no message]")),
|
||||
)),
|
||||
status => Err(ForgejoError::UnexpectedStatusCode(status)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn maybe_err(res: reqwest::Response) -> Option<String> {
|
||||
res.json::<ErrorMessage>().await.ok().map(|e| e.message)
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct ErrorMessage {
|
||||
message: Option<String>,
|
||||
message: String,
|
||||
// intentionally ignored, no need for now
|
||||
// url: Url
|
||||
}
|
||||
|
|
162
src/misc.rs
162
src/misc.rs
|
@ -1,162 +0,0 @@
|
|||
use super::*;
|
||||
|
||||
impl Forgejo {
|
||||
pub async fn get_gitignore_templates(&self) -> Result<Vec<String>, ForgejoError> {
|
||||
self.get("gitignore/templates").await
|
||||
}
|
||||
|
||||
pub async fn get_gitignore_template(
|
||||
&self,
|
||||
name: &str,
|
||||
) -> Result<Option<GitignoreTemplateInfo>, ForgejoError> {
|
||||
self.get_opt(&format!("gitignore/templates/{name}")).await
|
||||
}
|
||||
|
||||
pub async fn get_label_templates(&self) -> Result<Vec<String>, ForgejoError> {
|
||||
self.get("label/templates").await
|
||||
}
|
||||
|
||||
pub async fn get_label_template(&self, name: &str) -> Result<Vec<LabelTemplate>, ForgejoError> {
|
||||
self.get(&format!("label/templates/{name}")).await
|
||||
}
|
||||
|
||||
pub async fn get_licenses(&self) -> Result<Vec<LicenseTemplateListEntry>, ForgejoError> {
|
||||
self.get("licenses").await
|
||||
}
|
||||
|
||||
pub async fn get_license(
|
||||
&self,
|
||||
name: &str,
|
||||
) -> Result<Option<GitignoreTemplateInfo>, ForgejoError> {
|
||||
self.get_opt(&format!("license/{name}")).await
|
||||
}
|
||||
|
||||
pub async fn render_markdown(&self, opt: MarkdownOption) -> Result<String, ForgejoError> {
|
||||
self.post_str_out("markdown", &opt).await
|
||||
}
|
||||
|
||||
pub async fn render_markdown_raw(&self, body: String) -> Result<String, ForgejoError> {
|
||||
self.post_raw("markdown/raw", body).await
|
||||
}
|
||||
|
||||
pub async fn render_markup(&self, opt: MarkupOption) -> Result<String, ForgejoError> {
|
||||
self.post_str_out("markup", &opt).await
|
||||
}
|
||||
|
||||
pub async fn nodeinfo(&self) -> Result<NodeInfo, ForgejoError> {
|
||||
self.get("nodeinfo").await
|
||||
}
|
||||
|
||||
pub async fn signing_key(&self) -> Result<String, ForgejoError> {
|
||||
self.get_str("signing-key.gpg").await
|
||||
}
|
||||
|
||||
pub async fn version(&self) -> Result<ServerVersion, ForgejoError> {
|
||||
self.get("version").await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct GitignoreTemplateInfo {
|
||||
pub name: String,
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct LabelTemplate {
|
||||
pub color: String,
|
||||
pub description: String,
|
||||
pub exclusive: bool,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct LicenseTemplateListEntry {
|
||||
pub key: String,
|
||||
pub name: String,
|
||||
pub url: Url,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct LicenseTemplateInfo {
|
||||
pub body: String,
|
||||
pub implementation: String,
|
||||
pub key: String,
|
||||
pub name: String,
|
||||
pub url: Url,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Debug, PartialEq, Default)]
|
||||
pub struct MarkdownOption {
|
||||
#[serde(rename = "Context")]
|
||||
pub context: String,
|
||||
#[serde(rename = "Mode")]
|
||||
pub mode: String,
|
||||
#[serde(rename = "Text")]
|
||||
pub text: String,
|
||||
#[serde(rename = "Wiki")]
|
||||
pub wiki: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Debug, PartialEq, Default)]
|
||||
pub struct MarkupOption {
|
||||
#[serde(rename = "Context")]
|
||||
pub context: String,
|
||||
#[serde(rename = "FilePath")]
|
||||
pub file_path: String,
|
||||
#[serde(rename = "Mode")]
|
||||
pub mode: String,
|
||||
#[serde(rename = "Text")]
|
||||
pub text: String,
|
||||
#[serde(rename = "Wiki")]
|
||||
pub wiki: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct NodeInfo {
|
||||
pub metadata: std::collections::BTreeMap<String, String>,
|
||||
#[serde(rename = "openRegistrations")]
|
||||
pub open_registrations: bool,
|
||||
pub protocols: Vec<String>,
|
||||
pub services: NodeInfoServices,
|
||||
pub software: NodeInfoSoftware,
|
||||
pub usage: NodeInfoUsage,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct NodeInfoServices {
|
||||
pub inbound: Vec<String>,
|
||||
pub outbound: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct NodeInfoSoftware {
|
||||
pub homepage: Url,
|
||||
pub name: String,
|
||||
pub repository: Url,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct NodeInfoUsage {
|
||||
#[serde(rename = "localComments")]
|
||||
pub local_comments: u64,
|
||||
#[serde(rename = "localPosts")]
|
||||
pub local_posts: u64,
|
||||
pub users: NodeInfoUsageUsers,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct NodeInfoUsageUsers {
|
||||
#[serde(rename = "activeHalfYear")]
|
||||
pub active_half_year: u64,
|
||||
#[serde(rename = "activeMonth")]
|
||||
pub active_month: u64,
|
||||
pub total: u64,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct ServerVersion {
|
||||
pub version: String,
|
||||
}
|
|
@ -1,273 +0,0 @@
|
|||
use super::*;
|
||||
|
||||
impl Forgejo {
|
||||
pub async fn notifications(
|
||||
&self,
|
||||
query: NotificationQuery,
|
||||
) -> Result<Vec<NotificationThread>, ForgejoError> {
|
||||
self.get(&format!("notifications?{}", query.query_string()))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn set_notifications_state(
|
||||
&self,
|
||||
query: NotificationPutQuery,
|
||||
) -> Result<Vec<NotificationThread>, ForgejoError> {
|
||||
self.put(&format!("notifications?{}", query.query_string()))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn notification_count(&self) -> Result<Vec<NotificationCount>, ForgejoError> {
|
||||
self.get("notifications/new").await
|
||||
}
|
||||
|
||||
pub async fn get_notification(
|
||||
&self,
|
||||
id: u64,
|
||||
) -> Result<Option<NotificationThread>, ForgejoError> {
|
||||
self.get_opt(&format!("notifications/threads/{id}")).await
|
||||
}
|
||||
|
||||
pub async fn set_notification_state(
|
||||
&self,
|
||||
id: u64,
|
||||
to_status: ToStatus,
|
||||
) -> Result<Option<NotificationThread>, ForgejoError> {
|
||||
self.patch(
|
||||
&format!(
|
||||
"notifications/threads/{id}?to-status={}",
|
||||
to_status.as_str()
|
||||
),
|
||||
&(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_repo_notifications(
|
||||
&self,
|
||||
owner: &str,
|
||||
name: &str,
|
||||
query: NotificationQuery,
|
||||
) -> Result<Vec<NotificationThread>, ForgejoError> {
|
||||
self.get(&format!(
|
||||
"repos/{owner}/{name}/notifications?{}",
|
||||
query.query_string()
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn set_repo_notifications_state(
|
||||
&self,
|
||||
owner: &str,
|
||||
name: &str,
|
||||
query: NotificationPutQuery,
|
||||
) -> Result<Vec<NotificationThread>, ForgejoError> {
|
||||
self.put(&format!(
|
||||
"repos/{owner}/{name}/notifications?{}",
|
||||
query.query_string()
|
||||
))
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NotificationQuery {
|
||||
pub all: bool,
|
||||
pub include_unread: bool,
|
||||
pub include_read: bool,
|
||||
pub include_pinned: bool,
|
||||
pub subject_type: Option<NotificationSubjectType>,
|
||||
pub since: Option<time::OffsetDateTime>,
|
||||
pub before: Option<time::OffsetDateTime>,
|
||||
pub page: Option<u32>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
impl Default for NotificationQuery {
|
||||
fn default() -> Self {
|
||||
NotificationQuery {
|
||||
all: false,
|
||||
include_unread: true,
|
||||
include_read: false,
|
||||
include_pinned: true,
|
||||
subject_type: None,
|
||||
since: None,
|
||||
before: None,
|
||||
page: None,
|
||||
limit: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NotificationQuery {
|
||||
fn query_string(&self) -> String {
|
||||
use std::fmt::Write;
|
||||
let mut s = String::new();
|
||||
if self.all {
|
||||
s.push_str("all=true&");
|
||||
}
|
||||
if self.include_unread {
|
||||
s.push_str("status-types=unread&");
|
||||
}
|
||||
if self.include_read {
|
||||
s.push_str("status-types=read&");
|
||||
}
|
||||
if self.include_pinned {
|
||||
s.push_str("status-types=pinned&");
|
||||
}
|
||||
if let Some(subject_type) = self.subject_type {
|
||||
s.push_str("subject-type=");
|
||||
s.push_str(subject_type.as_str());
|
||||
s.push('&');
|
||||
}
|
||||
if let Some(since) = &self.since {
|
||||
s.push_str("since=");
|
||||
s.push_str(
|
||||
&since
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.unwrap(),
|
||||
);
|
||||
s.push('&');
|
||||
}
|
||||
if let Some(before) = &self.before {
|
||||
s.push_str("before=");
|
||||
s.push_str(
|
||||
&before
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.unwrap(),
|
||||
);
|
||||
s.push('&');
|
||||
}
|
||||
if let Some(page) = self.page {
|
||||
s.push_str("page=");
|
||||
s.write_fmt(format_args!("{page}"))
|
||||
.expect("writing to a string never fails");
|
||||
s.push('&');
|
||||
}
|
||||
if let Some(limit) = self.limit {
|
||||
s.push_str("limit=");
|
||||
s.write_fmt(format_args!("{limit}"))
|
||||
.expect("writing to a string never fails");
|
||||
}
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum NotificationSubjectType {
|
||||
Issue,
|
||||
Pull,
|
||||
Commit,
|
||||
Repository,
|
||||
}
|
||||
|
||||
impl NotificationSubjectType {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
NotificationSubjectType::Issue => "issue",
|
||||
NotificationSubjectType::Pull => "pull",
|
||||
NotificationSubjectType::Commit => "commit",
|
||||
NotificationSubjectType::Repository => "repository",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct NotificationThread {
|
||||
pub id: u64,
|
||||
pub pinned: bool,
|
||||
pub repository: Repository,
|
||||
pub subject: NotificationSubject,
|
||||
pub unread: bool,
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub updated_at: time::OffsetDateTime,
|
||||
pub url: Url,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct NotificationSubject {
|
||||
pub html_url: Url,
|
||||
pub latest_comment_html_url: Url,
|
||||
pub latest_comment_url: Url,
|
||||
pub state: String,
|
||||
pub title: String,
|
||||
#[serde(rename = "type")]
|
||||
pub _type: String,
|
||||
pub url: Url,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NotificationPutQuery {
|
||||
pub last_read_at: Option<time::OffsetDateTime>,
|
||||
pub all: bool,
|
||||
pub include_unread: bool,
|
||||
pub include_read: bool,
|
||||
pub include_pinned: bool,
|
||||
pub to_status: ToStatus,
|
||||
}
|
||||
|
||||
impl Default for NotificationPutQuery {
|
||||
fn default() -> Self {
|
||||
NotificationPutQuery {
|
||||
last_read_at: None,
|
||||
all: false,
|
||||
include_unread: true,
|
||||
include_read: false,
|
||||
include_pinned: false,
|
||||
to_status: ToStatus::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NotificationPutQuery {
|
||||
fn query_string(&self) -> String {
|
||||
let mut s = String::new();
|
||||
if let Some(last_read_at) = &self.last_read_at {
|
||||
s.push_str("since=");
|
||||
s.push_str(
|
||||
&last_read_at
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.unwrap(),
|
||||
);
|
||||
s.push('&');
|
||||
}
|
||||
if self.all {
|
||||
s.push_str("all=true&");
|
||||
}
|
||||
if self.include_unread {
|
||||
s.push_str("status-types=unread&");
|
||||
}
|
||||
if self.include_read {
|
||||
s.push_str("status-types=read&");
|
||||
}
|
||||
if self.include_pinned {
|
||||
s.push_str("status-types=pinned&");
|
||||
}
|
||||
s.push_str("subject-type=");
|
||||
s.push_str(self.to_status.as_str());
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub enum ToStatus {
|
||||
#[default]
|
||||
Read,
|
||||
Unread,
|
||||
Pinned,
|
||||
}
|
||||
|
||||
impl ToStatus {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
ToStatus::Read => "read",
|
||||
ToStatus::Unread => "unread",
|
||||
ToStatus::Pinned => "pinned",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct NotificationCount {
|
||||
pub new: u64,
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
use crate::*;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct Organization {
|
||||
#[serde(deserialize_with = "crate::none_if_blank_url")]
|
||||
pub avatar_url: Option<Url>,
|
||||
pub description: String,
|
||||
pub full_name: String,
|
||||
pub id: u64,
|
||||
pub location: Option<String>,
|
||||
pub name: String,
|
||||
pub repo_admin_change_team_access: bool,
|
||||
pub visibility: String,
|
||||
#[serde(deserialize_with = "crate::none_if_blank_url")]
|
||||
pub website: Option<Url>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct Team {
|
||||
pub can_create_org_repo: bool,
|
||||
pub description: String,
|
||||
pub id: u64,
|
||||
pub includes_all_repositories: bool,
|
||||
pub name: String,
|
||||
pub organization: Organization,
|
||||
pub permission: String,
|
||||
pub units: Vec<String>,
|
||||
pub units_map: BTreeMap<String, String>,
|
||||
}
|
174
src/package.rs
174
src/package.rs
|
@ -1,174 +0,0 @@
|
|||
use std::fmt::Write;
|
||||
|
||||
use super::*;
|
||||
|
||||
impl Forgejo {
|
||||
pub async fn get_user_packages(
|
||||
&self,
|
||||
owner: &str,
|
||||
query: PackagesQuery,
|
||||
) -> Result<Vec<Package>, ForgejoError> {
|
||||
self.get(&query.path(owner)).await
|
||||
}
|
||||
|
||||
pub async fn get_package(
|
||||
&self,
|
||||
owner: &str,
|
||||
_type: PackageType,
|
||||
name: &str,
|
||||
version: &str,
|
||||
) -> Result<Option<Package>, ForgejoError> {
|
||||
self.get_opt(&format!(
|
||||
"packages/{owner}/{}/{name}/{version}",
|
||||
_type.as_str()
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_package(
|
||||
&self,
|
||||
owner: &str,
|
||||
_type: PackageType,
|
||||
name: &str,
|
||||
version: &str,
|
||||
) -> Result<(), ForgejoError> {
|
||||
self.delete(&format!(
|
||||
"packages/{owner}/{}/{name}/{version}",
|
||||
_type.as_str()
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_package_files(
|
||||
&self,
|
||||
owner: &str,
|
||||
_type: PackageType,
|
||||
name: &str,
|
||||
version: &str,
|
||||
) -> Result<Vec<PackageFile>, ForgejoError> {
|
||||
self.get(&format!(
|
||||
"packages/{owner}/{}/{name}/{version}",
|
||||
_type.as_str()
|
||||
))
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct PackagesQuery {
|
||||
pub page: Option<u32>,
|
||||
pub limit: Option<u32>,
|
||||
pub kind: Option<PackageType>,
|
||||
pub query: String,
|
||||
}
|
||||
|
||||
impl PackagesQuery {
|
||||
fn path(&self, owner: &str) -> String {
|
||||
let mut s = String::from("packages/");
|
||||
s.push_str(owner);
|
||||
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('&');
|
||||
}
|
||||
if let Some(kind) = self.kind {
|
||||
s.push_str("type=");
|
||||
s.push_str(kind.as_str());
|
||||
s.push('&');
|
||||
}
|
||||
if !self.query.is_empty() {
|
||||
s.push_str("q=");
|
||||
s.push_str(&self.query);
|
||||
s.push('&');
|
||||
}
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq, Eq, Clone, Copy)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[non_exhaustive]
|
||||
pub enum PackageType {
|
||||
Alpine,
|
||||
Cargo,
|
||||
Chef,
|
||||
Composer,
|
||||
Conan,
|
||||
Conda,
|
||||
Container,
|
||||
Cran,
|
||||
Debian,
|
||||
Generic,
|
||||
Go,
|
||||
Helm,
|
||||
Maven,
|
||||
Npm,
|
||||
Nuget,
|
||||
Pub,
|
||||
Pypi,
|
||||
Rpm,
|
||||
RubyGems,
|
||||
Swift,
|
||||
Vagrant,
|
||||
}
|
||||
|
||||
impl PackageType {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
PackageType::Alpine => "alpine",
|
||||
PackageType::Cargo => "cargo",
|
||||
PackageType::Chef => "chef",
|
||||
PackageType::Composer => "composer",
|
||||
PackageType::Conan => "conan",
|
||||
PackageType::Conda => "conda",
|
||||
PackageType::Container => "container",
|
||||
PackageType::Cran => "cran",
|
||||
PackageType::Debian => "debian",
|
||||
PackageType::Generic => "generic",
|
||||
PackageType::Go => "go",
|
||||
PackageType::Helm => "helm",
|
||||
PackageType::Maven => "maven",
|
||||
PackageType::Npm => "npm",
|
||||
PackageType::Nuget => "nuget",
|
||||
PackageType::Pub => "pub",
|
||||
PackageType::Pypi => "pypi",
|
||||
PackageType::Rpm => "rpm",
|
||||
PackageType::RubyGems => "rubygems",
|
||||
PackageType::Swift => "swift",
|
||||
PackageType::Vagrant => "vagrant",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct Package {
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub created_at: time::OffsetDateTime,
|
||||
pub creator: User,
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
pub owner: User,
|
||||
pub repository: Option<Repository>,
|
||||
pub _type: PackageType,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct PackageFile {
|
||||
#[serde(rename = "Size")]
|
||||
pub size: u64,
|
||||
pub id: u64,
|
||||
pub md5: String,
|
||||
pub name: String,
|
||||
pub sha1: String,
|
||||
pub sha256: String,
|
||||
pub sha512: String,
|
||||
}
|
|
@ -1,743 +0,0 @@
|
|||
use super::*;
|
||||
|
||||
/// Repository operations.
|
||||
impl Forgejo {
|
||||
/// Gets info about the specified repository.
|
||||
pub async fn get_repo(
|
||||
&self,
|
||||
user: &str,
|
||||
repo: &str,
|
||||
) -> Result<Option<Repository>, ForgejoError> {
|
||||
self.get_opt(&format!("repos/{user}/{repo}/")).await
|
||||
}
|
||||
|
||||
/// Creates a repository.
|
||||
pub async fn create_repo(&self, repo: CreateRepoOption) -> Result<Repository, ForgejoError> {
|
||||
self.post("user/repos", &repo).await
|
||||
}
|
||||
|
||||
pub async fn get_pulls(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
query: PullQuery,
|
||||
) -> Result<Vec<PullRequest>, ForgejoError> {
|
||||
self.get(&query.to_string(owner, repo)).await
|
||||
}
|
||||
|
||||
pub async fn create_pr(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
opts: CreatePullRequestOption,
|
||||
) -> Result<PullRequest, ForgejoError> {
|
||||
self.post(&format!("repos/{owner}/{repo}/pulls"), &opts)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn is_merged(&self, owner: &str, repo: &str, pr: u64) -> Result<bool, ForgejoError> {
|
||||
self.get_exists(&format!("repos/{owner}/{repo}/pulls/{pr}/merge"))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn merge_pr(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
pr: u64,
|
||||
opts: MergePullRequestOption,
|
||||
) -> Result<(), ForgejoError> {
|
||||
self.post_unit(&format!("repos/{owner}/{repo}/pulls/{pr}/merge"), &opts)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn cancel_merge(&self, owner: &str, repo: &str, pr: u64) -> Result<(), ForgejoError> {
|
||||
self.delete(&format!("repos/{owner}/{repo}/pulls/{pr}/merge"))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_releases(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
query: ReleaseQuery,
|
||||
) -> Result<Vec<Release>, ForgejoError> {
|
||||
self.get(&query.to_string(owner, repo)).await
|
||||
}
|
||||
|
||||
pub async fn get_release(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
id: u64,
|
||||
) -> Result<Option<Release>, ForgejoError> {
|
||||
self.get_opt(&format!("repos/{owner}/{repo}/releases/{id}"))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_release_by_tag(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
tag: &str,
|
||||
) -> Result<Option<Release>, ForgejoError> {
|
||||
self.get_opt(&format!("repos/{owner}/{repo}/releases/tags/{tag}"))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_release(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
id: u64,
|
||||
) -> Result<(), ForgejoError> {
|
||||
self.delete(&format!("repos/{owner}/{repo}/releases/{id}"))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_release_by_tag(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
tag: &str,
|
||||
) -> Result<(), ForgejoError> {
|
||||
self.delete(&format!("repos/{owner}/{repo}/releases/tags/{tag}"))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn edit_release(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
id: u64,
|
||||
opts: EditReleaseOption,
|
||||
) -> Result<Release, ForgejoError> {
|
||||
self.patch(&format!("repos/{owner}/{repo}/releases/{id}"), &opts)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_release_attachments(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
id: u64,
|
||||
) -> Result<Vec<Attachment>, ForgejoError> {
|
||||
self.get(&format!("repos/{owner}/{repo}/releases/{id}/assets"))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_release_attachment(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
release_id: u64,
|
||||
attachment_id: u64,
|
||||
) -> Result<Attachment, ForgejoError> {
|
||||
self.get(&format!(
|
||||
"repos/{owner}/{repo}/releases/{release_id}/assets/{attachment_id}"
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_release_attachment(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
id: u64,
|
||||
name: &str,
|
||||
file: Vec<u8>,
|
||||
) -> Result<Attachment, ForgejoError> {
|
||||
self.post_multipart(
|
||||
&format!("repos/{owner}/{repo}/releases/{id}/assets?name={name}"),
|
||||
reqwest::multipart::Form::new().part(
|
||||
"attachment",
|
||||
reqwest::multipart::Part::bytes(file)
|
||||
.file_name("file")
|
||||
.mime_str("*/*")
|
||||
.unwrap(),
|
||||
),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_release_attachment(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
release_id: u64,
|
||||
attachment_id: u64,
|
||||
) -> Result<(), ForgejoError> {
|
||||
self.delete(&format!(
|
||||
"repos/{owner}/{repo}/releases/{release_id}/assets/{attachment_id}"
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn edit_release_attachment(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
release_id: u64,
|
||||
attachment_id: u64,
|
||||
opts: EditAttachmentOption,
|
||||
) -> Result<Attachment, ForgejoError> {
|
||||
self.patch(
|
||||
&format!("repos/{owner}/{repo}/releases/{release_id}/assets/{attachment_id}"),
|
||||
&opts,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_release(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
opts: CreateReleaseOption,
|
||||
) -> Result<Release, ForgejoError> {
|
||||
self.post(&format!("repos/{owner}/{repo}/releases"), &opts)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn latest_release(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
) -> Result<Option<Release>, ForgejoError> {
|
||||
self.get_opt(&format!("repos/{owner}/{repo}/releases/latest"))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn download_zip_archive(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
target: &str,
|
||||
) -> Result<Option<bytes::Bytes>, ForgejoError> {
|
||||
let request = self
|
||||
.client
|
||||
.get(
|
||||
self.url
|
||||
.join(&format!("api/v1/repos/{owner}/{repo}/archive/{target}.zip"))
|
||||
.unwrap(),
|
||||
)
|
||||
.build()?;
|
||||
self.execute_opt_raw(request).await
|
||||
}
|
||||
|
||||
pub async fn download_tarball_archive(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
target: &str,
|
||||
) -> Result<Option<bytes::Bytes>, ForgejoError> {
|
||||
let request = self
|
||||
.client
|
||||
.get(
|
||||
self.url
|
||||
.join(&format!(
|
||||
"api/v1/repos/{owner}/{repo}/archive/{target}.tar.gz"
|
||||
))
|
||||
.unwrap(),
|
||||
)
|
||||
.build()?;
|
||||
self.execute_opt_raw(request).await
|
||||
}
|
||||
|
||||
pub async fn download_release_attachment(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
release: u64,
|
||||
attach: u64,
|
||||
) -> Result<Option<bytes::Bytes>, ForgejoError> {
|
||||
let release = self
|
||||
.get_release_attachment(owner, repo, release, attach)
|
||||
.await?;
|
||||
let request = self.client.get(release.browser_download_url).build()?;
|
||||
self.execute_opt_raw(request).await
|
||||
}
|
||||
|
||||
pub async fn get_tags(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
query: TagQuery,
|
||||
) -> Result<Vec<Tag>, ForgejoError> {
|
||||
self.get(&query.to_string(owner, repo)).await
|
||||
}
|
||||
|
||||
pub async fn create_tag(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
opts: CreateTagOption,
|
||||
) -> Result<Tag, ForgejoError> {
|
||||
self.post(&format!("repos/{owner}/{repo}/tags"), &opts)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_tag(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
tag: &str,
|
||||
) -> Result<Option<Tag>, ForgejoError> {
|
||||
self.get_opt(&format!("repos/{owner}/{repo}/tags/{tag}"))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_tag(&self, owner: &str, repo: &str, tag: &str) -> Result<(), ForgejoError> {
|
||||
self.delete(&format!("repos/{owner}/{repo}/tags/{tag}"))
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct Repository {
|
||||
pub allow_merge_commits: bool,
|
||||
pub allow_rebase: bool,
|
||||
pub allow_rebase_explicit: bool,
|
||||
pub allow_rebase_update: bool,
|
||||
pub allow_squash_merge: bool,
|
||||
pub archived: bool,
|
||||
#[serde(with = "time::serde::rfc3339::option")]
|
||||
pub archived_at: Option<time::OffsetDateTime>,
|
||||
#[serde(deserialize_with = "crate::none_if_blank_url")]
|
||||
pub avatar_url: Option<Url>,
|
||||
pub clone_url: Url,
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub created_at: time::OffsetDateTime,
|
||||
pub default_allow_maintainer_edit: bool,
|
||||
pub default_branch: String,
|
||||
pub default_delete_branch_after_merge: bool,
|
||||
pub default_merge_style: String,
|
||||
pub description: String,
|
||||
pub empty: bool,
|
||||
pub external_tracker: Option<ExternalTracker>,
|
||||
pub external_wiki: Option<ExternalWiki>,
|
||||
pub fork: bool,
|
||||
pub forks_count: u64,
|
||||
pub full_name: String,
|
||||
pub has_actions: bool,
|
||||
pub has_issues: bool,
|
||||
pub has_packages: bool,
|
||||
pub has_projects: bool,
|
||||
pub has_pull_requests: bool,
|
||||
pub has_releases: bool,
|
||||
pub has_wiki: bool,
|
||||
pub html_url: Url,
|
||||
pub id: u64,
|
||||
pub ignore_whitespace_conflicts: bool,
|
||||
pub internal: bool,
|
||||
pub internal_tracker: Option<InternalTracker>,
|
||||
pub language: String,
|
||||
pub languages_url: Url,
|
||||
pub link: String,
|
||||
pub mirror: bool,
|
||||
pub mirror_interval: Option<String>,
|
||||
#[serde(with = "time::serde::rfc3339::option")]
|
||||
pub mirror_updated: Option<time::OffsetDateTime>,
|
||||
pub name: String,
|
||||
pub open_issues_count: u64,
|
||||
pub open_pr_counter: u64,
|
||||
#[serde(deserialize_with = "crate::none_if_blank_url")]
|
||||
pub original_url: Option<Url>,
|
||||
pub owner: User,
|
||||
pub parent: Option<Box<Repository>>,
|
||||
pub permissions: Permission,
|
||||
pub private: bool,
|
||||
pub release_counter: u64,
|
||||
pub repo_transfer: Option<RepoTransfer>,
|
||||
pub size: u64,
|
||||
pub ssh_url: String,
|
||||
pub stars_count: u64,
|
||||
pub template: bool,
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub updated_at: time::OffsetDateTime,
|
||||
pub url: Url,
|
||||
pub watchers_count: u64,
|
||||
pub website: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct RepositoryMeta {
|
||||
pub full_name: String,
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
pub owner: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Debug, PartialEq)]
|
||||
pub struct CreateRepoOption {
|
||||
pub auto_init: bool,
|
||||
pub default_branch: String,
|
||||
pub description: Option<String>,
|
||||
pub gitignores: String,
|
||||
pub issue_labels: String,
|
||||
pub license: String,
|
||||
pub name: String,
|
||||
pub private: bool,
|
||||
pub readme: String,
|
||||
pub template: bool,
|
||||
pub trust_model: TrustModel,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Debug, PartialEq)]
|
||||
pub enum TrustModel {
|
||||
Default,
|
||||
Collaborator,
|
||||
Committer,
|
||||
#[serde(rename = "collaboratorcommiter")]
|
||||
CollaboratorCommitter,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct Milestone {
|
||||
#[serde(with = "time::serde::rfc3339::option")]
|
||||
pub closed_at: Option<time::OffsetDateTime>,
|
||||
pub closed_issues: u64,
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub created_at: time::OffsetDateTime,
|
||||
pub description: String,
|
||||
#[serde(with = "time::serde::rfc3339::option")]
|
||||
pub due_on: Option<time::OffsetDateTime>,
|
||||
pub id: u64,
|
||||
pub open_issues: u64,
|
||||
pub state: State,
|
||||
pub title: String,
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub updated_at: time::OffsetDateTime,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct PullRequest {
|
||||
pub allow_maintainer_edit: bool,
|
||||
pub assignee: User,
|
||||
pub assignees: Vec<User>,
|
||||
pub base: PrBranchInfo,
|
||||
pub body: String,
|
||||
#[serde(with = "time::serde::rfc3339::option")]
|
||||
pub closed_at: Option<time::OffsetDateTime>,
|
||||
pub comments: u64,
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub created_at: time::OffsetDateTime,
|
||||
pub diff_url: Url,
|
||||
#[serde(with = "time::serde::rfc3339::option")]
|
||||
pub due_date: Option<time::OffsetDateTime>,
|
||||
pub head: PrBranchInfo,
|
||||
pub html_url: Url,
|
||||
pub id: u64,
|
||||
pub is_locked: bool,
|
||||
pub labels: Vec<Label>,
|
||||
pub merge_base: String,
|
||||
pub merge_commit_sha: Option<String>,
|
||||
pub mergeable: bool,
|
||||
pub merged: bool,
|
||||
#[serde(with = "time::serde::rfc3339::option")]
|
||||
pub merged_at: Option<time::OffsetDateTime>,
|
||||
pub merged_by: Option<User>,
|
||||
pub milestone: Option<Milestone>,
|
||||
pub number: u64,
|
||||
pub patch_url: Url,
|
||||
pub pin_order: u64,
|
||||
pub requested_reviewers: Option<Vec<User>>,
|
||||
pub state: State,
|
||||
pub title: String,
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub updated_at: time::OffsetDateTime,
|
||||
pub url: Url,
|
||||
pub user: User,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct PrBranchInfo {
|
||||
pub label: String,
|
||||
#[serde(rename = "ref")]
|
||||
pub _ref: String,
|
||||
pub repo: Repository,
|
||||
pub repo_id: u64,
|
||||
pub sha: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct PullRequestMeta {
|
||||
pub merged: bool,
|
||||
#[serde(with = "time::serde::rfc3339::option")]
|
||||
pub merged_at: Option<time::OffsetDateTime>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PullQuery {
|
||||
pub state: Option<State>,
|
||||
pub sort: Option<PullQuerySort>,
|
||||
pub milestone: Option<u64>,
|
||||
pub labels: Vec<u64>,
|
||||
pub page: Option<u32>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
impl PullQuery {
|
||||
fn to_string(&self, owner: &str, repo: &str) -> String {
|
||||
use std::fmt::Write;
|
||||
// This is different to other query struct serialization because
|
||||
// `labels` is serialized so strangely
|
||||
let mut s = String::new();
|
||||
s.push_str("repos/");
|
||||
s.push_str(owner);
|
||||
s.push('/');
|
||||
s.push_str(repo);
|
||||
s.push_str("/pulls?");
|
||||
if let Some(state) = self.state {
|
||||
s.push_str("state=");
|
||||
s.push_str(state.as_str());
|
||||
s.push('&');
|
||||
}
|
||||
if let Some(sort) = self.sort {
|
||||
s.push_str("sort=");
|
||||
s.push_str(sort.as_str());
|
||||
s.push('&');
|
||||
}
|
||||
if let Some(milestone) = self.milestone {
|
||||
s.push_str("sort=");
|
||||
s.write_fmt(format_args!("{milestone}"))
|
||||
.expect("writing to a string never fails");
|
||||
s.push('&');
|
||||
}
|
||||
for label in &self.labels {
|
||||
s.push_str("labels=");
|
||||
s.write_fmt(format_args!("{label}"))
|
||||
.expect("writing to a string never fails");
|
||||
s.push('&');
|
||||
}
|
||||
if let Some(page) = self.page {
|
||||
s.push_str("page=");
|
||||
s.write_fmt(format_args!("{page}"))
|
||||
.expect("writing to a string never fails");
|
||||
s.push('&');
|
||||
}
|
||||
if let Some(limit) = self.limit {
|
||||
s.push_str("limit=");
|
||||
s.write_fmt(format_args!("{limit}"))
|
||||
.expect("writing to a string never fails");
|
||||
s.push('&');
|
||||
}
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum PullQuerySort {
|
||||
Oldest,
|
||||
RecentUpdate,
|
||||
LeastUpdate,
|
||||
MostComment,
|
||||
LeastComment,
|
||||
Priority,
|
||||
}
|
||||
|
||||
impl PullQuerySort {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
PullQuerySort::Oldest => "oldest",
|
||||
PullQuerySort::RecentUpdate => "recentupdate",
|
||||
PullQuerySort::LeastUpdate => "leastupdate",
|
||||
PullQuerySort::MostComment => "mostcomment",
|
||||
PullQuerySort::LeastComment => "leastcomment",
|
||||
PullQuerySort::Priority => "priority",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Debug, PartialEq, Default)]
|
||||
pub struct CreatePullRequestOption {
|
||||
pub assignee: Option<String>,
|
||||
pub assignees: Vec<String>,
|
||||
pub base: String,
|
||||
pub body: String,
|
||||
#[serde(with = "time::serde::rfc3339::option")]
|
||||
pub due_date: Option<time::OffsetDateTime>,
|
||||
pub head: String,
|
||||
pub labels: Vec<u64>,
|
||||
pub milestone: Option<u64>,
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Debug, PartialEq, Default)]
|
||||
pub struct MergePullRequestOption {
|
||||
#[serde(rename = "Do")]
|
||||
pub act: MergePrAction,
|
||||
#[serde(rename = "MergeCommitId")]
|
||||
pub merge_commit_id: Option<String>,
|
||||
#[serde(rename = "MergeMessageField")]
|
||||
pub merge_message_field: Option<String>,
|
||||
#[serde(rename = "MergeTitleField")]
|
||||
pub merge_title_field: Option<String>,
|
||||
pub delete_branch_after_merge: Option<bool>,
|
||||
pub force_merge: Option<bool>,
|
||||
pub head_commit_id: Option<String>,
|
||||
pub merge_when_checks_succeed: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Debug, PartialEq, Default)]
|
||||
pub enum MergePrAction {
|
||||
#[serde(rename = "merge")]
|
||||
#[default]
|
||||
Merge,
|
||||
#[serde(rename = "rebase")]
|
||||
Rebase,
|
||||
#[serde(rename = "rebase-merge")]
|
||||
RebaseMerge,
|
||||
#[serde(rename = "squash")]
|
||||
Squash,
|
||||
#[serde(rename = "manually-merged")]
|
||||
ManuallyMerged,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct Release {
|
||||
pub assets: Vec<Attachment>,
|
||||
pub author: User,
|
||||
pub body: String,
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub created_at: time::OffsetDateTime,
|
||||
pub draft: bool,
|
||||
pub html_url: Url,
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
pub prerelease: bool,
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub published_at: time::OffsetDateTime,
|
||||
pub tag_name: String,
|
||||
pub tarball_url: Url,
|
||||
pub target_commitish: String,
|
||||
pub url: Url,
|
||||
pub zipball_url: Url,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Debug, PartialEq, Default)]
|
||||
pub struct CreateReleaseOption {
|
||||
pub body: String,
|
||||
pub draft: bool,
|
||||
pub name: String,
|
||||
pub prerelease: bool,
|
||||
pub tag_name: String,
|
||||
pub target_commitish: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Debug, PartialEq, Default)]
|
||||
pub struct EditReleaseOption {
|
||||
pub body: Option<String>,
|
||||
pub draft: Option<bool>,
|
||||
pub name: Option<String>,
|
||||
pub prerelease: Option<bool>,
|
||||
pub tag_name: Option<String>,
|
||||
pub target_commitish: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ReleaseQuery {
|
||||
pub draft: Option<bool>,
|
||||
pub prerelease: Option<bool>,
|
||||
pub page: Option<u32>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
impl ReleaseQuery {
|
||||
fn to_string(&self, owner: &str, repo: &str) -> String {
|
||||
format!(
|
||||
"repos/{owner}/{repo}/releases?draft={}&pre-release={}&page={}&limit={}",
|
||||
opt_bool_s(self.draft),
|
||||
opt_bool_s(self.prerelease),
|
||||
self.page.map(|page| page.to_string()).unwrap_or_default(),
|
||||
self.limit.map(|page| page.to_string()).unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn opt_bool_s(b: Option<bool>) -> &'static str {
|
||||
match b {
|
||||
Some(true) => "true",
|
||||
Some(false) => "false",
|
||||
None => "",
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct Tag {
|
||||
pub commit: CommitMeta,
|
||||
pub id: String,
|
||||
pub message: String,
|
||||
pub name: String,
|
||||
pub tarball_url: Url,
|
||||
pub zipball_url: Url,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Debug, PartialEq, Default)]
|
||||
pub struct CreateTagOption {
|
||||
pub message: Option<String>,
|
||||
pub tag_name: String,
|
||||
pub target: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct TagQuery {
|
||||
pub page: Option<u32>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
impl TagQuery {
|
||||
fn to_string(&self, owner: &str, repo: &str) -> String {
|
||||
format!(
|
||||
"repos/{owner}/{repo}/tags?page={}&limit={}",
|
||||
self.page.map(|page| page.to_string()).unwrap_or_default(),
|
||||
self.limit.map(|page| page.to_string()).unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct CommitMeta {
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub created: time::OffsetDateTime,
|
||||
pub url: Url,
|
||||
pub sha: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct ExternalTracker {
|
||||
#[serde(rename = "external_tracker_format")]
|
||||
pub format: String,
|
||||
#[serde(rename = "external_tracker_regexp_pattern")]
|
||||
pub regexp_pattern: String,
|
||||
#[serde(rename = "external_tracker_style")]
|
||||
pub style: String,
|
||||
#[serde(rename = "external_tracker_url")]
|
||||
pub url: Url,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct InternalTracker {
|
||||
pub allow_only_contributors_to_track_time: bool,
|
||||
pub enable_issue_dependencies: bool,
|
||||
pub enable_time_tracker: bool,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct ExternalWiki {
|
||||
#[serde(rename = "external_wiki_url")]
|
||||
pub url: Url,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct Permission {
|
||||
pub admin: bool,
|
||||
pub pull: bool,
|
||||
pub push: bool,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct RepoTransfer {
|
||||
pub doer: User,
|
||||
pub recipient: User,
|
||||
pub teams: Vec<Team>,
|
||||
}
|
59
src/user.rs
59
src/user.rs
|
@ -1,59 +0,0 @@
|
|||
use super::*;
|
||||
|
||||
/// User operations.
|
||||
impl Forgejo {
|
||||
/// Returns info about the authorized user.
|
||||
pub async fn myself(&self) -> Result<User, ForgejoError> {
|
||||
self.get("user").await
|
||||
}
|
||||
|
||||
/// Returns info about the specified user.
|
||||
pub async fn get_user(&self, user: &str) -> Result<Option<User>, ForgejoError> {
|
||||
self.get_opt(&format!("users/{user}/")).await
|
||||
}
|
||||
|
||||
/// Gets the list of users that follow the specified user.
|
||||
pub async fn get_followers(&self, user: &str) -> Result<Option<Vec<User>>, ForgejoError> {
|
||||
self.get_opt(&format!("users/{user}/followers/")).await
|
||||
}
|
||||
|
||||
/// Gets the list of users the specified user is following.
|
||||
pub async fn get_following(&self, user: &str) -> Result<Option<Vec<User>>, ForgejoError> {
|
||||
self.get_opt(&format!("users/{user}/following/")).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub struct User {
|
||||
pub active: bool,
|
||||
pub avatar_url: Url,
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub created: time::OffsetDateTime,
|
||||
pub description: String,
|
||||
pub email: String,
|
||||
pub followers_count: u64,
|
||||
pub following_count: u64,
|
||||
pub full_name: String,
|
||||
pub id: u64,
|
||||
pub is_admin: bool,
|
||||
pub language: String,
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub last_login: time::OffsetDateTime,
|
||||
pub location: String,
|
||||
pub login: String,
|
||||
pub login_name: String,
|
||||
pub prohibit_login: bool,
|
||||
pub restricted: bool,
|
||||
pub starred_repos_count: u64,
|
||||
pub website: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, PartialEq)]
|
||||
pub enum UserVisibility {
|
||||
#[serde(rename = "public")]
|
||||
Public,
|
||||
#[serde(rename = "limited")]
|
||||
Limited,
|
||||
#[serde(rename = "private")]
|
||||
Private,
|
||||
}
|
24321
swagger.v1.json
Normal file
24321
swagger.v1.json
Normal file
File diff suppressed because it is too large
Load diff
507
tests/ci_test.rs
507
tests/ci_test.rs
|
@ -1,60 +1,50 @@
|
|||
use eyre::{ensure, eyre, WrapErr};
|
||||
use forgejo_api::Forgejo;
|
||||
use forgejo_api::{structs::*, Forgejo};
|
||||
|
||||
#[tokio::test]
|
||||
async fn ci() -> eyre::Result<()> {
|
||||
let url = url::Url::parse(&std::env::var("FORGEJO_API_CI_INSTANCE_URL")?)?;
|
||||
let token = std::env::var("FORGEJO_API_CI_TOKEN")?;
|
||||
let api = Forgejo::new(forgejo_api::Auth::Token(&token), url)?;
|
||||
|
||||
let mut results = Vec::new();
|
||||
|
||||
results.push(user(&api).await.wrap_err("user error"));
|
||||
results.push(repo(&api).await.wrap_err("repo error"));
|
||||
results.push(admin(&api).await.wrap_err("admin error"));
|
||||
|
||||
let mut errors = 0;
|
||||
for report in results.into_iter().filter_map(Result::err) {
|
||||
errors += 1;
|
||||
for (i, err) in report.chain().enumerate() {
|
||||
println!("{i}. {err}");
|
||||
if let Some(err) = err.downcast_ref::<forgejo_api::ForgejoError>() {
|
||||
if let forgejo_api::ForgejoError::BadStructure(_, body) = err {
|
||||
println!("BODY: {body}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if errors > 0 {
|
||||
eyre::bail!("test failed");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
fn get_api() -> Forgejo {
|
||||
let url = url::Url::parse(&std::env::var("FORGEJO_API_CI_INSTANCE_URL").unwrap()).unwrap();
|
||||
let token = std::env::var("FORGEJO_API_CI_TOKEN").unwrap();
|
||||
Forgejo::new(forgejo_api::Auth::Token(&token), url).unwrap()
|
||||
}
|
||||
|
||||
async fn user(api: &forgejo_api::Forgejo) -> eyre::Result<()> {
|
||||
let myself = api.myself().await?;
|
||||
ensure!(myself.is_admin, "user should be admin");
|
||||
ensure!(
|
||||
myself.login == "TestingAdmin",
|
||||
#[tokio::test]
|
||||
async fn user() {
|
||||
let api = get_api();
|
||||
|
||||
let myself = api.user_get_current().await.unwrap();
|
||||
assert!(myself.is_admin.unwrap(), "user should be admin");
|
||||
assert_eq!(
|
||||
myself.login.as_ref().unwrap(),
|
||||
"TestingAdmin",
|
||||
"user should be named \"TestingAdmin\""
|
||||
);
|
||||
|
||||
let myself_indirect = api
|
||||
.get_user("TestingAdmin")
|
||||
.await?
|
||||
.ok_or_else(|| eyre!("\"TestingAdmin\" not found, but should have been."))?;
|
||||
ensure!(
|
||||
myself == myself_indirect,
|
||||
let myself_indirect = api.user_get("TestingAdmin").await.unwrap();
|
||||
assert_eq!(
|
||||
myself, myself_indirect,
|
||||
"result of `myself` does not match result of `get_user`"
|
||||
);
|
||||
|
||||
let following = api.get_following("TestingAdmin").await?;
|
||||
ensure!(following == Some(Vec::new()), "following list not empty");
|
||||
let followers = api.get_followers("TestingAdmin").await?;
|
||||
ensure!(followers == Some(Vec::new()), "follower list not empty");
|
||||
let query = UserListFollowingQuery {
|
||||
page: None,
|
||||
limit: None,
|
||||
};
|
||||
let following = api
|
||||
.user_list_following("TestingAdmin", query)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(following, Vec::new(), "following list not empty");
|
||||
|
||||
let url = url::Url::parse(&std::env::var("FORGEJO_API_CI_INSTANCE_URL")?)?;
|
||||
let query = UserListFollowersQuery {
|
||||
page: None,
|
||||
limit: None,
|
||||
};
|
||||
let followers = api
|
||||
.user_list_followers("TestingAdmin", query)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(followers, Vec::new(), "follower list not empty");
|
||||
|
||||
let url = url::Url::parse(&std::env::var("FORGEJO_API_CI_INSTANCE_URL").unwrap()).unwrap();
|
||||
let password_api = Forgejo::new(
|
||||
forgejo_api::Auth::Password {
|
||||
username: "TestingAdmin",
|
||||
|
@ -63,103 +53,133 @@ async fn user(api: &forgejo_api::Forgejo) -> eyre::Result<()> {
|
|||
},
|
||||
url,
|
||||
)
|
||||
.wrap_err("failed to log in using username and password")?;
|
||||
.expect("failed to log in using username and password");
|
||||
|
||||
ensure!(
|
||||
api.myself().await? == password_api.myself().await?,
|
||||
assert!(
|
||||
api.user_get_current().await.unwrap() == password_api.user_get_current().await.unwrap(),
|
||||
"users not equal comparing token-auth and pass-auth"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn repo(api: &forgejo_api::Forgejo) -> eyre::Result<()> {
|
||||
tokio::fs::create_dir("/test_repo").await?;
|
||||
#[tokio::test]
|
||||
async fn repo() {
|
||||
let api = get_api();
|
||||
|
||||
tokio::fs::create_dir("./test_repo").await.unwrap();
|
||||
let git = || {
|
||||
let mut cmd = std::process::Command::new("git");
|
||||
cmd.current_dir("/test_repo");
|
||||
cmd.current_dir("./test_repo");
|
||||
cmd
|
||||
};
|
||||
let _ = git()
|
||||
.args(["config", "--global", "init.defaultBranch", "main"])
|
||||
.status()?;
|
||||
let _ = git().args(["init"]).status()?;
|
||||
.status()
|
||||
.unwrap();
|
||||
let _ = git().args(["init"]).status().unwrap();
|
||||
let _ = git()
|
||||
.args(["config", "user.name", "TestingAdmin"])
|
||||
.status()?;
|
||||
.status()
|
||||
.unwrap();
|
||||
let _ = git()
|
||||
.args(["config", "user.email", "admin@noreply.example.org"])
|
||||
.status()?;
|
||||
tokio::fs::write("/test_repo/README.md", "# Test\nThis is a test repo").await?;
|
||||
let _ = git().args(["add", "."]).status()?;
|
||||
let _ = git().args(["commit", "-m", "initial commit"]).status()?;
|
||||
.status()
|
||||
.unwrap();
|
||||
tokio::fs::write("./test_repo/README.md", "# Test\nThis is a test repo")
|
||||
.await
|
||||
.unwrap();
|
||||
let _ = git().args(["add", "."]).status().unwrap();
|
||||
let _ = git()
|
||||
.args(["commit", "-m", "initial commit"])
|
||||
.status()
|
||||
.unwrap();
|
||||
|
||||
let repo_opt = forgejo_api::CreateRepoOption {
|
||||
auto_init: false,
|
||||
default_branch: "main".into(),
|
||||
let repo_opt = CreateRepoOption {
|
||||
auto_init: Some(false),
|
||||
default_branch: Some("main".into()),
|
||||
description: Some("Test Repo".into()),
|
||||
gitignores: "".into(),
|
||||
issue_labels: "".into(),
|
||||
license: "".into(),
|
||||
gitignores: Some("".into()),
|
||||
issue_labels: Some("".into()),
|
||||
license: Some("".into()),
|
||||
name: "test".into(),
|
||||
private: false,
|
||||
readme: "".into(),
|
||||
template: false,
|
||||
trust_model: forgejo_api::TrustModel::Default,
|
||||
private: Some(false),
|
||||
readme: None,
|
||||
template: Some(false),
|
||||
trust_model: Some(CreateRepoOptionTrustModel::Default),
|
||||
};
|
||||
let remote_repo = api.create_repo(repo_opt).await?;
|
||||
ensure!(
|
||||
remote_repo.has_pull_requests,
|
||||
let remote_repo = api.create_current_user_repo(repo_opt).await.unwrap();
|
||||
assert!(
|
||||
remote_repo.has_pull_requests.unwrap(),
|
||||
"repo does not accept pull requests"
|
||||
);
|
||||
ensure!(
|
||||
remote_repo.owner.login == "TestingAdmin",
|
||||
assert!(
|
||||
remote_repo.owner.as_ref().unwrap().login.as_ref().unwrap() == "TestingAdmin",
|
||||
"repo owner is not \"TestingAdmin\""
|
||||
);
|
||||
ensure!(remote_repo.name == "test", "repo owner is not \"test\"");
|
||||
assert!(
|
||||
remote_repo.name.as_ref().unwrap() == "test",
|
||||
"repo owner is not \"test\""
|
||||
);
|
||||
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
|
||||
|
||||
let mut remote_url = remote_repo.clone_url.clone();
|
||||
let mut remote_url = remote_repo.clone_url.clone().unwrap();
|
||||
remote_url.set_username("TestingAdmin").unwrap();
|
||||
remote_url.set_password(Some("password")).unwrap();
|
||||
let _ = git()
|
||||
.args(["remote", "add", "origin", remote_url.as_str()])
|
||||
.status()?;
|
||||
let _ = git().args(["push", "-u", "origin", "main"]).status()?;
|
||||
.status()
|
||||
.unwrap();
|
||||
let _ = git()
|
||||
.args(["push", "-u", "origin", "main"])
|
||||
.status()
|
||||
.unwrap();
|
||||
|
||||
let _ = git().args(["switch", "-c", "test"]).status()?;
|
||||
let _ = git().args(["switch", "-c", "test"]).status().unwrap();
|
||||
tokio::fs::write(
|
||||
"/test_repo/example.rs",
|
||||
"./test_repo/example.rs",
|
||||
"fn add_one(x: u32) -> u32 { x + 1 }",
|
||||
)
|
||||
.await?;
|
||||
let _ = git().args(["add", "."]).status()?;
|
||||
let _ = git().args(["commit", "-m", "egg"]).status()?;
|
||||
let _ = git().args(["push", "-u", "origin", "test"]).status()?;
|
||||
.await
|
||||
.unwrap();
|
||||
let _ = git().args(["add", "."]).status().unwrap();
|
||||
let _ = git().args(["commit", "-m", "egg"]).status().unwrap();
|
||||
let _ = git()
|
||||
.args(["push", "-u", "origin", "test"])
|
||||
.status()
|
||||
.unwrap();
|
||||
|
||||
let pr_opt = forgejo_api::CreatePullRequestOption {
|
||||
let pr_opt = CreatePullRequestOption {
|
||||
assignee: None,
|
||||
assignees: vec!["TestingAdmin".into()],
|
||||
base: "main".into(),
|
||||
body: "This is a test PR".into(),
|
||||
assignees: Some(vec!["TestingAdmin".into()]),
|
||||
base: Some("main".into()),
|
||||
body: Some("This is a test PR".into()),
|
||||
due_date: None,
|
||||
head: "test".into(),
|
||||
labels: Vec::new(),
|
||||
head: Some("test".into()),
|
||||
labels: None,
|
||||
milestone: None,
|
||||
title: "test pr".into(),
|
||||
title: Some("test pr".into()),
|
||||
};
|
||||
let pr = api
|
||||
.create_pr("TestingAdmin", "test", pr_opt)
|
||||
.repo_create_pull_request("TestingAdmin", "test", pr_opt)
|
||||
.await
|
||||
.wrap_err("couldn't create pr")?;
|
||||
.expect("couldn't create pr");
|
||||
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
|
||||
let is_merged = api
|
||||
.is_merged("TestingAdmin", "test", pr.number)
|
||||
.repo_pull_request_is_merged("TestingAdmin", "test", pr.number.unwrap())
|
||||
.await
|
||||
.wrap_err_with(|| eyre!("couldn't find unmerged pr {}", pr.number))?;
|
||||
ensure!(!is_merged, "pr should not yet be merged");
|
||||
let merge_opt = forgejo_api::MergePullRequestOption {
|
||||
act: forgejo_api::MergePrAction::Merge,
|
||||
.is_ok();
|
||||
assert!(!is_merged, "pr should not yet be merged");
|
||||
let pr_files_query = RepoGetPullRequestFilesQuery {
|
||||
skip_to: None,
|
||||
whitespace: None,
|
||||
page: None,
|
||||
limit: None,
|
||||
};
|
||||
let (_, _) = api
|
||||
.repo_get_pull_request_files("TestingAdmin", "test", pr.number.unwrap(), pr_files_query)
|
||||
.await
|
||||
.unwrap();
|
||||
let merge_opt = MergePullRequestOption {
|
||||
r#do: MergePullRequestOptionDo::Merge,
|
||||
merge_commit_id: None,
|
||||
merge_message_field: None,
|
||||
merge_title_field: None,
|
||||
|
@ -168,230 +188,279 @@ async fn repo(api: &forgejo_api::Forgejo) -> eyre::Result<()> {
|
|||
head_commit_id: None,
|
||||
merge_when_checks_succeed: None,
|
||||
};
|
||||
api.merge_pr("TestingAdmin", "test", pr.number, merge_opt)
|
||||
api.repo_merge_pull_request("TestingAdmin", "test", pr.number.unwrap(), merge_opt)
|
||||
.await
|
||||
.wrap_err_with(|| eyre!("couldn't merge pr {}", pr.number))?;
|
||||
.expect("couldn't merge pr");
|
||||
let is_merged = api
|
||||
.is_merged("TestingAdmin", "test", pr.number)
|
||||
.repo_pull_request_is_merged("TestingAdmin", "test", pr.number.unwrap())
|
||||
.await
|
||||
.wrap_err_with(|| eyre!("couldn't find merged pr {}", pr.number))?;
|
||||
ensure!(is_merged, "pr should be merged");
|
||||
let _ = git().args(["fetch"]).status()?;
|
||||
let _ = git().args(["pull"]).status()?;
|
||||
.is_ok();
|
||||
assert!(is_merged, "pr should be merged");
|
||||
let _ = git().args(["fetch"]).status().unwrap();
|
||||
let _ = git().args(["pull"]).status().unwrap();
|
||||
|
||||
ensure!(
|
||||
api.get_releases("TestingAdmin", "test", forgejo_api::ReleaseQuery::default())
|
||||
let query = RepoListReleasesQuery {
|
||||
draft: None,
|
||||
pre_release: None,
|
||||
per_page: None,
|
||||
page: None,
|
||||
limit: None,
|
||||
};
|
||||
assert!(
|
||||
api.repo_list_releases("TestingAdmin", "test", query)
|
||||
.await
|
||||
.wrap_err("releases list not found")?
|
||||
.unwrap()
|
||||
.is_empty(),
|
||||
"there should be no releases yet"
|
||||
);
|
||||
|
||||
let tag_opt = forgejo_api::CreateTagOption {
|
||||
let tag_opt = CreateTagOption {
|
||||
message: Some("This is a tag!".into()),
|
||||
tag_name: "v1.0".into(),
|
||||
target: None,
|
||||
};
|
||||
api.create_tag("TestingAdmin", "test", tag_opt)
|
||||
api.repo_create_tag("TestingAdmin", "test", tag_opt)
|
||||
.await
|
||||
.wrap_err("failed to create tag")?;
|
||||
.expect("failed to create tag");
|
||||
|
||||
let release_opt = forgejo_api::CreateReleaseOption {
|
||||
body: "This is a release!".into(),
|
||||
draft: true,
|
||||
name: "v1.0".into(),
|
||||
prerelease: false,
|
||||
let release_opt = CreateReleaseOption {
|
||||
body: Some("This is a release!".into()),
|
||||
draft: Some(true),
|
||||
name: Some("v1.0".into()),
|
||||
prerelease: Some(false),
|
||||
tag_name: "v1.0".into(),
|
||||
target_commitish: None,
|
||||
};
|
||||
let release = api
|
||||
.create_release("TestingAdmin", "test", release_opt)
|
||||
.repo_create_release("TestingAdmin", "test", release_opt)
|
||||
.await
|
||||
.wrap_err("failed to create release")?;
|
||||
let edit_release = forgejo_api::EditReleaseOption {
|
||||
.expect("failed to create release");
|
||||
let edit_release = EditReleaseOption {
|
||||
body: None,
|
||||
draft: Some(false),
|
||||
..Default::default()
|
||||
name: None,
|
||||
prerelease: None,
|
||||
tag_name: None,
|
||||
target_commitish: None,
|
||||
};
|
||||
api.edit_release("TestingAdmin", "test", release.id, edit_release)
|
||||
api.repo_edit_release("TestingAdmin", "test", release.id.unwrap(), edit_release)
|
||||
.await
|
||||
.wrap_err("failed to edit release")?;
|
||||
.expect("failed to edit release");
|
||||
|
||||
let release_by_tag = api
|
||||
.get_release_by_tag("TestingAdmin", "test", "v1.0")
|
||||
.repo_get_release_by_tag("TestingAdmin", "test", "v1.0")
|
||||
.await
|
||||
.wrap_err("failed to find release")?;
|
||||
.expect("failed to find release");
|
||||
let release_latest = api
|
||||
.latest_release("TestingAdmin", "test")
|
||||
.repo_get_latest_release("TestingAdmin", "test")
|
||||
.await
|
||||
.wrap_err("failed to find latest release")?;
|
||||
ensure!(release_by_tag == release_latest, "releases not equal");
|
||||
.expect("failed to find latest release");
|
||||
assert!(release_by_tag == release_latest, "releases not equal");
|
||||
|
||||
let attachment = api
|
||||
.create_release_attachment(
|
||||
.repo_create_release_attachment(
|
||||
"TestingAdmin",
|
||||
"test",
|
||||
release.id,
|
||||
"test.txt",
|
||||
release.id.unwrap(),
|
||||
b"This is a file!".to_vec(),
|
||||
RepoCreateReleaseAttachmentQuery {
|
||||
name: Some("test.txt".into()),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.wrap_err("failed to create release attachment")?;
|
||||
ensure!(
|
||||
api.download_release_attachment("TestingAdmin", "test", release.id, attachment.id)
|
||||
.await?
|
||||
.as_deref()
|
||||
== Some(b"This is a file!"),
|
||||
.expect("failed to create release attachment");
|
||||
assert!(
|
||||
&*api
|
||||
.download_release_attachment(
|
||||
"TestingAdmin",
|
||||
"test",
|
||||
release.id.unwrap(),
|
||||
attachment.id.unwrap()
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
== b"This is a file!",
|
||||
"couldn't download attachment"
|
||||
);
|
||||
ensure!(
|
||||
api.download_zip_archive("TestingAdmin", "test", "v1.0")
|
||||
.await?
|
||||
.is_some(),
|
||||
"couldn't download zip archive"
|
||||
);
|
||||
ensure!(
|
||||
api.download_tarball_archive("TestingAdmin", "test", "v1.0")
|
||||
.await?
|
||||
.is_some(),
|
||||
"couldn't download tape archive"
|
||||
);
|
||||
|
||||
api.delete_release_attachment("TestingAdmin", "test", release.id, attachment.id)
|
||||
let _zip_archive = api
|
||||
.repo_get_archive("TestingAdmin", "test", "v1.0.zip")
|
||||
.await
|
||||
.wrap_err("failed to deleted attachment")?;
|
||||
|
||||
api.delete_release("TestingAdmin", "test", release.id)
|
||||
.unwrap();
|
||||
let _tar_archive = api
|
||||
.repo_get_archive("TestingAdmin", "test", "v1.0.tar.gz")
|
||||
.await
|
||||
.wrap_err("failed to delete release")?;
|
||||
.unwrap();
|
||||
// check these contents when their return value is fixed
|
||||
|
||||
api.delete_tag("TestingAdmin", "test", "v1.0")
|
||||
api.repo_delete_release_attachment(
|
||||
"TestingAdmin",
|
||||
"test",
|
||||
release.id.unwrap(),
|
||||
attachment.id.unwrap(),
|
||||
)
|
||||
.await
|
||||
.expect("failed to deleted attachment");
|
||||
|
||||
api.repo_delete_release("TestingAdmin", "test", release.id.unwrap())
|
||||
.await
|
||||
.wrap_err("failed to delete release")?;
|
||||
.expect("failed to delete release");
|
||||
|
||||
Ok(())
|
||||
api.repo_delete_tag("TestingAdmin", "test", "v1.0")
|
||||
.await
|
||||
.expect("failed to delete release");
|
||||
}
|
||||
|
||||
async fn admin(api: &forgejo_api::Forgejo) -> eyre::Result<()> {
|
||||
let user_opt = forgejo_api::CreateUserOption {
|
||||
#[tokio::test]
|
||||
async fn admin() {
|
||||
let api = get_api();
|
||||
|
||||
let user_opt = CreateUserOption {
|
||||
created_at: None,
|
||||
email: "user@noreply.example.org".into(),
|
||||
email: "pipis@noreply.example.org".into(),
|
||||
full_name: None,
|
||||
login_name: None,
|
||||
must_change_password: false,
|
||||
password: "userpass".into(),
|
||||
restricted: false,
|
||||
send_notify: true,
|
||||
must_change_password: None,
|
||||
password: Some("userpass".into()),
|
||||
restricted: Some(false),
|
||||
send_notify: Some(true),
|
||||
source_id: None,
|
||||
username: "Pipis".into(),
|
||||
visibility: "public".into(),
|
||||
visibility: Some("public".into()),
|
||||
};
|
||||
let _ = api
|
||||
.admin_create_user(user_opt)
|
||||
.await
|
||||
.wrap_err("failed to create user")?;
|
||||
.expect("failed to create user");
|
||||
|
||||
let query = AdminSearchUsersQuery {
|
||||
source_id: None,
|
||||
login_name: None,
|
||||
page: None,
|
||||
limit: None,
|
||||
};
|
||||
let users = api
|
||||
.admin_users(forgejo_api::AdminUserQuery::default())
|
||||
.admin_search_users(query)
|
||||
.await
|
||||
.wrap_err("failed to search users")?;
|
||||
ensure!(
|
||||
users.iter().find(|u| u.login == "Pipis").is_some(),
|
||||
"could not find new user"
|
||||
);
|
||||
let users = api
|
||||
.admin_get_emails(forgejo_api::EmailListQuery::default())
|
||||
.await
|
||||
.wrap_err("failed to search emails")?;
|
||||
ensure!(
|
||||
.expect("failed to search users");
|
||||
assert!(
|
||||
users
|
||||
.iter()
|
||||
.find(|u| u.email == "user@noreply.example.org")
|
||||
.find(|u| u.login.as_ref().unwrap() == "Pipis")
|
||||
.is_some(),
|
||||
"could not find new user"
|
||||
);
|
||||
let query = AdminGetAllEmailsQuery {
|
||||
page: None,
|
||||
limit: None,
|
||||
};
|
||||
let users = api
|
||||
.admin_get_all_emails(query)
|
||||
.await
|
||||
.expect("failed to search emails");
|
||||
assert!(
|
||||
users
|
||||
.iter()
|
||||
.find(|u| u.email.as_ref().unwrap() == "pipis@noreply.example.org")
|
||||
.is_some(),
|
||||
"could not find new user"
|
||||
);
|
||||
|
||||
let org_opt = forgejo_api::CreateOrgOption {
|
||||
let org_opt = CreateOrgOption {
|
||||
description: None,
|
||||
email: None,
|
||||
full_name: None,
|
||||
location: None,
|
||||
repo_admin_change_team_access: None,
|
||||
username: "test-org".into(),
|
||||
visibility: forgejo_api::OrgVisibility::Public,
|
||||
visibility: Some(CreateOrgOptionVisibility::Public),
|
||||
website: None,
|
||||
};
|
||||
let _ = api
|
||||
.admin_create_org("Pipis", org_opt)
|
||||
.await
|
||||
.wrap_err("failed to create org")?;
|
||||
ensure!(
|
||||
!api.admin_get_orgs(forgejo_api::AdminOrganizationQuery::default())
|
||||
.await?
|
||||
.is_empty(),
|
||||
.expect("failed to create org");
|
||||
let query = AdminGetAllOrgsQuery {
|
||||
page: None,
|
||||
limit: None,
|
||||
};
|
||||
assert!(
|
||||
!api.admin_get_all_orgs(query).await.unwrap().is_empty(),
|
||||
"org list empty"
|
||||
);
|
||||
|
||||
let key_opt = forgejo_api::CreateKeyOption {
|
||||
let key_opt = CreateKeyOption {
|
||||
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN68ehQAsbGEwlXPa2AxbAh1QxFQrtRel2jeC0hRlPc1 user@noreply.example.org".into(),
|
||||
read_only: None,
|
||||
title: "Example Key".into(),
|
||||
};
|
||||
let key = api
|
||||
.admin_add_key("Pipis", key_opt)
|
||||
.admin_create_public_key("Pipis", key_opt)
|
||||
.await
|
||||
.wrap_err("failed to create key")?;
|
||||
api.admin_delete_key("Pipis", key.id)
|
||||
.expect("failed to create key");
|
||||
api.admin_delete_user_public_key("Pipis", key.id.unwrap())
|
||||
.await
|
||||
.wrap_err("failed to delete key")?;
|
||||
.expect("failed to delete key");
|
||||
|
||||
let rename_opt = forgejo_api::RenameUserOption {
|
||||
let rename_opt = RenameUserOption {
|
||||
new_username: "Bepis".into(),
|
||||
};
|
||||
api.admin_rename_user("Pipis", rename_opt)
|
||||
.await
|
||||
.wrap_err("failed to rename user")?;
|
||||
api.admin_delete_user("Bepis", true)
|
||||
.expect("failed to rename user");
|
||||
let query = AdminDeleteUserQuery { purge: Some(true) };
|
||||
api.admin_delete_user("Bepis", query)
|
||||
.await
|
||||
.wrap_err("failed to delete user")?;
|
||||
ensure!(
|
||||
api.admin_delete_user("Ghost", true).await.is_err(),
|
||||
.expect("failed to delete user");
|
||||
let query = AdminDeleteUserQuery { purge: Some(true) };
|
||||
assert!(
|
||||
api.admin_delete_user("Ghost", query).await.is_err(),
|
||||
"deleting fake user should fail"
|
||||
);
|
||||
|
||||
let query = AdminCronListQuery {
|
||||
page: None,
|
||||
limit: None,
|
||||
};
|
||||
let crons = api
|
||||
.admin_get_crons(forgejo_api::CronQuery::default())
|
||||
.admin_cron_list(query)
|
||||
.await
|
||||
.wrap_err("failed to get crons list")?;
|
||||
api.admin_run_cron(&crons.get(0).ok_or_else(|| eyre!("no crons"))?.name)
|
||||
.expect("failed to get crons list");
|
||||
api.admin_cron_run(&crons.get(0).expect("no crons").name.as_ref().unwrap())
|
||||
.await
|
||||
.wrap_err("failed to run cron")?;
|
||||
.expect("failed to run cron");
|
||||
|
||||
let hook_opt = forgejo_api::CreateHookOption {
|
||||
let hook_opt = CreateHookOption {
|
||||
active: None,
|
||||
authorization_header: None,
|
||||
branch_filter: None,
|
||||
config: forgejo_api::CreateHookOptionConfig {
|
||||
content_type: "json".into(),
|
||||
url: url::Url::parse("http://test.local/").unwrap(),
|
||||
other: Default::default(),
|
||||
config: CreateHookOptionConfig {
|
||||
// content_type: "json".into(),
|
||||
// url: url::Url::parse("http://test.local/").unwrap(),
|
||||
additional: [
|
||||
("content_type".into(), "json".into()),
|
||||
("url".into(), "http://test.local/".into()),
|
||||
]
|
||||
.into(),
|
||||
},
|
||||
events: Vec::new(),
|
||||
_type: forgejo_api::HookType::Forgejo,
|
||||
events: Some(Vec::new()),
|
||||
r#type: CreateHookOptionType::Gitea,
|
||||
};
|
||||
// yarr har har me matey this is me hook
|
||||
let hook = api
|
||||
.admin_create_hook(hook_opt)
|
||||
.await
|
||||
.wrap_err("failed to create hook")?;
|
||||
let edit_hook = forgejo_api::EditHookOption {
|
||||
.expect("failed to create hook");
|
||||
let edit_hook = EditHookOption {
|
||||
active: Some(true),
|
||||
..Default::default()
|
||||
authorization_header: None,
|
||||
branch_filter: None,
|
||||
config: None,
|
||||
events: None,
|
||||
};
|
||||
api.admin_edit_hook(hook.id, edit_hook)
|
||||
api.admin_edit_hook(hook.id.unwrap(), edit_hook)
|
||||
.await
|
||||
.wrap_err("failed to edit hook")?;
|
||||
api.admin_delete_hook(hook.id)
|
||||
.expect("failed to edit hook");
|
||||
api.admin_delete_hook(hook.id.unwrap())
|
||||
.await
|
||||
.wrap_err("failed to delete hook")?;
|
||||
|
||||
Ok(())
|
||||
.expect("failed to delete hook");
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue