1
0
Fork 0

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:
Cyborus 2024-03-20 18:09:31 +00:00
commit 96b92daf23
21 changed files with 40902 additions and 2735 deletions

61
Cargo.lock generated
View file

@ -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",

View file

@ -1,3 +1,4 @@
workspace = { members = ["generator"] }
[package]
name = "forgejo-api"
version = "0.1.0"

13
generator/Cargo.toml Normal file
View 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
View 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
View 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 &param._in {
ParameterIn::Path { param: _ } | ParameterIn::FormData { param: _ } => {
write!(&mut out, "/// - `{}`", param.name)?;
if let Some(description) = &param.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) = &param.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(&param, 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(
&param._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(&param.name);
match &param._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 &param._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

File diff suppressed because it is too large Load diff

654
generator/src/structs.rs Normal file
View 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 } = &param._in {
let field_name = crate::sanitize_ident(&param.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) = &param.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)
}

View file

@ -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

File diff suppressed because it is too large Load diff

2
src/generated/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod methods;
pub mod structs;

5959
src/generated/structs.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -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,
}

View file

@ -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
}

View file

@ -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,
}

View file

@ -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,
}

View file

@ -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>,
}

View file

@ -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,
}

View file

@ -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>,
}

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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");
}