Merge pull request 'Auto-generate API' (#38) from autogen into main
Reviewed-on: https://codeberg.org/Cyborus/forgejo-api/pulls/38
This commit is contained in:
		
						commit
						96b92daf23
					
				
					 21 changed files with 40902 additions and 2735 deletions
				
			
		
							
								
								
									
										61
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										61
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							| 
						 | 
				
			
			@ -136,9 +136,9 @@ dependencies = [
 | 
			
		|||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "eyre"
 | 
			
		||||
version = "0.6.9"
 | 
			
		||||
version = "0.6.11"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "80f656be11ddf91bd709454d15d5bd896fbaf4cc3314e69349e4d1569f5b46cd"
 | 
			
		||||
checksum = "b6267a1fa6f59179ea4afc8e50fd8612a3cc60bc858f786ff877a4a8cb042799"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "indenter",
 | 
			
		||||
 "once_cell",
 | 
			
		||||
| 
						 | 
				
			
			@ -191,9 +191,9 @@ dependencies = [
 | 
			
		|||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "form_urlencoded"
 | 
			
		||||
version = "1.2.0"
 | 
			
		||||
version = "1.2.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
 | 
			
		||||
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "percent-encoding",
 | 
			
		||||
]
 | 
			
		||||
| 
						 | 
				
			
			@ -237,6 +237,17 @@ dependencies = [
 | 
			
		|||
 "pin-utils",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "generator"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "eyre",
 | 
			
		||||
 "heck",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "url",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "gimli"
 | 
			
		||||
version = "0.28.0"
 | 
			
		||||
| 
						 | 
				
			
			@ -268,6 +279,12 @@ version = "0.12.3"
 | 
			
		|||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "heck"
 | 
			
		||||
version = "0.4.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "http"
 | 
			
		||||
version = "0.2.9"
 | 
			
		||||
| 
						 | 
				
			
			@ -341,9 +358,9 @@ dependencies = [
 | 
			
		|||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "idna"
 | 
			
		||||
version = "0.4.0"
 | 
			
		||||
version = "0.5.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
 | 
			
		||||
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "unicode-bidi",
 | 
			
		||||
 "unicode-normalization",
 | 
			
		||||
| 
						 | 
				
			
			@ -531,9 +548,9 @@ dependencies = [
 | 
			
		|||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "percent-encoding"
 | 
			
		||||
version = "2.3.0"
 | 
			
		||||
version = "2.3.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
 | 
			
		||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "pin-project-lite"
 | 
			
		||||
| 
						 | 
				
			
			@ -561,18 +578,18 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
 | 
			
		|||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "proc-macro2"
 | 
			
		||||
version = "1.0.69"
 | 
			
		||||
version = "1.0.76"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da"
 | 
			
		||||
checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "unicode-ident",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "quote"
 | 
			
		||||
version = "1.0.33"
 | 
			
		||||
version = "1.0.35"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
 | 
			
		||||
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
]
 | 
			
		||||
| 
						 | 
				
			
			@ -684,18 +701,18 @@ dependencies = [
 | 
			
		|||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "serde"
 | 
			
		||||
version = "1.0.192"
 | 
			
		||||
version = "1.0.195"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001"
 | 
			
		||||
checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "serde_derive",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "serde_derive"
 | 
			
		||||
version = "1.0.192"
 | 
			
		||||
version = "1.0.195"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1"
 | 
			
		||||
checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
| 
						 | 
				
			
			@ -704,9 +721,9 @@ dependencies = [
 | 
			
		|||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "serde_json"
 | 
			
		||||
version = "1.0.108"
 | 
			
		||||
version = "1.0.111"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
 | 
			
		||||
checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "itoa",
 | 
			
		||||
 "ryu",
 | 
			
		||||
| 
						 | 
				
			
			@ -762,9 +779,9 @@ checksum = "b5097ec7ea7218135541ad96348f1441d0c616537dd4ed9c47205920c35d7d97"
 | 
			
		|||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "syn"
 | 
			
		||||
version = "2.0.39"
 | 
			
		||||
version = "2.0.48"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a"
 | 
			
		||||
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
| 
						 | 
				
			
			@ -983,9 +1000,9 @@ dependencies = [
 | 
			
		|||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "url"
 | 
			
		||||
version = "2.4.1"
 | 
			
		||||
version = "2.5.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5"
 | 
			
		||||
checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "form_urlencoded",
 | 
			
		||||
 "idna",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,3 +1,4 @@
 | 
			
		|||
workspace = { members = ["generator"] }
 | 
			
		||||
[package]
 | 
			
		||||
name = "forgejo-api"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										13
									
								
								generator/Cargo.toml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								generator/Cargo.toml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
[package]
 | 
			
		||||
name = "generator"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
edition = "2021"
 | 
			
		||||
 | 
			
		||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
eyre = "0.6.11"
 | 
			
		||||
heck = "0.4.1"
 | 
			
		||||
serde = { version = "1.0.195", features = ["derive"] }
 | 
			
		||||
serde_json = "1.0.111"
 | 
			
		||||
url = { version = "2.5.0", features = ["serde"] }
 | 
			
		||||
							
								
								
									
										287
									
								
								generator/src/main.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										287
									
								
								generator/src/main.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,287 @@
 | 
			
		|||
use std::{ffi::OsString, path::PathBuf};
 | 
			
		||||
 | 
			
		||||
mod methods;
 | 
			
		||||
mod openapi;
 | 
			
		||||
mod structs;
 | 
			
		||||
 | 
			
		||||
use heck::{ToPascalCase, ToSnakeCase};
 | 
			
		||||
use openapi::*;
 | 
			
		||||
 | 
			
		||||
fn main() -> eyre::Result<()> {
 | 
			
		||||
    let spec = get_spec()?;
 | 
			
		||||
    let files = [
 | 
			
		||||
        ("mod.rs".into(), "pub mod structs;\npub mod methods;".into()),
 | 
			
		||||
        ("methods.rs".into(), methods::create_methods(&spec)?),
 | 
			
		||||
        ("structs.rs".into(), structs::create_structs(&spec)?),
 | 
			
		||||
    ];
 | 
			
		||||
    save_generated(&files)?;
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn get_spec() -> eyre::Result<OpenApiV2> {
 | 
			
		||||
    let path = std::env::var_os("FORGEJO_API_SPEC_PATH")
 | 
			
		||||
        .unwrap_or_else(|| OsString::from("./swagger.v1.json"));
 | 
			
		||||
    let file = std::fs::read(path)?;
 | 
			
		||||
    let spec = serde_json::from_slice::<OpenApiV2>(&file)?;
 | 
			
		||||
    spec.validate()?;
 | 
			
		||||
    Ok(spec)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn save_generated(files: &[(String, String)]) -> eyre::Result<()> {
 | 
			
		||||
    let root_path = PathBuf::from(
 | 
			
		||||
        std::env::var_os("FORGEJO_API_GENERATED_PATH")
 | 
			
		||||
            .unwrap_or_else(|| OsString::from("./src/generated/")),
 | 
			
		||||
    );
 | 
			
		||||
    for (path, file) in files {
 | 
			
		||||
        let path = root_path.join(path);
 | 
			
		||||
        std::fs::create_dir_all(path.parent().ok_or_else(|| eyre::eyre!("no parent dir"))?)?;
 | 
			
		||||
        std::fs::write(&path, file)?;
 | 
			
		||||
        run_rustfmt_on(&path);
 | 
			
		||||
    }
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn run_rustfmt_on(path: &std::path::Path) {
 | 
			
		||||
    let mut rustfmt = std::process::Command::new("rustfmt");
 | 
			
		||||
 | 
			
		||||
    rustfmt.arg(path);
 | 
			
		||||
    rustfmt.args(["--edition", "2021"]);
 | 
			
		||||
 | 
			
		||||
    if let Err(e) = rustfmt.status() {
 | 
			
		||||
        println!("Tried to format {path:?}, but failed to do so! :(");
 | 
			
		||||
        println!("Error:\n{e}");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn schema_ref_type_name(spec: &OpenApiV2, schema: &MaybeRef<Schema>) -> eyre::Result<String> {
 | 
			
		||||
    let name = if let MaybeRef::Ref { _ref } = schema {
 | 
			
		||||
        _ref.rsplit_once("/").map(|(_, b)| b)
 | 
			
		||||
    } else {
 | 
			
		||||
        None
 | 
			
		||||
    };
 | 
			
		||||
    let schema = schema.deref(spec)?;
 | 
			
		||||
    schema_type_name(spec, name, schema)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn schema_type_name(
 | 
			
		||||
    spec: &OpenApiV2,
 | 
			
		||||
    definition_name: Option<&str>,
 | 
			
		||||
    schema: &Schema,
 | 
			
		||||
) -> eyre::Result<String> {
 | 
			
		||||
    if let Some(ty) = &schema._type {
 | 
			
		||||
        match ty {
 | 
			
		||||
            SchemaType::One(prim) => {
 | 
			
		||||
                let name = match prim {
 | 
			
		||||
                    Primitive::String => match schema.format.as_deref() {
 | 
			
		||||
                        Some("date") => "time::Date",
 | 
			
		||||
                        Some("date-time") => "time::OffsetDateTime",
 | 
			
		||||
                        _ => "String",
 | 
			
		||||
                    }
 | 
			
		||||
                    .to_string(),
 | 
			
		||||
                    Primitive::Number => match schema.format.as_deref() {
 | 
			
		||||
                        Some("float") => "f32",
 | 
			
		||||
                        Some("double") => "f64",
 | 
			
		||||
                        _ => "f64",
 | 
			
		||||
                    }
 | 
			
		||||
                    .to_string(),
 | 
			
		||||
                    Primitive::Integer => match schema.format.as_deref() {
 | 
			
		||||
                        Some("int32") => "u32",
 | 
			
		||||
                        Some("int64") => "u64",
 | 
			
		||||
                        _ => "u32",
 | 
			
		||||
                    }
 | 
			
		||||
                    .to_string(),
 | 
			
		||||
                    Primitive::Boolean => "bool".to_string(),
 | 
			
		||||
                    Primitive::Array => {
 | 
			
		||||
                        let item_name = match &schema.items {
 | 
			
		||||
                            Some(item_schema) => schema_ref_type_name(spec, item_schema)?,
 | 
			
		||||
                            None => "serde_json::Value".into(),
 | 
			
		||||
                        };
 | 
			
		||||
                        format!("Vec<{item_name}>")
 | 
			
		||||
                    }
 | 
			
		||||
                    Primitive::Null => "()".to_string(),
 | 
			
		||||
                    Primitive::Object => {
 | 
			
		||||
                        match (&schema.title, definition_name) {
 | 
			
		||||
                            // Some of the titles are actually descriptions; not sure why
 | 
			
		||||
                            // Checking for a space filters that out
 | 
			
		||||
                            (Some(title), _) if !title.contains(' ') => title.to_string(),
 | 
			
		||||
                            (_, Some(definition_name)) => definition_name.to_string(),
 | 
			
		||||
                            (_, None) => "BTreeMap<String, serde_json::Value>".to_string(),
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                };
 | 
			
		||||
                Ok(name.to_owned())
 | 
			
		||||
            }
 | 
			
		||||
            SchemaType::List(_) => todo!(),
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        Ok("serde_json::Value".into())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn schema_is_string(spec: &OpenApiV2, schema: &MaybeRef<Schema>) -> eyre::Result<bool> {
 | 
			
		||||
    let schema = schema.deref(spec)?;
 | 
			
		||||
    let is_str = match schema._type {
 | 
			
		||||
        Some(SchemaType::One(Primitive::String)) => true,
 | 
			
		||||
        _ => false,
 | 
			
		||||
    };
 | 
			
		||||
    Ok(is_str)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn sanitize_ident(s: &str) -> String {
 | 
			
		||||
    let mut s = s.to_snake_case();
 | 
			
		||||
    let keywords = [
 | 
			
		||||
        "as",
 | 
			
		||||
        "break",
 | 
			
		||||
        "const",
 | 
			
		||||
        "continue",
 | 
			
		||||
        "crate",
 | 
			
		||||
        "else",
 | 
			
		||||
        "enum",
 | 
			
		||||
        "extern",
 | 
			
		||||
        "false",
 | 
			
		||||
        "fn",
 | 
			
		||||
        "for",
 | 
			
		||||
        "if",
 | 
			
		||||
        "impl",
 | 
			
		||||
        "in",
 | 
			
		||||
        "let",
 | 
			
		||||
        "loop",
 | 
			
		||||
        "match",
 | 
			
		||||
        "mod",
 | 
			
		||||
        "move",
 | 
			
		||||
        "mut",
 | 
			
		||||
        "pub",
 | 
			
		||||
        "ref",
 | 
			
		||||
        "return",
 | 
			
		||||
        "self",
 | 
			
		||||
        "Self",
 | 
			
		||||
        "static",
 | 
			
		||||
        "struct",
 | 
			
		||||
        "super",
 | 
			
		||||
        "trait",
 | 
			
		||||
        "true",
 | 
			
		||||
        "type",
 | 
			
		||||
        "unsafe",
 | 
			
		||||
        "use",
 | 
			
		||||
        "where",
 | 
			
		||||
        "while",
 | 
			
		||||
        "abstract",
 | 
			
		||||
        "become",
 | 
			
		||||
        "box",
 | 
			
		||||
        "do",
 | 
			
		||||
        "final",
 | 
			
		||||
        "macro",
 | 
			
		||||
        "override",
 | 
			
		||||
        "priv",
 | 
			
		||||
        "typeof",
 | 
			
		||||
        "unsized",
 | 
			
		||||
        "virtual",
 | 
			
		||||
        "yield",
 | 
			
		||||
        "async",
 | 
			
		||||
        "await",
 | 
			
		||||
        "dyn",
 | 
			
		||||
        "try",
 | 
			
		||||
        "macro_rules",
 | 
			
		||||
        "union",
 | 
			
		||||
    ];
 | 
			
		||||
    if s == "self" {
 | 
			
		||||
        s = "this".into();
 | 
			
		||||
    }
 | 
			
		||||
    if keywords.contains(&&*s) {
 | 
			
		||||
        s.insert_str(0, "r#");
 | 
			
		||||
    }
 | 
			
		||||
    s
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn schema_subtype_name(
 | 
			
		||||
    spec: &OpenApiV2,
 | 
			
		||||
    parent_name: &str,
 | 
			
		||||
    name: &str,
 | 
			
		||||
    schema: &Schema,
 | 
			
		||||
    ty: &mut String,
 | 
			
		||||
) -> eyre::Result<bool> {
 | 
			
		||||
    let b = match schema {
 | 
			
		||||
        Schema {
 | 
			
		||||
            _type: Some(SchemaType::One(Primitive::Object)),
 | 
			
		||||
            properties: Some(_),
 | 
			
		||||
            ..
 | 
			
		||||
        }
 | 
			
		||||
        | Schema {
 | 
			
		||||
            _type: Some(SchemaType::One(Primitive::String)),
 | 
			
		||||
            _enum: Some(_),
 | 
			
		||||
            ..
 | 
			
		||||
        } => {
 | 
			
		||||
            *ty = format!("{parent_name}{}", name.to_pascal_case());
 | 
			
		||||
            true
 | 
			
		||||
        }
 | 
			
		||||
        Schema {
 | 
			
		||||
            _type: Some(SchemaType::One(Primitive::Object)),
 | 
			
		||||
            properties: None,
 | 
			
		||||
            additional_properties: Some(additional),
 | 
			
		||||
            ..
 | 
			
		||||
        } => {
 | 
			
		||||
            let additional = additional.deref(spec)?;
 | 
			
		||||
            let mut additional_ty = crate::schema_type_name(spec, None, additional)?;
 | 
			
		||||
            schema_subtype_name(spec, parent_name, name, additional, &mut additional_ty)?;
 | 
			
		||||
            *ty = format!("BTreeMap<String, {additional_ty}>");
 | 
			
		||||
            true
 | 
			
		||||
        }
 | 
			
		||||
        Schema {
 | 
			
		||||
            _type: Some(SchemaType::One(Primitive::Array)),
 | 
			
		||||
            items: Some(items),
 | 
			
		||||
            ..
 | 
			
		||||
        } => {
 | 
			
		||||
            if let MaybeRef::Value { value } = &**items {
 | 
			
		||||
                if schema_subtype_name(spec, parent_name, name, value, ty)? {
 | 
			
		||||
                    *ty = format!("Vec<{ty}>");
 | 
			
		||||
                    true
 | 
			
		||||
                } else {
 | 
			
		||||
                    false
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                false
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        _ => false,
 | 
			
		||||
    };
 | 
			
		||||
    Ok(b)
 | 
			
		||||
}
 | 
			
		||||
fn schema_subtypes(
 | 
			
		||||
    spec: &OpenApiV2,
 | 
			
		||||
    parent_name: &str,
 | 
			
		||||
    name: &str,
 | 
			
		||||
    schema: &Schema,
 | 
			
		||||
    subtypes: &mut Vec<String>,
 | 
			
		||||
) -> eyre::Result<()> {
 | 
			
		||||
    match schema {
 | 
			
		||||
        Schema {
 | 
			
		||||
            _type: Some(SchemaType::One(Primitive::Object)),
 | 
			
		||||
            properties: Some(_),
 | 
			
		||||
            ..
 | 
			
		||||
        } => {
 | 
			
		||||
            let name = format!("{parent_name}{}", name.to_pascal_case());
 | 
			
		||||
            let subtype = structs::create_struct_for_definition(spec, &name, schema)?;
 | 
			
		||||
            subtypes.push(subtype);
 | 
			
		||||
        }
 | 
			
		||||
        Schema {
 | 
			
		||||
            _type: Some(SchemaType::One(Primitive::String)),
 | 
			
		||||
            _enum: Some(_enum),
 | 
			
		||||
            ..
 | 
			
		||||
        } => {
 | 
			
		||||
            let name = format!("{parent_name}{}", name.to_pascal_case());
 | 
			
		||||
            let subtype = structs::create_enum(&name, schema.description.as_deref(), _enum, false)?;
 | 
			
		||||
            subtypes.push(subtype);
 | 
			
		||||
        }
 | 
			
		||||
        Schema {
 | 
			
		||||
            _type: Some(SchemaType::One(Primitive::Array)),
 | 
			
		||||
            items: Some(items),
 | 
			
		||||
            ..
 | 
			
		||||
        } => {
 | 
			
		||||
            if let MaybeRef::Value { value } = &**items {
 | 
			
		||||
                schema_subtypes(spec, parent_name, name, value, subtypes)?;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        _ => (),
 | 
			
		||||
    };
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										612
									
								
								generator/src/methods.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										612
									
								
								generator/src/methods.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,612 @@
 | 
			
		|||
use crate::{openapi::*, schema_ref_type_name};
 | 
			
		||||
use eyre::WrapErr;
 | 
			
		||||
use heck::{ToPascalCase, ToSnakeCase};
 | 
			
		||||
use std::fmt::Write;
 | 
			
		||||
 | 
			
		||||
pub fn create_methods(spec: &OpenApiV2) -> eyre::Result<String> {
 | 
			
		||||
    let mut s = String::new();
 | 
			
		||||
    s.push_str("use crate::ForgejoError;\n");
 | 
			
		||||
    s.push_str("use std::collections::BTreeMap;");
 | 
			
		||||
    s.push_str("use super::structs::*;\n");
 | 
			
		||||
    s.push_str("\n");
 | 
			
		||||
    s.push_str("impl crate::Forgejo {\n");
 | 
			
		||||
    for (path, item) in &spec.paths {
 | 
			
		||||
        s.push_str(&create_methods_for_path(&spec, path, item).wrap_err_with(|| path.clone())?);
 | 
			
		||||
    }
 | 
			
		||||
    s.push_str("}\n");
 | 
			
		||||
    Ok(s)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn create_methods_for_path(spec: &OpenApiV2, path: &str, item: &PathItem) -> eyre::Result<String> {
 | 
			
		||||
    let mut s = String::new();
 | 
			
		||||
    if let Some(op) = &item.get {
 | 
			
		||||
        s.push_str(&create_get_method(spec, path, op).wrap_err("GET")?);
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(op) = &item.put {
 | 
			
		||||
        s.push_str(&create_put_method(spec, path, op).wrap_err("PUT")?);
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(op) = &item.post {
 | 
			
		||||
        s.push_str(&create_post_method(spec, path, op).wrap_err("POST")?);
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(op) = &item.delete {
 | 
			
		||||
        s.push_str(&create_delete_method(spec, path, op).wrap_err("DELETE")?);
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(op) = &item.options {
 | 
			
		||||
        s.push_str(&create_options_method(spec, path, op).wrap_err("OPTIONS")?);
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(op) = &item.head {
 | 
			
		||||
        s.push_str(&create_head_method(spec, path, op).wrap_err("HEAD")?);
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(op) = &item.patch {
 | 
			
		||||
        s.push_str(&create_patch_method(spec, path, op).wrap_err("PATCH")?);
 | 
			
		||||
    }
 | 
			
		||||
    Ok(s)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn create_get_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> {
 | 
			
		||||
    let doc = method_docs(spec, op)?;
 | 
			
		||||
    let sig = fn_signature_from_op(spec, op)?;
 | 
			
		||||
    let body = create_method_body(spec, "get", path, op)?;
 | 
			
		||||
    Ok(format!("{doc}{sig} {{\n    {body}\n}}\n\n"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn create_put_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> {
 | 
			
		||||
    let doc = method_docs(spec, op)?;
 | 
			
		||||
    let sig = fn_signature_from_op(spec, op)?;
 | 
			
		||||
    let body = create_method_body(spec, "put", path, op)?;
 | 
			
		||||
    Ok(format!("{doc}{sig} {{\n    {body}\n}}\n\n"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn create_post_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> {
 | 
			
		||||
    let doc = method_docs(spec, op)?;
 | 
			
		||||
    let sig = fn_signature_from_op(spec, op)?;
 | 
			
		||||
    let body = create_method_body(spec, "post", path, op)?;
 | 
			
		||||
    Ok(format!("{doc}{sig} {{\n    {body}\n}}\n\n"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn create_delete_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> {
 | 
			
		||||
    let doc = method_docs(spec, op)?;
 | 
			
		||||
    let sig = fn_signature_from_op(spec, op)?;
 | 
			
		||||
    let body = create_method_body(spec, "delete", path, op)?;
 | 
			
		||||
    Ok(format!("{doc}{sig} {{\n    {body}\n}}\n\n"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn create_options_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> {
 | 
			
		||||
    let doc = method_docs(spec, op)?;
 | 
			
		||||
    let sig = fn_signature_from_op(spec, op)?;
 | 
			
		||||
    let body = create_method_body(spec, "options", path, op)?;
 | 
			
		||||
    Ok(format!("{doc}{sig} {{\n    {body}\n}}\n\n"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn create_head_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> {
 | 
			
		||||
    let doc = method_docs(spec, op)?;
 | 
			
		||||
    let sig = fn_signature_from_op(spec, op)?;
 | 
			
		||||
    let body = create_method_body(spec, "head", path, op)?;
 | 
			
		||||
    Ok(format!("{doc}{sig} {{\n    {body}\n}}\n\n"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn create_patch_method(spec: &OpenApiV2, path: &str, op: &Operation) -> eyre::Result<String> {
 | 
			
		||||
    let doc = method_docs(spec, op)?;
 | 
			
		||||
    let sig = fn_signature_from_op(spec, op)?;
 | 
			
		||||
    let body = create_method_body(spec, "patch", path, op)?;
 | 
			
		||||
    Ok(format!("{doc}{sig} {{\n    {body}\n}}\n\n"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn method_docs(spec: &OpenApiV2, op: &Operation) -> eyre::Result<String> {
 | 
			
		||||
    let mut out = String::new();
 | 
			
		||||
    let mut prev = false;
 | 
			
		||||
    if let Some(summary) = &op.summary {
 | 
			
		||||
        write!(&mut out, "/// {summary}\n")?;
 | 
			
		||||
        prev = true;
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(params) = &op.parameters {
 | 
			
		||||
        if prev {
 | 
			
		||||
            out.push_str("///\n");
 | 
			
		||||
        }
 | 
			
		||||
        for param in params {
 | 
			
		||||
            let param = param.deref(spec)?;
 | 
			
		||||
            match ¶m._in {
 | 
			
		||||
                ParameterIn::Path { param: _ } | ParameterIn::FormData { param: _ } => {
 | 
			
		||||
                    write!(&mut out, "/// - `{}`", param.name)?;
 | 
			
		||||
                    if let Some(description) = ¶m.description {
 | 
			
		||||
                        write!(&mut out, ": {}", description)?;
 | 
			
		||||
                    }
 | 
			
		||||
                    writeln!(&mut out)?;
 | 
			
		||||
                }
 | 
			
		||||
                ParameterIn::Body { schema } => {
 | 
			
		||||
                    write!(&mut out, "/// - `{}`", param.name)?;
 | 
			
		||||
                    let ty = schema_ref_type_name(spec, &schema)?;
 | 
			
		||||
                    if let Some(description) = ¶m.description {
 | 
			
		||||
                        write!(&mut out, ": {}\n\n///   See [`{}`]", description, ty)?;
 | 
			
		||||
                    } else {
 | 
			
		||||
                        write!(&mut out, ": See [`{}`]", ty)?;
 | 
			
		||||
                    }
 | 
			
		||||
                    writeln!(&mut out)?;
 | 
			
		||||
                }
 | 
			
		||||
                _ => (),
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if out.ends_with("/// \n") {
 | 
			
		||||
        out.truncate(out.len() - 5);
 | 
			
		||||
    }
 | 
			
		||||
    Ok(out)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn fn_signature_from_op(spec: &OpenApiV2, op: &Operation) -> eyre::Result<String> {
 | 
			
		||||
    let name = op
 | 
			
		||||
        .operation_id
 | 
			
		||||
        .as_deref()
 | 
			
		||||
        .ok_or_else(|| eyre::eyre!("operation did not have id"))?
 | 
			
		||||
        .to_snake_case()
 | 
			
		||||
        .replace("o_auth2", "oauth2");
 | 
			
		||||
    let args = fn_args_from_op(spec, op)?;
 | 
			
		||||
    let ty = fn_return_from_op(spec, op)?;
 | 
			
		||||
    Ok(format!(
 | 
			
		||||
        "pub async fn {name}({args}) -> Result<{ty}, ForgejoError>"
 | 
			
		||||
    ))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn fn_args_from_op(spec: &OpenApiV2, op: &Operation) -> eyre::Result<String> {
 | 
			
		||||
    let mut args = "&self".to_string();
 | 
			
		||||
    let mut has_query = false;
 | 
			
		||||
    // let mut has_headers = false;
 | 
			
		||||
    if let Some(params) = &op.parameters {
 | 
			
		||||
        for param in params {
 | 
			
		||||
            let full_param = param.deref(spec)?;
 | 
			
		||||
            match &full_param._in {
 | 
			
		||||
                ParameterIn::Path { param } => {
 | 
			
		||||
                    let type_name = param_type(¶m, false)?;
 | 
			
		||||
                    args.push_str(", ");
 | 
			
		||||
                    args.push_str(&crate::sanitize_ident(&full_param.name));
 | 
			
		||||
                    args.push_str(": ");
 | 
			
		||||
                    args.push_str(&type_name);
 | 
			
		||||
                }
 | 
			
		||||
                ParameterIn::Query { param: _ } => has_query = true,
 | 
			
		||||
                ParameterIn::Header { param: _ } => (), // has_headers = true,
 | 
			
		||||
                ParameterIn::Body { schema } => {
 | 
			
		||||
                    let ty = crate::schema_ref_type_name(spec, schema)?;
 | 
			
		||||
                    args.push_str(", ");
 | 
			
		||||
                    args.push_str(&crate::sanitize_ident(&full_param.name));
 | 
			
		||||
                    args.push_str(": ");
 | 
			
		||||
                    args.push_str(&ty);
 | 
			
		||||
                }
 | 
			
		||||
                ParameterIn::FormData { param: _ } => {
 | 
			
		||||
                    args.push_str(", ");
 | 
			
		||||
                    args.push_str(&crate::sanitize_ident(&full_param.name));
 | 
			
		||||
                    args.push_str(": Vec<u8>");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if has_query {
 | 
			
		||||
        let query_ty = crate::structs::query_struct_name(op)?;
 | 
			
		||||
        args.push_str(", query: ");
 | 
			
		||||
        args.push_str(&query_ty);
 | 
			
		||||
    }
 | 
			
		||||
    Ok(args)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn param_type(param: &NonBodyParameter, owned: bool) -> eyre::Result<String> {
 | 
			
		||||
    param_type_inner(
 | 
			
		||||
        ¶m._type,
 | 
			
		||||
        param.format.as_deref(),
 | 
			
		||||
        param.items.as_ref(),
 | 
			
		||||
        owned,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn param_type_inner(
 | 
			
		||||
    ty: &ParameterType,
 | 
			
		||||
    format: Option<&str>,
 | 
			
		||||
    items: Option<&Items>,
 | 
			
		||||
    owned: bool,
 | 
			
		||||
) -> eyre::Result<String> {
 | 
			
		||||
    let ty_name = match ty {
 | 
			
		||||
        ParameterType::String => match format.as_deref() {
 | 
			
		||||
            Some("date") => "time::Date",
 | 
			
		||||
            Some("date-time") => "time::OffsetDateTime",
 | 
			
		||||
            _ => {
 | 
			
		||||
                if owned {
 | 
			
		||||
                    "String"
 | 
			
		||||
                } else {
 | 
			
		||||
                    "&str"
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        .into(),
 | 
			
		||||
        ParameterType::Number => match format.as_deref() {
 | 
			
		||||
            Some("float") => "f32",
 | 
			
		||||
            Some("double") => "f64",
 | 
			
		||||
            _ => "f64",
 | 
			
		||||
        }
 | 
			
		||||
        .into(),
 | 
			
		||||
        ParameterType::Integer => match format.as_deref() {
 | 
			
		||||
            Some("int32") => "u32",
 | 
			
		||||
            Some("int64") => "u64",
 | 
			
		||||
            _ => "u32",
 | 
			
		||||
        }
 | 
			
		||||
        .into(),
 | 
			
		||||
        ParameterType::Boolean => "bool".into(),
 | 
			
		||||
        ParameterType::Array => {
 | 
			
		||||
            let item = items
 | 
			
		||||
                .as_ref()
 | 
			
		||||
                .ok_or_else(|| eyre::eyre!("array must have item type defined"))?;
 | 
			
		||||
            let item_ty_name = param_type_inner(
 | 
			
		||||
                &item._type,
 | 
			
		||||
                item.format.as_deref(),
 | 
			
		||||
                item.items.as_deref(),
 | 
			
		||||
                owned,
 | 
			
		||||
            )?;
 | 
			
		||||
            if owned {
 | 
			
		||||
                format!("Vec<{item_ty_name}>")
 | 
			
		||||
            } else {
 | 
			
		||||
                format!("&[{item_ty_name}]")
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        ParameterType::File => {
 | 
			
		||||
            if owned {
 | 
			
		||||
                format!("Vec<u8>")
 | 
			
		||||
            } else {
 | 
			
		||||
                format!("&[u8]")
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
    Ok(ty_name)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn fn_return_from_op(spec: &OpenApiV2, op: &Operation) -> eyre::Result<ResponseType> {
 | 
			
		||||
    let responses = op
 | 
			
		||||
        .responses
 | 
			
		||||
        .http_codes
 | 
			
		||||
        .iter()
 | 
			
		||||
        .filter(|(k, _)| k.starts_with("2"))
 | 
			
		||||
        .map(|(_, v)| response_ref_type_name(spec, v, op))
 | 
			
		||||
        .collect::<Result<Vec<_>, _>>()?;
 | 
			
		||||
    let mut iter = responses.into_iter();
 | 
			
		||||
    let mut response = iter
 | 
			
		||||
        .next()
 | 
			
		||||
        .ok_or_else(|| eyre::eyre!("must have at least one response type"))?;
 | 
			
		||||
    for next in iter {
 | 
			
		||||
        response = response.merge(next)?;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(response)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn response_ref_type_name(
 | 
			
		||||
    spec: &OpenApiV2,
 | 
			
		||||
    response_ref: &MaybeRef<Response>,
 | 
			
		||||
    op: &Operation,
 | 
			
		||||
) -> eyre::Result<ResponseType> {
 | 
			
		||||
    let response = response_ref.deref(spec)?;
 | 
			
		||||
    let mut ty = ResponseType::default();
 | 
			
		||||
    if response.headers.is_some() {
 | 
			
		||||
        let parent_name = match &response_ref {
 | 
			
		||||
            MaybeRef::Ref { _ref } => _ref
 | 
			
		||||
                .rsplit_once("/")
 | 
			
		||||
                .ok_or_else(|| eyre::eyre!("invalid ref"))?
 | 
			
		||||
                .1
 | 
			
		||||
                .to_string(),
 | 
			
		||||
            MaybeRef::Value { value: _ } => {
 | 
			
		||||
                eyre::bail!("could not find parent name for header type")
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        ty.headers = Some(format!("{}Headers", parent_name));
 | 
			
		||||
    }
 | 
			
		||||
    let produces = op
 | 
			
		||||
        .produces
 | 
			
		||||
        .as_deref()
 | 
			
		||||
        .or_else(|| spec.produces.as_deref())
 | 
			
		||||
        .unwrap_or_default();
 | 
			
		||||
    // can't use .contains() because Strings
 | 
			
		||||
    let produces_json = produces.iter().any(|i| matches!(&**i, "application/json"));
 | 
			
		||||
    let produces_text = produces.iter().any(|i| i.starts_with("text/"));
 | 
			
		||||
    let produces_other = produces
 | 
			
		||||
        .iter()
 | 
			
		||||
        .any(|i| !matches!(&**i, "application/json") && !i.starts_with("text/"));
 | 
			
		||||
    match (produces_json, produces_text, produces_other) {
 | 
			
		||||
        (true, false, false) => {
 | 
			
		||||
            if let Some(schema) = &response.schema {
 | 
			
		||||
                ty.kind = Some(ResponseKind::Json);
 | 
			
		||||
                let mut body = crate::schema_ref_type_name(spec, schema)?;
 | 
			
		||||
                if let MaybeRef::Value { value } = schema {
 | 
			
		||||
                    let op_name = op.operation_id.as_deref().ok_or_else(|| eyre::eyre!("no operation id"))?.to_pascal_case();
 | 
			
		||||
                    crate::schema_subtype_name(spec, &op_name, "Response", value, &mut body)?;
 | 
			
		||||
                }
 | 
			
		||||
                ty.body = Some(body);
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        (false, _, true) => {
 | 
			
		||||
            ty.kind = Some(ResponseKind::Bytes);
 | 
			
		||||
            ty.body = Some("Vec<u8>".into());
 | 
			
		||||
        }
 | 
			
		||||
        (false, true, false) => {
 | 
			
		||||
            ty.kind = Some(ResponseKind::Text);
 | 
			
		||||
            ty.body = Some("String".into());
 | 
			
		||||
        }
 | 
			
		||||
        (false, false, false) => {
 | 
			
		||||
            ty.kind = None;
 | 
			
		||||
            ty.body = None;
 | 
			
		||||
        }
 | 
			
		||||
        _ => eyre::bail!("produces value unsupported. json: {produces_json}, text: {produces_text}, other: {produces_other}"),
 | 
			
		||||
    };
 | 
			
		||||
    Ok(ty)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn create_method_body(
 | 
			
		||||
    spec: &OpenApiV2,
 | 
			
		||||
    method: &str,
 | 
			
		||||
    path: &str,
 | 
			
		||||
    op: &Operation,
 | 
			
		||||
) -> eyre::Result<String> {
 | 
			
		||||
    let request = create_method_request(spec, method, path, op)?;
 | 
			
		||||
    let response = create_method_response(spec, op)?;
 | 
			
		||||
    Ok(format!("{request}\n    {response}"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn create_method_request(
 | 
			
		||||
    spec: &OpenApiV2,
 | 
			
		||||
    method: &str,
 | 
			
		||||
    path: &str,
 | 
			
		||||
    op: &Operation,
 | 
			
		||||
) -> eyre::Result<String> {
 | 
			
		||||
    let mut has_query = false;
 | 
			
		||||
    // let mut has_headers = false;
 | 
			
		||||
    let mut body_method = String::new();
 | 
			
		||||
    if let Some(params) = &op.parameters {
 | 
			
		||||
        for param in params {
 | 
			
		||||
            let param = param.deref(spec)?;
 | 
			
		||||
            let name = crate::sanitize_ident(¶m.name);
 | 
			
		||||
            match ¶m._in {
 | 
			
		||||
                ParameterIn::Path { param: _ } => (/* do nothing */),
 | 
			
		||||
                ParameterIn::Query { param: _ } => has_query = true,
 | 
			
		||||
                ParameterIn::Header { param: _ } => (), // _has_headers = true,
 | 
			
		||||
                ParameterIn::Body { schema: _ } => {
 | 
			
		||||
                    if !body_method.is_empty() {
 | 
			
		||||
                        eyre::bail!("cannot have more than one body parameter");
 | 
			
		||||
                    }
 | 
			
		||||
                    if param_is_string(spec, param)? {
 | 
			
		||||
                        body_method = format!(".body({name})");
 | 
			
		||||
                    } else {
 | 
			
		||||
                        body_method = format!(".json(&{name})");
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                ParameterIn::FormData { param: _ } => {
 | 
			
		||||
                    if !body_method.is_empty() {
 | 
			
		||||
                        eyre::bail!("cannot have more than one body parameter");
 | 
			
		||||
                    }
 | 
			
		||||
                    body_method = format!(".multipart(reqwest::multipart::Form::new().part(\"attachment\", reqwest::multipart::Part::bytes({name}).file_name(\"file\").mime_str(\"*/*\").unwrap()))");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    let mut fmt_str = sanitize_path_arg(path)?;
 | 
			
		||||
    if has_query {
 | 
			
		||||
        fmt_str.push_str("?{query}");
 | 
			
		||||
    }
 | 
			
		||||
    let path_arg = if fmt_str.contains("{") {
 | 
			
		||||
        format!("&format!(\"{fmt_str}\")")
 | 
			
		||||
    } else {
 | 
			
		||||
        format!("\"{fmt_str}\"")
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let out = format!("let request = self.{method}({path_arg}){body_method}.build()?;");
 | 
			
		||||
    Ok(out)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn param_is_string(spec: &OpenApiV2, param: &Parameter) -> eyre::Result<bool> {
 | 
			
		||||
    match ¶m._in {
 | 
			
		||||
        ParameterIn::Body { schema } => crate::schema_is_string(spec, schema),
 | 
			
		||||
        ParameterIn::Path { param }
 | 
			
		||||
        | ParameterIn::Query { param }
 | 
			
		||||
        | ParameterIn::Header { param }
 | 
			
		||||
        | ParameterIn::FormData { param } => {
 | 
			
		||||
            let is_str = match param._type {
 | 
			
		||||
                ParameterType::String => true,
 | 
			
		||||
                _ => false,
 | 
			
		||||
            };
 | 
			
		||||
            Ok(is_str)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn sanitize_path_arg(mut path: &str) -> eyre::Result<String> {
 | 
			
		||||
    let mut out = String::new();
 | 
			
		||||
    loop {
 | 
			
		||||
        let (head, tail) = match path.split_once("{") {
 | 
			
		||||
            Some(i) => i,
 | 
			
		||||
            None => {
 | 
			
		||||
                out.push_str(path);
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        path = tail;
 | 
			
		||||
        out.push_str(head);
 | 
			
		||||
        out.push('{');
 | 
			
		||||
        let (head, tail) = match path.split_once("}") {
 | 
			
		||||
            Some(i) => i,
 | 
			
		||||
            None => {
 | 
			
		||||
                eyre::bail!("unmatched bracket");
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        path = tail;
 | 
			
		||||
        out.push_str(&head.to_snake_case());
 | 
			
		||||
        out.push('}');
 | 
			
		||||
    }
 | 
			
		||||
    if out.starts_with("/") {
 | 
			
		||||
        out.remove(0);
 | 
			
		||||
    }
 | 
			
		||||
    Ok(out)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn create_method_response(spec: &OpenApiV2, op: &Operation) -> eyre::Result<String> {
 | 
			
		||||
    let mut has_empty = false;
 | 
			
		||||
    let mut only_empty = true;
 | 
			
		||||
    for (code, res) in &op.responses.http_codes {
 | 
			
		||||
        let response = response_ref_type_name(spec, res, op)?;
 | 
			
		||||
        if !code.starts_with("2") {
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
        if matches!(response.body.as_deref(), Some("()") | None) {
 | 
			
		||||
            has_empty = true;
 | 
			
		||||
        } else {
 | 
			
		||||
            only_empty = false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    let fn_ret = fn_return_from_op(spec, op)?;
 | 
			
		||||
    let optional = has_empty && !only_empty;
 | 
			
		||||
    let mut out = String::new();
 | 
			
		||||
    out.push_str("let response = self.execute(request).await?;\n");
 | 
			
		||||
    out.push_str("match response.status().as_u16() {\n");
 | 
			
		||||
    for (code, res) in &op.responses.http_codes {
 | 
			
		||||
        let res = res.deref(spec)?;
 | 
			
		||||
        if !code.starts_with("2") {
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
        out.push_str(code);
 | 
			
		||||
        out.push_str(" => Ok(");
 | 
			
		||||
        let mut handlers = Vec::new();
 | 
			
		||||
        let header_handler = match &res.headers {
 | 
			
		||||
            Some(_) => {
 | 
			
		||||
                if fn_ret
 | 
			
		||||
                    .headers
 | 
			
		||||
                    .as_ref()
 | 
			
		||||
                    .map(|s| s.starts_with("Option<"))
 | 
			
		||||
                    .unwrap()
 | 
			
		||||
                {
 | 
			
		||||
                    Some("Some(response.headers().try_into()?)")
 | 
			
		||||
                } else {
 | 
			
		||||
                    Some("response.headers().try_into()?")
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            None => {
 | 
			
		||||
                if fn_ret.headers.is_some() {
 | 
			
		||||
                    Some("None")
 | 
			
		||||
                } else {
 | 
			
		||||
                    None
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        handlers.extend(header_handler);
 | 
			
		||||
        let body_handler = match fn_ret.kind {
 | 
			
		||||
            Some(ResponseKind::Text) => {
 | 
			
		||||
                if optional {
 | 
			
		||||
                    Some("Some(response.text().await?)")
 | 
			
		||||
                } else {
 | 
			
		||||
                    Some("response.text().await?")
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            Some(ResponseKind::Json) => {
 | 
			
		||||
                if optional {
 | 
			
		||||
                    Some("Some(response.json().await?)")
 | 
			
		||||
                } else {
 | 
			
		||||
                    Some("response.json().await?")
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            Some(ResponseKind::Bytes) => {
 | 
			
		||||
                if optional {
 | 
			
		||||
                    Some("Some(response.bytes().await?[..].to_vec())")
 | 
			
		||||
                } else {
 | 
			
		||||
                    Some("response.bytes().await?[..].to_vec()")
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            None => {
 | 
			
		||||
                if optional {
 | 
			
		||||
                    Some("None")
 | 
			
		||||
                } else {
 | 
			
		||||
                    None
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        handlers.extend(body_handler);
 | 
			
		||||
        match handlers[..] {
 | 
			
		||||
            [single] => out.push_str(single),
 | 
			
		||||
            _ => {
 | 
			
		||||
                out.push('(');
 | 
			
		||||
                for (i, item) in handlers.iter().copied().enumerate() {
 | 
			
		||||
                    out.push_str(item);
 | 
			
		||||
                    if i + 1 < handlers.len() {
 | 
			
		||||
                        out.push_str(", ");
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                out.push(')');
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        out.push_str("),\n");
 | 
			
		||||
    }
 | 
			
		||||
    out.push_str("_ => Err(ForgejoError::UnexpectedStatusCode(response.status()))\n");
 | 
			
		||||
    out.push_str("}\n");
 | 
			
		||||
 | 
			
		||||
    Ok(out)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Default)]
 | 
			
		||||
struct ResponseType {
 | 
			
		||||
    headers: Option<String>,
 | 
			
		||||
    body: Option<String>,
 | 
			
		||||
    kind: Option<ResponseKind>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ResponseType {
 | 
			
		||||
    fn merge(self, other: Self) -> eyre::Result<Self> {
 | 
			
		||||
        let headers = match (self.headers, other.headers) {
 | 
			
		||||
            (Some(a), Some(b)) if a != b => eyre::bail!("incompatible header types in response"),
 | 
			
		||||
            (Some(a), None) => Some(format!("Option<{a}>")),
 | 
			
		||||
            (None, Some(b)) => Some(format!("Option<{b}>")),
 | 
			
		||||
            (a, b) => a.or(b),
 | 
			
		||||
        };
 | 
			
		||||
        let body = match (self.body.as_deref(), other.body.as_deref()) {
 | 
			
		||||
            (Some(a), Some(b)) if a != b => eyre::bail!("incompatible header types in response"),
 | 
			
		||||
            (Some(a), Some("()") | None) => Some(format!("Option<{a}>")),
 | 
			
		||||
            (Some("()") | None, Some(b)) => Some(format!("Option<{b}>")),
 | 
			
		||||
            (_, _) => self.body.or(other.body),
 | 
			
		||||
        };
 | 
			
		||||
        use ResponseKind::*;
 | 
			
		||||
        let kind = match (self.kind, other.kind) {
 | 
			
		||||
            (a, None) => a,
 | 
			
		||||
            (None, b) => b,
 | 
			
		||||
            (Some(Json), Some(Json)) => Some(Json),
 | 
			
		||||
            (Some(Bytes), Some(Bytes)) => Some(Bytes),
 | 
			
		||||
            (Some(Bytes), Some(Text)) => Some(Bytes),
 | 
			
		||||
            (Some(Text), Some(Bytes)) => Some(Bytes),
 | 
			
		||||
            (Some(Text), Some(Text)) => Some(Text),
 | 
			
		||||
            _ => eyre::bail!("incompatible response kinds"),
 | 
			
		||||
        };
 | 
			
		||||
        let new = Self {
 | 
			
		||||
            headers,
 | 
			
		||||
            body,
 | 
			
		||||
            kind,
 | 
			
		||||
        };
 | 
			
		||||
        Ok(new)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Copy)]
 | 
			
		||||
enum ResponseKind {
 | 
			
		||||
    Json,
 | 
			
		||||
    Text,
 | 
			
		||||
    Bytes,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl std::fmt::Display for ResponseType {
 | 
			
		||||
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 | 
			
		||||
        let mut tys = Vec::new();
 | 
			
		||||
        tys.extend(self.headers.as_deref());
 | 
			
		||||
        tys.extend(self.body.as_deref());
 | 
			
		||||
        match tys[..] {
 | 
			
		||||
            [single] => f.write_str(single),
 | 
			
		||||
            _ => {
 | 
			
		||||
                write!(f, "(")?;
 | 
			
		||||
                for (i, item) in tys.iter().copied().enumerate() {
 | 
			
		||||
                    f.write_str(item)?;
 | 
			
		||||
                    if i + 1 < tys.len() {
 | 
			
		||||
                        write!(f, ", ")?;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                write!(f, ")")?;
 | 
			
		||||
                Ok(())
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1447
									
								
								generator/src/openapi.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1447
									
								
								generator/src/openapi.rs
									
										
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										654
									
								
								generator/src/structs.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										654
									
								
								generator/src/structs.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,654 @@
 | 
			
		|||
use crate::openapi::*;
 | 
			
		||||
use eyre::WrapErr;
 | 
			
		||||
use heck::ToPascalCase;
 | 
			
		||||
use std::fmt::Write;
 | 
			
		||||
 | 
			
		||||
pub fn create_structs(spec: &OpenApiV2) -> eyre::Result<String> {
 | 
			
		||||
    let mut s = String::new();
 | 
			
		||||
    s.push_str("use crate::StructureError;");
 | 
			
		||||
    s.push_str("use std::collections::BTreeMap;");
 | 
			
		||||
    if let Some(definitions) = &spec.definitions {
 | 
			
		||||
        for (name, schema) in definitions {
 | 
			
		||||
            let strukt = create_struct_for_definition(&spec, name, schema)?;
 | 
			
		||||
            s.push_str(&strukt);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(responses) = &spec.responses {
 | 
			
		||||
        for (name, response) in responses {
 | 
			
		||||
            if let Some(headers) = &response.headers {
 | 
			
		||||
                let strukt = create_header_struct(name, headers)?;
 | 
			
		||||
                s.push_str(&strukt);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let tys = create_response_struct(spec, name, response)?;
 | 
			
		||||
            s.push_str(&tys);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    for (_, item) in &spec.paths {
 | 
			
		||||
        let strukt = create_query_structs_for_path(spec, item)?;
 | 
			
		||||
        s.push_str(&strukt);
 | 
			
		||||
 | 
			
		||||
        let strukt = create_response_structs(spec, item)?;
 | 
			
		||||
        s.push_str(&strukt);
 | 
			
		||||
    }
 | 
			
		||||
    Ok(s)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn create_struct_for_definition(
 | 
			
		||||
    spec: &OpenApiV2,
 | 
			
		||||
    name: &str,
 | 
			
		||||
    schema: &Schema,
 | 
			
		||||
) -> eyre::Result<String> {
 | 
			
		||||
    if matches!(schema._type, Some(SchemaType::One(Primitive::Array))) {
 | 
			
		||||
        return Ok(String::new());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if schema._type == Some(SchemaType::One(Primitive::String)) {
 | 
			
		||||
        if let Some(_enum) = &schema._enum {
 | 
			
		||||
            return create_enum(name, schema.description.as_deref(), _enum, false);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let mut subtypes = Vec::new();
 | 
			
		||||
 | 
			
		||||
    let docs = create_struct_docs(schema)?;
 | 
			
		||||
    let mut fields = String::new();
 | 
			
		||||
    let required = schema.required.as_deref().unwrap_or_default();
 | 
			
		||||
    if let Some(properties) = &schema.properties {
 | 
			
		||||
        for (prop_name, prop_schema) in properties {
 | 
			
		||||
            let prop_ty = crate::schema_ref_type_name(spec, prop_schema)?;
 | 
			
		||||
            let field_name = crate::sanitize_ident(prop_name);
 | 
			
		||||
            let mut field_ty = prop_ty.clone();
 | 
			
		||||
            if let MaybeRef::Value { value } = &prop_schema {
 | 
			
		||||
                crate::schema_subtype_name(spec, name, prop_name, value, &mut field_ty)?;
 | 
			
		||||
                crate::schema_subtypes(spec, name, prop_name, value, &mut subtypes)?;
 | 
			
		||||
            }
 | 
			
		||||
            if field_name.ends_with("url") && field_name != "ssh_url" && field_ty == "String" {
 | 
			
		||||
                field_ty = "url::Url".into()
 | 
			
		||||
            }
 | 
			
		||||
            if field_ty == name {
 | 
			
		||||
                field_ty = format!("Box<{field_ty}>")
 | 
			
		||||
            }
 | 
			
		||||
            if !required.contains(prop_name) {
 | 
			
		||||
                field_ty = format!("Option<{field_ty}>")
 | 
			
		||||
            }
 | 
			
		||||
            if field_ty == "Option<url::Url>" {
 | 
			
		||||
                fields.push_str("#[serde(deserialize_with = \"crate::none_if_blank_url\")]\n");
 | 
			
		||||
            }
 | 
			
		||||
            if field_ty == "time::OffsetDateTime" {
 | 
			
		||||
                fields.push_str("#[serde(with = \"time::serde::rfc3339\")]\n");
 | 
			
		||||
            }
 | 
			
		||||
            if field_ty == "Option<time::OffsetDateTime>" {
 | 
			
		||||
                fields.push_str("#[serde(with = \"time::serde::rfc3339::option\")]\n");
 | 
			
		||||
            }
 | 
			
		||||
            if let MaybeRef::Value { value } = &prop_schema {
 | 
			
		||||
                if let Some(desc) = &value.description {
 | 
			
		||||
                    for line in desc.lines() {
 | 
			
		||||
                        fields.push_str("/// ");
 | 
			
		||||
                        fields.push_str(line);
 | 
			
		||||
                        fields.push_str("\n/// \n");
 | 
			
		||||
                    }
 | 
			
		||||
                    if fields.ends_with("/// \n") {
 | 
			
		||||
                        fields.truncate(fields.len() - 5);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            if &field_name != prop_name {
 | 
			
		||||
                fields.push_str("#[serde(rename = \"");
 | 
			
		||||
                fields.push_str(prop_name);
 | 
			
		||||
                fields.push_str("\")]\n");
 | 
			
		||||
            }
 | 
			
		||||
            fields.push_str("pub ");
 | 
			
		||||
            fields.push_str(&field_name);
 | 
			
		||||
            fields.push_str(": ");
 | 
			
		||||
            fields.push_str(&field_ty);
 | 
			
		||||
            fields.push_str(",\n");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if let Some(additonal_schema) = &schema.additional_properties {
 | 
			
		||||
        let prop_ty = crate::schema_ref_type_name(spec, additonal_schema)?;
 | 
			
		||||
        fields.push_str("#[serde(flatten)]\n");
 | 
			
		||||
        fields.push_str("pub additional: BTreeMap<String, ");
 | 
			
		||||
        fields.push_str(&prop_ty);
 | 
			
		||||
        fields.push_str(">,\n");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let mut out = format!("{docs}#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]\npub struct {name} {{\n{fields}}}\n\n");
 | 
			
		||||
    for subtype in subtypes {
 | 
			
		||||
        out.push_str(&subtype);
 | 
			
		||||
    }
 | 
			
		||||
    Ok(out)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn create_enum(
 | 
			
		||||
    name: &str,
 | 
			
		||||
    desc: Option<&str>,
 | 
			
		||||
    _enum: &[serde_json::Value],
 | 
			
		||||
    imp_as_str: bool,
 | 
			
		||||
) -> eyre::Result<String> {
 | 
			
		||||
    let mut variants = String::new();
 | 
			
		||||
    let mut imp = String::new();
 | 
			
		||||
    imp.push_str("match self {");
 | 
			
		||||
    let docs = create_struct_docs_str(desc)?;
 | 
			
		||||
    for variant in _enum {
 | 
			
		||||
        match variant {
 | 
			
		||||
            serde_json::Value::String(s) => {
 | 
			
		||||
                let variant_name = s.to_pascal_case();
 | 
			
		||||
                variants.push_str("#[serde(rename = \"");
 | 
			
		||||
                variants.push_str(s);
 | 
			
		||||
                variants.push_str("\")]");
 | 
			
		||||
                variants.push_str(&variant_name);
 | 
			
		||||
                variants.push_str(",\n");
 | 
			
		||||
 | 
			
		||||
                writeln!(&mut imp, "{name}::{variant_name} => \"{s}\",")?;
 | 
			
		||||
            }
 | 
			
		||||
            x => eyre::bail!("cannot create enum variant. expected string, got {x:?}"),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    imp.push_str("}");
 | 
			
		||||
 | 
			
		||||
    let strukt = format!(
 | 
			
		||||
        "
 | 
			
		||||
{docs}
 | 
			
		||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
 | 
			
		||||
pub enum {name} {{
 | 
			
		||||
    {variants}
 | 
			
		||||
}}"
 | 
			
		||||
    );
 | 
			
		||||
    let out = if imp_as_str {
 | 
			
		||||
        let imp = format!(
 | 
			
		||||
            "\n\nimpl {name} {{
 | 
			
		||||
                fn as_str(&self) -> &'static str {{
 | 
			
		||||
                    {imp}
 | 
			
		||||
                }}
 | 
			
		||||
            }}"
 | 
			
		||||
        );
 | 
			
		||||
        format!("{strukt} {imp}")
 | 
			
		||||
    } else {
 | 
			
		||||
        strukt
 | 
			
		||||
    };
 | 
			
		||||
    Ok(out)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn create_struct_docs(schema: &Schema) -> eyre::Result<String> {
 | 
			
		||||
    create_struct_docs_str(schema.description.as_deref())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn create_struct_docs_str(description: Option<&str>) -> eyre::Result<String> {
 | 
			
		||||
    let doc = match description {
 | 
			
		||||
        Some(desc) => {
 | 
			
		||||
            let mut out = String::new();
 | 
			
		||||
            for line in desc.lines() {
 | 
			
		||||
                out.push_str("/// ");
 | 
			
		||||
                out.push_str(line);
 | 
			
		||||
                out.push_str("\n/// \n");
 | 
			
		||||
            }
 | 
			
		||||
            if out.ends_with("/// \n") {
 | 
			
		||||
                out.truncate(out.len() - 5);
 | 
			
		||||
            }
 | 
			
		||||
            out
 | 
			
		||||
        }
 | 
			
		||||
        None => String::new(),
 | 
			
		||||
    };
 | 
			
		||||
    Ok(doc)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn create_query_structs_for_path(spec: &OpenApiV2, item: &PathItem) -> eyre::Result<String> {
 | 
			
		||||
    let mut s = String::new();
 | 
			
		||||
    if let Some(op) = &item.get {
 | 
			
		||||
        s.push_str(&create_query_struct(spec, op).wrap_err("GET")?);
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(op) = &item.put {
 | 
			
		||||
        s.push_str(&create_query_struct(spec, op).wrap_err("PUT")?);
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(op) = &item.post {
 | 
			
		||||
        s.push_str(&create_query_struct(spec, op).wrap_err("POST")?);
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(op) = &item.delete {
 | 
			
		||||
        s.push_str(&create_query_struct(spec, op).wrap_err("DELETE")?);
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(op) = &item.options {
 | 
			
		||||
        s.push_str(&create_query_struct(spec, op).wrap_err("OPTIONS")?);
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(op) = &item.head {
 | 
			
		||||
        s.push_str(&create_query_struct(spec, op).wrap_err("HEAD")?);
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(op) = &item.patch {
 | 
			
		||||
        s.push_str(&create_query_struct(spec, op).wrap_err("PATCH")?);
 | 
			
		||||
    }
 | 
			
		||||
    Ok(s)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn query_struct_name(op: &Operation) -> eyre::Result<String> {
 | 
			
		||||
    let mut ty = op
 | 
			
		||||
        .operation_id
 | 
			
		||||
        .as_deref()
 | 
			
		||||
        .ok_or_else(|| eyre::eyre!("operation did not have id"))?
 | 
			
		||||
        .to_pascal_case()
 | 
			
		||||
        .replace("o_auth2", "oauth2");
 | 
			
		||||
    ty.push_str("Query");
 | 
			
		||||
    Ok(ty)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn create_query_struct(spec: &OpenApiV2, op: &Operation) -> eyre::Result<String> {
 | 
			
		||||
    let params = match &op.parameters {
 | 
			
		||||
        Some(params) => params,
 | 
			
		||||
        None => return Ok(String::new()),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let op_name = query_struct_name(op)?;
 | 
			
		||||
    let mut enums = Vec::new();
 | 
			
		||||
    let mut fields = String::new();
 | 
			
		||||
    let mut imp = String::new();
 | 
			
		||||
    for param in params {
 | 
			
		||||
        let param = param.deref(spec)?;
 | 
			
		||||
        if let ParameterIn::Query { param: query_param } = ¶m._in {
 | 
			
		||||
            let field_name = crate::sanitize_ident(¶m.name);
 | 
			
		||||
            let ty = match &query_param {
 | 
			
		||||
                NonBodyParameter {
 | 
			
		||||
                    _type: ParameterType::String,
 | 
			
		||||
                    _enum: Some(_enum),
 | 
			
		||||
                    ..
 | 
			
		||||
                } => {
 | 
			
		||||
                    let name = format!("{op_name}{}", param.name.to_pascal_case());
 | 
			
		||||
                    let enum_def = create_enum(&name, None, _enum, true)?;
 | 
			
		||||
                    enums.push(enum_def);
 | 
			
		||||
                    name
 | 
			
		||||
                }
 | 
			
		||||
                NonBodyParameter {
 | 
			
		||||
                    _type: ParameterType::Array,
 | 
			
		||||
                    items:
 | 
			
		||||
                        Some(Items {
 | 
			
		||||
                            _type: ParameterType::String,
 | 
			
		||||
                            _enum: Some(_enum),
 | 
			
		||||
                            ..
 | 
			
		||||
                        }),
 | 
			
		||||
                    ..
 | 
			
		||||
                } => {
 | 
			
		||||
                    let name = format!("{op_name}{}", param.name.to_pascal_case());
 | 
			
		||||
                    let enum_def = create_enum(&name, None, _enum, true)?;
 | 
			
		||||
                    enums.push(enum_def);
 | 
			
		||||
                    format!("Vec<{name}>")
 | 
			
		||||
                }
 | 
			
		||||
                _ => crate::methods::param_type(query_param, true)?,
 | 
			
		||||
            };
 | 
			
		||||
            if let Some(desc) = ¶m.description {
 | 
			
		||||
                for line in desc.lines() {
 | 
			
		||||
                    fields.push_str("/// ");
 | 
			
		||||
                    fields.push_str(line);
 | 
			
		||||
                    fields.push_str("\n/// \n");
 | 
			
		||||
                }
 | 
			
		||||
                if fields.ends_with("/// \n") {
 | 
			
		||||
                    fields.truncate(fields.len() - 5);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            fields.push_str("pub ");
 | 
			
		||||
            fields.push_str(&field_name);
 | 
			
		||||
            fields.push_str(": ");
 | 
			
		||||
            if query_param.required {
 | 
			
		||||
                fields.push_str(&ty);
 | 
			
		||||
            } else {
 | 
			
		||||
                fields.push_str("Option<");
 | 
			
		||||
                fields.push_str(&ty);
 | 
			
		||||
                fields.push_str(">");
 | 
			
		||||
            }
 | 
			
		||||
            fields.push_str(",\n");
 | 
			
		||||
 | 
			
		||||
            let mut handler = String::new();
 | 
			
		||||
            if query_param.required {
 | 
			
		||||
                writeln!(&mut handler, "let {field_name} = &self.{field_name};")?;
 | 
			
		||||
            } else {
 | 
			
		||||
                writeln!(
 | 
			
		||||
                    &mut handler,
 | 
			
		||||
                    "if let Some({field_name}) = &self.{field_name} {{"
 | 
			
		||||
                )?;
 | 
			
		||||
            }
 | 
			
		||||
            match &query_param._type {
 | 
			
		||||
                ParameterType::String => {
 | 
			
		||||
                    if let Some(_enum) = &query_param._enum {
 | 
			
		||||
                        writeln!(
 | 
			
		||||
                            &mut handler,
 | 
			
		||||
                            "write!(f, \"{}={{}}&\", {}.as_str())?;",
 | 
			
		||||
                            param.name, field_name,
 | 
			
		||||
                        )?;
 | 
			
		||||
                    } else {
 | 
			
		||||
                        match query_param.format.as_deref() {
 | 
			
		||||
                            Some("date-time" | "date") => {
 | 
			
		||||
                                writeln!(
 | 
			
		||||
                                    &mut handler,
 | 
			
		||||
                                    "write!(f, \"{}={{field_name}}&\", field_name = {field_name}.format(&time::format_description::well_known::Rfc3339).unwrap())?;",
 | 
			
		||||
                                    param.name)?;
 | 
			
		||||
                            }
 | 
			
		||||
                            _ => {
 | 
			
		||||
                                writeln!(
 | 
			
		||||
                                    &mut handler,
 | 
			
		||||
                                    "write!(f, \"{}={{{}}}&\")?;",
 | 
			
		||||
                                    param.name,
 | 
			
		||||
                                    field_name.strip_prefix("r#").unwrap_or(&field_name)
 | 
			
		||||
                                )?;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                ParameterType::Number | ParameterType::Integer | ParameterType::Boolean => {
 | 
			
		||||
                    writeln!(
 | 
			
		||||
                        &mut handler,
 | 
			
		||||
                        "write!(f, \"{}={{{}}}&\")?;",
 | 
			
		||||
                        param.name,
 | 
			
		||||
                        field_name.strip_prefix("r#").unwrap_or(&field_name)
 | 
			
		||||
                    )?;
 | 
			
		||||
                }
 | 
			
		||||
                ParameterType::Array => {
 | 
			
		||||
                    let format = query_param
 | 
			
		||||
                        .collection_format
 | 
			
		||||
                        .unwrap_or(CollectionFormat::Csv);
 | 
			
		||||
                    let item = query_param
 | 
			
		||||
                        .items
 | 
			
		||||
                        .as_ref()
 | 
			
		||||
                        .ok_or_else(|| eyre::eyre!("array must have item type defined"))?;
 | 
			
		||||
                    let item_pusher = match item._type {
 | 
			
		||||
                        ParameterType::String => {
 | 
			
		||||
                            if let Some(_enum) = &item._enum {
 | 
			
		||||
                                "write!(f, \"{}\", item.as_str())?;"
 | 
			
		||||
                            } else {
 | 
			
		||||
                                match query_param.format.as_deref() {
 | 
			
		||||
                                    Some("date-time" | "date") => {
 | 
			
		||||
                                        "write!(f, \"{{date}}\", item.format(&time::format_description::well_known::Rfc3339).unwrap())?;"
 | 
			
		||||
                                    },
 | 
			
		||||
                                    _ => {
 | 
			
		||||
                                        "write!(f, \"{item}\")?;"
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        ParameterType::Number | ParameterType::Integer | ParameterType::Boolean => {
 | 
			
		||||
                            "write!(f, \"{item}\")?;"
 | 
			
		||||
                        }
 | 
			
		||||
                        ParameterType::Array => {
 | 
			
		||||
                            eyre::bail!("nested arrays not supported in query");
 | 
			
		||||
                        }
 | 
			
		||||
                        ParameterType::File => eyre::bail!("cannot send file in query"),
 | 
			
		||||
                    };
 | 
			
		||||
                    match format {
 | 
			
		||||
                        CollectionFormat::Csv => {
 | 
			
		||||
                            handler.push_str(&simple_query_array(
 | 
			
		||||
                                param,
 | 
			
		||||
                                item_pusher,
 | 
			
		||||
                                &field_name,
 | 
			
		||||
                                ",",
 | 
			
		||||
                            )?);
 | 
			
		||||
                        }
 | 
			
		||||
                        CollectionFormat::Ssv => {
 | 
			
		||||
                            handler.push_str(&simple_query_array(
 | 
			
		||||
                                param,
 | 
			
		||||
                                item_pusher,
 | 
			
		||||
                                &field_name,
 | 
			
		||||
                                " ",
 | 
			
		||||
                            )?);
 | 
			
		||||
                        }
 | 
			
		||||
                        CollectionFormat::Tsv => {
 | 
			
		||||
                            handler.push_str(&simple_query_array(
 | 
			
		||||
                                param,
 | 
			
		||||
                                item_pusher,
 | 
			
		||||
                                &field_name,
 | 
			
		||||
                                "\\t",
 | 
			
		||||
                            )?);
 | 
			
		||||
                        }
 | 
			
		||||
                        CollectionFormat::Pipes => {
 | 
			
		||||
                            handler.push_str(&simple_query_array(
 | 
			
		||||
                                param,
 | 
			
		||||
                                item_pusher,
 | 
			
		||||
                                &field_name,
 | 
			
		||||
                                "|",
 | 
			
		||||
                            )?);
 | 
			
		||||
                        }
 | 
			
		||||
                        CollectionFormat::Multi => {
 | 
			
		||||
                            writeln!(&mut handler)?;
 | 
			
		||||
                            writeln!(&mut handler, "if !{field_name}.is_empty() {{")?;
 | 
			
		||||
                            writeln!(&mut handler, "for item in {field_name} {{")?;
 | 
			
		||||
                            writeln!(&mut handler, "write!(f, \"{}=\")?;", param.name)?;
 | 
			
		||||
                            handler.push_str(item_pusher);
 | 
			
		||||
                            handler.push('\n');
 | 
			
		||||
                            writeln!(&mut handler, "write!(f, \"&\")?;")?;
 | 
			
		||||
                            writeln!(&mut handler, "}}")?;
 | 
			
		||||
                            writeln!(&mut handler, "}}")?;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                ParameterType::File => eyre::bail!("cannot send file in query"),
 | 
			
		||||
            }
 | 
			
		||||
            if !query_param.required {
 | 
			
		||||
                writeln!(&mut handler, "}}")?;
 | 
			
		||||
            }
 | 
			
		||||
            imp.push_str(&handler);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let result = if fields.is_empty() {
 | 
			
		||||
        String::new()
 | 
			
		||||
    } else {
 | 
			
		||||
        let mut out = format!(
 | 
			
		||||
            "
 | 
			
		||||
pub struct {op_name} {{
 | 
			
		||||
    {fields}
 | 
			
		||||
}}
 | 
			
		||||
 | 
			
		||||
impl std::fmt::Display for {op_name} {{
 | 
			
		||||
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{
 | 
			
		||||
        {imp}
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }}
 | 
			
		||||
}}
 | 
			
		||||
"
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        for _enum in enums {
 | 
			
		||||
            out.push_str(&_enum);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        out
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    Ok(result)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn simple_query_array(
 | 
			
		||||
    param: &Parameter,
 | 
			
		||||
    item_pusher: &str,
 | 
			
		||||
    name: &str,
 | 
			
		||||
    sep: &str,
 | 
			
		||||
) -> eyre::Result<String> {
 | 
			
		||||
    let mut out = String::new();
 | 
			
		||||
 | 
			
		||||
    writeln!(
 | 
			
		||||
        &mut out,
 | 
			
		||||
        "
 | 
			
		||||
if !{name}.is_empty() {{
 | 
			
		||||
    write!(f, \"{}=\")?;
 | 
			
		||||
    for (item, i) in {name}.iter().enumerate() {{
 | 
			
		||||
        {item_pusher}
 | 
			
		||||
        if i < {name}.len() - 1 {{
 | 
			
		||||
            write!(f, \"{sep}\")?;
 | 
			
		||||
        }}
 | 
			
		||||
    }}
 | 
			
		||||
    write!(f, \"&\")?;
 | 
			
		||||
}}",
 | 
			
		||||
        param.name
 | 
			
		||||
    )?;
 | 
			
		||||
 | 
			
		||||
    Ok(out)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn create_header_struct(
 | 
			
		||||
    name: &str,
 | 
			
		||||
    headers: &std::collections::BTreeMap<String, Header>,
 | 
			
		||||
) -> eyre::Result<String> {
 | 
			
		||||
    let ty_name = format!("{name}Headers").to_pascal_case();
 | 
			
		||||
    let mut fields = String::new();
 | 
			
		||||
    let mut imp = String::new();
 | 
			
		||||
    let mut imp_ret = String::new();
 | 
			
		||||
    for (header_name, header) in headers {
 | 
			
		||||
        let ty = header_type(header)?;
 | 
			
		||||
        let field_name = crate::sanitize_ident(header_name);
 | 
			
		||||
        fields.push_str("pub ");
 | 
			
		||||
        fields.push_str(&field_name);
 | 
			
		||||
        fields.push_str(": Option<");
 | 
			
		||||
        fields.push_str(&ty);
 | 
			
		||||
        fields.push_str(">,\n");
 | 
			
		||||
 | 
			
		||||
        write!(
 | 
			
		||||
            &mut imp,
 | 
			
		||||
            "
 | 
			
		||||
            let {field_name} = map.get(\"{header_name}\").map(|s| -> Result<_, _> {{
 | 
			
		||||
                let s = s.to_str().map_err(|_| StructureError::HeaderNotAscii)?;
 | 
			
		||||
        "
 | 
			
		||||
        )
 | 
			
		||||
        .unwrap();
 | 
			
		||||
        match &header._type {
 | 
			
		||||
            ParameterType::String => imp.push_str("Ok(s.to_string())"),
 | 
			
		||||
            ParameterType::Number => match header.format.as_deref() {
 | 
			
		||||
                Some("float") => {
 | 
			
		||||
                    imp.push_str("s.parse::<f32>().map_err(|_| StructureError::HeaderParseFailed)")
 | 
			
		||||
                }
 | 
			
		||||
                Some("double") | _ => {
 | 
			
		||||
                    imp.push_str("s.parse::<f64>()).map_err(|_| StructureError::HeaderParseFailed)")
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            ParameterType::Integer => match header.format.as_deref() {
 | 
			
		||||
                Some("int64") => {
 | 
			
		||||
                    imp.push_str("s.parse::<u64>().map_err(|_| StructureError::HeaderParseFailed)")
 | 
			
		||||
                }
 | 
			
		||||
                Some("int32") | _ => {
 | 
			
		||||
                    imp.push_str("s.parse::<u32>().map_err(|_| StructureError::HeaderParseFailed)")
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            ParameterType::Boolean => {
 | 
			
		||||
                imp.push_str("s.parse::<bool>().map_err(|_| StructureError::HeaderParseFailed)")
 | 
			
		||||
            }
 | 
			
		||||
            ParameterType::Array => {
 | 
			
		||||
                let sep = match header.collection_format {
 | 
			
		||||
                    Some(CollectionFormat::Csv) | None => ",",
 | 
			
		||||
                    Some(CollectionFormat::Ssv) => " ",
 | 
			
		||||
                    Some(CollectionFormat::Tsv) => "\\t",
 | 
			
		||||
                    Some(CollectionFormat::Pipes) => "|",
 | 
			
		||||
                    Some(CollectionFormat::Multi) => {
 | 
			
		||||
                        eyre::bail!("multi format not supported in headers")
 | 
			
		||||
                    }
 | 
			
		||||
                };
 | 
			
		||||
                let items = header
 | 
			
		||||
                    .items
 | 
			
		||||
                    .as_ref()
 | 
			
		||||
                    .ok_or_else(|| eyre::eyre!("items property must be set for arrays"))?;
 | 
			
		||||
                if items._type == ParameterType::String {
 | 
			
		||||
                    imp.push_str("Ok(");
 | 
			
		||||
                }
 | 
			
		||||
                imp.push_str("s.split(\"");
 | 
			
		||||
                imp.push_str(sep);
 | 
			
		||||
                imp.push_str("\").map(|s| ");
 | 
			
		||||
                imp.push_str(match items._type {
 | 
			
		||||
                    ParameterType::String => "s.to_string()).collect::<Vec<_>>())",
 | 
			
		||||
                    ParameterType::Number => match items.format.as_deref() {
 | 
			
		||||
                        Some("float") => "s.parse::<f32>()).collect::<Result<Vec<_>, _>>().map_err(|_| StructureError::HeaderParseFailed)",
 | 
			
		||||
                        Some("double") | _ => "s.parse::<f64>()).collect::<Result<Vec<_>, _>>().map_err(|_| StructureError::HeaderParseFailed)",
 | 
			
		||||
                    },
 | 
			
		||||
                    ParameterType::Integer => match items.format.as_deref() {
 | 
			
		||||
                        Some("int64") => "s.parse::<u64>()).collect::<Result<Vec<_>, _>>().map_err(|_| StructureError::HeaderParseFailed)",
 | 
			
		||||
                        Some("int32") | _ => "s.parse::<u32>()).collect::<Result<Vec<_>, _>>().map_err(|_| StructureError::HeaderParseFailed)",
 | 
			
		||||
                    },
 | 
			
		||||
                    ParameterType::Boolean => "s.parse::<bool>()).collect::<Result<Vec<_>, _>>().map_err(|_| StructureError::HeaderParseFailed)",
 | 
			
		||||
                    ParameterType::Array => eyre::bail!("nested arrays not supported in headers"),
 | 
			
		||||
                    ParameterType::File => eyre::bail!("files not supported in headers"),
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
            ParameterType::File => eyre::bail!("files not supported in headers"),
 | 
			
		||||
        }
 | 
			
		||||
        imp.push_str("}).transpose()?;");
 | 
			
		||||
 | 
			
		||||
        imp_ret.push_str(&field_name);
 | 
			
		||||
        imp_ret.push_str(", ");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(format!(
 | 
			
		||||
        "
 | 
			
		||||
pub struct {ty_name} {{
 | 
			
		||||
    {fields}
 | 
			
		||||
}}
 | 
			
		||||
 | 
			
		||||
impl TryFrom<&reqwest::header::HeaderMap> for {ty_name} {{
 | 
			
		||||
    type Error = StructureError;
 | 
			
		||||
 | 
			
		||||
    fn try_from(map: &reqwest::header::HeaderMap) -> Result<Self, Self::Error> {{
 | 
			
		||||
        {imp}
 | 
			
		||||
        Ok(Self {{ {imp_ret} }})
 | 
			
		||||
    }}
 | 
			
		||||
}}
 | 
			
		||||
"
 | 
			
		||||
    ))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn header_type(header: &Header) -> eyre::Result<String> {
 | 
			
		||||
    crate::methods::param_type_inner(
 | 
			
		||||
        &header._type,
 | 
			
		||||
        header.format.as_deref(),
 | 
			
		||||
        header.items.as_ref(),
 | 
			
		||||
        true,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn create_response_structs(spec: &OpenApiV2, item: &PathItem) -> eyre::Result<String> {
 | 
			
		||||
    let mut s = String::new();
 | 
			
		||||
    if let Some(op) = &item.get {
 | 
			
		||||
        s.push_str(&create_response_structs_for_op(spec, op).wrap_err("GET")?);
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(op) = &item.put {
 | 
			
		||||
        s.push_str(&create_response_structs_for_op(spec, op).wrap_err("PUT")?);
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(op) = &item.post {
 | 
			
		||||
        s.push_str(&create_response_structs_for_op(spec, op).wrap_err("POST")?);
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(op) = &item.delete {
 | 
			
		||||
        s.push_str(&create_response_structs_for_op(spec, op).wrap_err("DELETE")?);
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(op) = &item.options {
 | 
			
		||||
        s.push_str(&create_response_structs_for_op(spec, op).wrap_err("OPTIONS")?);
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(op) = &item.head {
 | 
			
		||||
        s.push_str(&create_response_structs_for_op(spec, op).wrap_err("HEAD")?);
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(op) = &item.patch {
 | 
			
		||||
        s.push_str(&create_response_structs_for_op(spec, op).wrap_err("PATCH")?);
 | 
			
		||||
    }
 | 
			
		||||
    Ok(s)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn create_response_structs_for_op(spec: &OpenApiV2, op: &Operation) -> eyre::Result<String> {
 | 
			
		||||
    let mut out = String::new();
 | 
			
		||||
    let op_name = op
 | 
			
		||||
        .operation_id
 | 
			
		||||
        .as_deref()
 | 
			
		||||
        .ok_or_else(|| eyre::eyre!("no operation id"))?
 | 
			
		||||
        .to_pascal_case();
 | 
			
		||||
    for (_, response) in &op.responses.http_codes {
 | 
			
		||||
        let response = response.deref(spec)?;
 | 
			
		||||
        let tys = create_response_struct(spec, &op_name, response)?;
 | 
			
		||||
        out.push_str(&tys);
 | 
			
		||||
    }
 | 
			
		||||
    Ok(out)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn create_response_struct(
 | 
			
		||||
    spec: &OpenApiV2,
 | 
			
		||||
    name: &str,
 | 
			
		||||
    res: &Response,
 | 
			
		||||
) -> eyre::Result<String> {
 | 
			
		||||
    let mut types = Vec::new();
 | 
			
		||||
    if let Some(MaybeRef::Value { value }) = &res.schema {
 | 
			
		||||
        crate::schema_subtypes(spec, name, "Response", value, &mut types)?;
 | 
			
		||||
    }
 | 
			
		||||
    let mut out = String::new();
 | 
			
		||||
    for ty in types {
 | 
			
		||||
        out.push_str(&ty);
 | 
			
		||||
    }
 | 
			
		||||
    Ok(out)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										502
									
								
								src/admin.rs
									
										
									
									
									
								
							
							
						
						
									
										502
									
								
								src/admin.rs
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,502 +0,0 @@
 | 
			
		|||
use super::*;
 | 
			
		||||
 | 
			
		||||
use std::collections::BTreeMap;
 | 
			
		||||
use std::fmt::Write;
 | 
			
		||||
 | 
			
		||||
impl Forgejo {
 | 
			
		||||
    pub async fn admin_get_crons(&self, query: CronQuery) -> Result<Vec<Cron>, ForgejoError> {
 | 
			
		||||
        self.get(&query.path()).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn admin_run_cron(&self, name: &str) -> Result<(), ForgejoError> {
 | 
			
		||||
        self.post_unit(&format!("admin/cron/{name}"), &()).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn admin_get_emails(
 | 
			
		||||
        &self,
 | 
			
		||||
        query: EmailListQuery,
 | 
			
		||||
    ) -> Result<Vec<Email>, ForgejoError> {
 | 
			
		||||
        self.get(&query.path()).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn admin_search_emails(
 | 
			
		||||
        &self,
 | 
			
		||||
        query: EmailSearchQuery,
 | 
			
		||||
    ) -> Result<Vec<Email>, ForgejoError> {
 | 
			
		||||
        self.get(&query.path()).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn admin_get_hooks(&self, query: HookQuery) -> Result<Vec<Hook>, ForgejoError> {
 | 
			
		||||
        self.get(&query.path()).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn admin_create_hook(&self, opt: CreateHookOption) -> Result<Hook, ForgejoError> {
 | 
			
		||||
        self.post("admin/hooks", &opt).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn admin_get_hook(&self, id: u64) -> Result<Option<Hook>, ForgejoError> {
 | 
			
		||||
        self.get_opt(&format!("admin/hooks/{id}")).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn admin_delete_hook(&self, id: u64) -> Result<(), ForgejoError> {
 | 
			
		||||
        self.delete(&format!("admin/hooks/{id}")).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn admin_edit_hook(
 | 
			
		||||
        &self,
 | 
			
		||||
        id: u64,
 | 
			
		||||
        opt: EditHookOption,
 | 
			
		||||
    ) -> Result<Hook, ForgejoError> {
 | 
			
		||||
        self.patch(&format!("admin/hooks/{id}"), &opt).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn admin_get_orgs(
 | 
			
		||||
        &self,
 | 
			
		||||
        query: AdminOrganizationQuery,
 | 
			
		||||
    ) -> Result<Vec<Organization>, ForgejoError> {
 | 
			
		||||
        self.get(&query.path()).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn admin_unadopted_repos(
 | 
			
		||||
        &self,
 | 
			
		||||
        query: UnadoptedRepoQuery,
 | 
			
		||||
    ) -> Result<Vec<String>, ForgejoError> {
 | 
			
		||||
        self.get(&query.path()).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn admin_adopt(&self, owner: &str, repo: &str) -> Result<(), ForgejoError> {
 | 
			
		||||
        self.post(&format!("admin/unadopted/{owner}/{repo}"), &())
 | 
			
		||||
            .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn admin_delete_unadopted(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
    ) -> Result<(), ForgejoError> {
 | 
			
		||||
        self.delete(&format!("admin/unadopted/{owner}/{repo}"))
 | 
			
		||||
            .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn admin_users(&self, query: AdminUserQuery) -> Result<Vec<User>, ForgejoError> {
 | 
			
		||||
        self.get(&query.path()).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn admin_create_user(&self, opt: CreateUserOption) -> Result<User, ForgejoError> {
 | 
			
		||||
        self.post("admin/users", &opt).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn admin_delete_user(&self, user: &str, purge: bool) -> Result<(), ForgejoError> {
 | 
			
		||||
        self.delete(&format!("admin/users/{user}?purge={purge}"))
 | 
			
		||||
            .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn admin_edit_user(
 | 
			
		||||
        &self,
 | 
			
		||||
        user: &str,
 | 
			
		||||
        opt: CreateUserOption,
 | 
			
		||||
    ) -> Result<User, ForgejoError> {
 | 
			
		||||
        self.patch(&format!("admin/users/{user}"), &opt).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn admin_add_key(
 | 
			
		||||
        &self,
 | 
			
		||||
        user: &str,
 | 
			
		||||
        opt: CreateKeyOption,
 | 
			
		||||
    ) -> Result<PublicKey, ForgejoError> {
 | 
			
		||||
        self.post(&format!("admin/users/{user}/keys"), &opt).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn admin_delete_key(&self, user: &str, id: u64) -> Result<(), ForgejoError> {
 | 
			
		||||
        self.delete(&format!("admin/users/{user}/keys/{id}")).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn admin_create_org(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        opt: CreateOrgOption,
 | 
			
		||||
    ) -> Result<Organization, ForgejoError> {
 | 
			
		||||
        self.post(&format!("admin/users/{owner}/orgs"), &opt).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn admin_rename_user(
 | 
			
		||||
        &self,
 | 
			
		||||
        user: &str,
 | 
			
		||||
        opt: RenameUserOption,
 | 
			
		||||
    ) -> Result<(), ForgejoError> {
 | 
			
		||||
        self.post_unit(&format!("admin/users/{user}/rename"), &opt)
 | 
			
		||||
            .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn admin_create_repo(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        opt: CreateRepoOption,
 | 
			
		||||
    ) -> Result<Repository, ForgejoError> {
 | 
			
		||||
        self.post(&format!("admin/users/{owner}/repos"), &opt).await
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct Cron {
 | 
			
		||||
    pub exec_times: u64,
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339")]
 | 
			
		||||
    pub next: time::OffsetDateTime,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339")]
 | 
			
		||||
    pub prev: time::OffsetDateTime,
 | 
			
		||||
    pub schedule: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Default, Debug)]
 | 
			
		||||
pub struct CronQuery {
 | 
			
		||||
    pub page: Option<u32>,
 | 
			
		||||
    pub limit: Option<u32>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl CronQuery {
 | 
			
		||||
    fn path(&self) -> String {
 | 
			
		||||
        let mut s = String::from("admin/cron?");
 | 
			
		||||
        if let Some(page) = self.page {
 | 
			
		||||
            s.push_str("page=");
 | 
			
		||||
            s.write_fmt(format_args!("{page}"))
 | 
			
		||||
                .expect("writing to string can't fail");
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(limit) = self.limit {
 | 
			
		||||
            s.push_str("limit=");
 | 
			
		||||
            s.write_fmt(format_args!("{limit}"))
 | 
			
		||||
                .expect("writing to string can't fail");
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        s
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct Email {
 | 
			
		||||
    pub email: String,
 | 
			
		||||
    pub primary: bool,
 | 
			
		||||
    pub user_id: u64,
 | 
			
		||||
    pub username: String,
 | 
			
		||||
    pub verified: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Default, Debug)]
 | 
			
		||||
pub struct EmailListQuery {
 | 
			
		||||
    pub page: Option<u32>,
 | 
			
		||||
    pub limit: Option<u32>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl EmailListQuery {
 | 
			
		||||
    fn path(&self) -> String {
 | 
			
		||||
        let mut s = String::from("admin/emails?");
 | 
			
		||||
        if let Some(page) = self.page {
 | 
			
		||||
            s.push_str("page=");
 | 
			
		||||
            s.write_fmt(format_args!("{page}"))
 | 
			
		||||
                .expect("writing to string can't fail");
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(limit) = self.limit {
 | 
			
		||||
            s.push_str("limit=");
 | 
			
		||||
            s.write_fmt(format_args!("{limit}"))
 | 
			
		||||
                .expect("writing to string can't fail");
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        s
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Default, Debug)]
 | 
			
		||||
pub struct EmailSearchQuery {
 | 
			
		||||
    pub query: String,
 | 
			
		||||
    pub page: Option<u32>,
 | 
			
		||||
    pub limit: Option<u32>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl EmailSearchQuery {
 | 
			
		||||
    fn path(&self) -> String {
 | 
			
		||||
        let mut s = String::from("admin/emails/search?");
 | 
			
		||||
        if !self.query.is_empty() {
 | 
			
		||||
            s.push_str("q=");
 | 
			
		||||
            s.push_str(&self.query);
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(page) = self.page {
 | 
			
		||||
            s.push_str("page=");
 | 
			
		||||
            s.write_fmt(format_args!("{page}"))
 | 
			
		||||
                .expect("writing to string can't fail");
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(limit) = self.limit {
 | 
			
		||||
            s.push_str("limit=");
 | 
			
		||||
            s.write_fmt(format_args!("{limit}"))
 | 
			
		||||
                .expect("writing to string can't fail");
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        s
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct Hook {
 | 
			
		||||
    pub active: bool,
 | 
			
		||||
    pub authorization_header: String,
 | 
			
		||||
    pub branch_filter: String,
 | 
			
		||||
    pub config: std::collections::BTreeMap<String, String>,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339")]
 | 
			
		||||
    pub created_at: time::OffsetDateTime,
 | 
			
		||||
    pub events: Vec<String>,
 | 
			
		||||
    pub id: u64,
 | 
			
		||||
    #[serde(rename = "type")]
 | 
			
		||||
    pub _type: HookType,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339")]
 | 
			
		||||
    pub updated_at: time::OffsetDateTime,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
#[non_exhaustive]
 | 
			
		||||
#[serde(rename_all = "lowercase")]
 | 
			
		||||
pub enum HookType {
 | 
			
		||||
    Forgejo,
 | 
			
		||||
    Dingtalk,
 | 
			
		||||
    Discord,
 | 
			
		||||
    Gitea,
 | 
			
		||||
    Gogs,
 | 
			
		||||
    Msteams,
 | 
			
		||||
    Slack,
 | 
			
		||||
    Telegram,
 | 
			
		||||
    Feishu,
 | 
			
		||||
    Wechatwork,
 | 
			
		||||
    Packagist,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Default, Debug)]
 | 
			
		||||
pub struct HookQuery {
 | 
			
		||||
    pub page: Option<u32>,
 | 
			
		||||
    pub limit: Option<u32>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl HookQuery {
 | 
			
		||||
    fn path(&self) -> String {
 | 
			
		||||
        let mut s = String::from("admin/hooks?");
 | 
			
		||||
        if let Some(page) = self.page {
 | 
			
		||||
            s.push_str("page=");
 | 
			
		||||
            s.write_fmt(format_args!("{page}"))
 | 
			
		||||
                .expect("writing to string can't fail");
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(limit) = self.limit {
 | 
			
		||||
            s.push_str("limit=");
 | 
			
		||||
            s.write_fmt(format_args!("{limit}"))
 | 
			
		||||
                .expect("writing to string can't fail");
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        s
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, Debug, PartialEq)]
 | 
			
		||||
pub struct CreateHookOption {
 | 
			
		||||
    pub active: Option<bool>,
 | 
			
		||||
    pub authorization_header: Option<String>,
 | 
			
		||||
    pub branch_filter: Option<String>,
 | 
			
		||||
    pub config: CreateHookOptionConfig,
 | 
			
		||||
    pub events: Vec<String>,
 | 
			
		||||
    #[serde(rename = "type")]
 | 
			
		||||
    pub _type: HookType,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, Debug, PartialEq)]
 | 
			
		||||
pub struct CreateHookOptionConfig {
 | 
			
		||||
    pub content_type: String,
 | 
			
		||||
    pub url: Url,
 | 
			
		||||
    #[serde(flatten)]
 | 
			
		||||
    pub other: BTreeMap<String, String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, Debug, PartialEq, Default)]
 | 
			
		||||
pub struct EditHookOption {
 | 
			
		||||
    pub active: Option<bool>,
 | 
			
		||||
    pub authorization_header: Option<String>,
 | 
			
		||||
    pub branch_filter: Option<String>,
 | 
			
		||||
    pub config: Option<BTreeMap<String, String>>,
 | 
			
		||||
    pub events: Option<Vec<String>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Default, Debug)]
 | 
			
		||||
pub struct AdminOrganizationQuery {
 | 
			
		||||
    pub page: Option<u32>,
 | 
			
		||||
    pub limit: Option<u32>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl AdminOrganizationQuery {
 | 
			
		||||
    fn path(&self) -> String {
 | 
			
		||||
        let mut s = String::from("admin/orgs?");
 | 
			
		||||
        if let Some(page) = self.page {
 | 
			
		||||
            s.push_str("page=");
 | 
			
		||||
            s.write_fmt(format_args!("{page}"))
 | 
			
		||||
                .expect("writing to string can't fail");
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(limit) = self.limit {
 | 
			
		||||
            s.push_str("limit=");
 | 
			
		||||
            s.write_fmt(format_args!("{limit}"))
 | 
			
		||||
                .expect("writing to string can't fail");
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        s
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Default, Debug)]
 | 
			
		||||
pub struct UnadoptedRepoQuery {
 | 
			
		||||
    pub page: Option<u32>,
 | 
			
		||||
    pub limit: Option<u32>,
 | 
			
		||||
    pub pattern: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl UnadoptedRepoQuery {
 | 
			
		||||
    fn path(&self) -> String {
 | 
			
		||||
        let mut s = String::from("admin/unadopted?");
 | 
			
		||||
        if let Some(page) = self.page {
 | 
			
		||||
            s.push_str("page=");
 | 
			
		||||
            s.write_fmt(format_args!("{page}"))
 | 
			
		||||
                .expect("writing to string can't fail");
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(limit) = self.limit {
 | 
			
		||||
            s.push_str("limit=");
 | 
			
		||||
            s.write_fmt(format_args!("{limit}"))
 | 
			
		||||
                .expect("writing to string can't fail");
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        if !self.pattern.is_empty() {
 | 
			
		||||
            s.push_str("pattern=");
 | 
			
		||||
            s.push_str(&self.pattern);
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        s
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Default, Debug)]
 | 
			
		||||
pub struct AdminUserQuery {
 | 
			
		||||
    pub source_id: Option<u64>,
 | 
			
		||||
    pub login_name: String,
 | 
			
		||||
    pub page: Option<u32>,
 | 
			
		||||
    pub limit: Option<u32>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl AdminUserQuery {
 | 
			
		||||
    fn path(&self) -> String {
 | 
			
		||||
        let mut s = String::from("admin/users?");
 | 
			
		||||
        if let Some(source_id) = self.source_id {
 | 
			
		||||
            s.push_str("source_id=");
 | 
			
		||||
            s.write_fmt(format_args!("{source_id}"))
 | 
			
		||||
                .expect("writing to string can't fail");
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        if !self.login_name.is_empty() {
 | 
			
		||||
            s.push_str("login_name=");
 | 
			
		||||
            s.push_str(&self.login_name);
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(page) = self.page {
 | 
			
		||||
            s.push_str("page=");
 | 
			
		||||
            s.write_fmt(format_args!("{page}"))
 | 
			
		||||
                .expect("writing to string can't fail");
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(limit) = self.limit {
 | 
			
		||||
            s.push_str("limit=");
 | 
			
		||||
            s.write_fmt(format_args!("{limit}"))
 | 
			
		||||
                .expect("writing to string can't fail");
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        s
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, Debug, PartialEq)]
 | 
			
		||||
pub struct CreateUserOption {
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339::option")]
 | 
			
		||||
    pub created_at: Option<time::OffsetDateTime>,
 | 
			
		||||
    pub email: String,
 | 
			
		||||
    pub full_name: Option<String>,
 | 
			
		||||
    pub login_name: Option<String>,
 | 
			
		||||
    pub must_change_password: bool,
 | 
			
		||||
    pub password: String,
 | 
			
		||||
    pub restricted: bool,
 | 
			
		||||
    pub send_notify: bool,
 | 
			
		||||
    pub source_id: Option<u64>,
 | 
			
		||||
    pub username: String,
 | 
			
		||||
    pub visibility: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, Debug, PartialEq, Default)]
 | 
			
		||||
pub struct EditUserOption {
 | 
			
		||||
    pub active: Option<bool>,
 | 
			
		||||
    pub admin: Option<bool>,
 | 
			
		||||
    pub allow_create_organization: Option<bool>,
 | 
			
		||||
    pub allow_git_hook: Option<bool>,
 | 
			
		||||
    pub allow_import_local: Option<bool>,
 | 
			
		||||
    pub description: Option<String>,
 | 
			
		||||
    pub email: Option<String>,
 | 
			
		||||
    pub full_name: Option<String>,
 | 
			
		||||
    pub location: Option<String>,
 | 
			
		||||
    pub login_name: Option<String>,
 | 
			
		||||
    pub max_repo_creation: Option<u64>,
 | 
			
		||||
    pub must_change_password: Option<bool>,
 | 
			
		||||
    pub password: Option<String>,
 | 
			
		||||
    pub prohibit_login: Option<bool>,
 | 
			
		||||
    pub restricted: Option<bool>,
 | 
			
		||||
    pub source_id: Option<u64>,
 | 
			
		||||
    pub visibility: Option<String>,
 | 
			
		||||
    pub website: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, Debug, PartialEq)]
 | 
			
		||||
pub struct CreateKeyOption {
 | 
			
		||||
    pub key: String,
 | 
			
		||||
    pub read_only: Option<bool>,
 | 
			
		||||
    pub title: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct PublicKey {
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339")]
 | 
			
		||||
    pub created_at: time::OffsetDateTime,
 | 
			
		||||
    pub fingerprint: String,
 | 
			
		||||
    pub id: u64,
 | 
			
		||||
    pub key: String,
 | 
			
		||||
    pub key_type: String,
 | 
			
		||||
    pub read_only: Option<bool>,
 | 
			
		||||
    pub title: String,
 | 
			
		||||
    pub url: Option<Url>,
 | 
			
		||||
    pub user: User,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, Debug, PartialEq)]
 | 
			
		||||
pub struct CreateOrgOption {
 | 
			
		||||
    pub description: Option<String>,
 | 
			
		||||
    pub full_name: Option<String>,
 | 
			
		||||
    pub location: Option<String>,
 | 
			
		||||
    pub repo_admin_change_team_access: Option<bool>,
 | 
			
		||||
    pub username: String,
 | 
			
		||||
    pub visibility: OrgVisibility,
 | 
			
		||||
    pub website: Option<Url>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, Debug, PartialEq)]
 | 
			
		||||
#[serde(rename_all = "lowercase")]
 | 
			
		||||
pub enum OrgVisibility {
 | 
			
		||||
    Public,
 | 
			
		||||
    Limited,
 | 
			
		||||
    Private,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, Debug, PartialEq)]
 | 
			
		||||
pub struct RenameUserOption {
 | 
			
		||||
    pub new_username: String,
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7226
									
								
								src/generated/methods.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7226
									
								
								src/generated/methods.rs
									
										
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										2
									
								
								src/generated/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/generated/mod.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,2 @@
 | 
			
		|||
pub mod methods;
 | 
			
		||||
pub mod structs;
 | 
			
		||||
							
								
								
									
										5959
									
								
								src/generated/structs.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5959
									
								
								src/generated/structs.rs
									
										
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										347
									
								
								src/issue.rs
									
										
									
									
									
								
							
							
						
						
									
										347
									
								
								src/issue.rs
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,347 +0,0 @@
 | 
			
		|||
use super::*;
 | 
			
		||||
 | 
			
		||||
impl Forgejo {
 | 
			
		||||
    pub async fn get_repo_issues(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        query: IssueQuery,
 | 
			
		||||
    ) -> Result<Vec<Issue>, ForgejoError> {
 | 
			
		||||
        self.get(&query.to_string(owner, repo)).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn create_issue(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        opts: CreateIssueOption,
 | 
			
		||||
    ) -> Result<Issue, ForgejoError> {
 | 
			
		||||
        self.post(&format!("repos/{owner}/{repo}/issues"), &opts)
 | 
			
		||||
            .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn get_issue(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        id: u64,
 | 
			
		||||
    ) -> Result<Option<Issue>, ForgejoError> {
 | 
			
		||||
        self.get_opt(&format!("repos/{owner}/{repo}/issues/{id}"))
 | 
			
		||||
            .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn delete_issue(&self, owner: &str, repo: &str, id: u64) -> Result<(), ForgejoError> {
 | 
			
		||||
        self.delete(&format!("repos/{owner}/{repo}/issues/{id}"))
 | 
			
		||||
            .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn edit_issue(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        id: u64,
 | 
			
		||||
        opts: EditIssueOption,
 | 
			
		||||
    ) -> Result<Issue, ForgejoError> {
 | 
			
		||||
        self.patch(&format!("repos/{owner}/{repo}/issues/{id}"), &opts)
 | 
			
		||||
            .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn get_repo_comments(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        query: RepoCommentQuery,
 | 
			
		||||
    ) -> Result<Vec<Comment>, ForgejoError> {
 | 
			
		||||
        self.get(&query.to_string(owner, repo)).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn get_issue_comments(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        issue_id: u64,
 | 
			
		||||
        query: IssueCommentQuery,
 | 
			
		||||
    ) -> Result<Vec<Comment>, ForgejoError> {
 | 
			
		||||
        self.get(&query.to_string(owner, repo, issue_id)).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn create_comment(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        issue_id: u64,
 | 
			
		||||
        opts: CreateIssueCommentOption,
 | 
			
		||||
    ) -> Result<Comment, ForgejoError> {
 | 
			
		||||
        self.post(
 | 
			
		||||
            &format!("repos/{owner}/{repo}/issues/{issue_id}/comments"),
 | 
			
		||||
            &opts,
 | 
			
		||||
        )
 | 
			
		||||
        .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn get_comment(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        id: u64,
 | 
			
		||||
    ) -> Result<Option<Comment>, ForgejoError> {
 | 
			
		||||
        self.get_opt(&format!("repos/{owner}/{repo}/issues/comments/{id}"))
 | 
			
		||||
            .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn delete_comment(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        id: u64,
 | 
			
		||||
    ) -> Result<(), ForgejoError> {
 | 
			
		||||
        self.delete(&format!("repos/{owner}/{repo}/issues/comments/{id}"))
 | 
			
		||||
            .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn edit_comment(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        id: u64,
 | 
			
		||||
        opts: EditIssueCommentOption,
 | 
			
		||||
    ) -> Result<Comment, ForgejoError> {
 | 
			
		||||
        self.patch(&format!("repos/{owner}/{repo}/issues/comments/{id}"), &opts)
 | 
			
		||||
            .await
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct Issue {
 | 
			
		||||
    pub assets: Vec<Attachment>,
 | 
			
		||||
    pub assignee: Option<User>,
 | 
			
		||||
    pub assignees: Option<Vec<User>>,
 | 
			
		||||
    pub body: String,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339::option")]
 | 
			
		||||
    pub closed_at: Option<time::OffsetDateTime>,
 | 
			
		||||
    pub comments: u64,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339")]
 | 
			
		||||
    pub created_at: time::OffsetDateTime,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339::option")]
 | 
			
		||||
    pub due_date: Option<time::OffsetDateTime>,
 | 
			
		||||
    pub html_url: Url,
 | 
			
		||||
    pub id: u64,
 | 
			
		||||
    pub is_locked: bool,
 | 
			
		||||
    pub labels: Vec<Label>,
 | 
			
		||||
    pub milestone: Option<Milestone>,
 | 
			
		||||
    pub number: u64,
 | 
			
		||||
    pub original_author: String,
 | 
			
		||||
    pub original_author_id: u64,
 | 
			
		||||
    pub pin_order: u64,
 | 
			
		||||
    pub pull_request: Option<PullRequestMeta>,
 | 
			
		||||
    #[serde(rename = "ref")]
 | 
			
		||||
    pub _ref: String,
 | 
			
		||||
    pub repository: RepositoryMeta,
 | 
			
		||||
    pub state: State,
 | 
			
		||||
    pub title: String,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339")]
 | 
			
		||||
    pub updated_at: time::OffsetDateTime,
 | 
			
		||||
    pub url: Url,
 | 
			
		||||
    pub user: User,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct Label {
 | 
			
		||||
    pub color: String,
 | 
			
		||||
    pub description: String,
 | 
			
		||||
    pub exclusive: bool,
 | 
			
		||||
    pub id: u64,
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub url: Url,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct Attachment {
 | 
			
		||||
    pub browser_download_url: Url,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339")]
 | 
			
		||||
    pub created_at: time::OffsetDateTime,
 | 
			
		||||
    pub download_count: u64,
 | 
			
		||||
    pub id: u64,
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub size: u64,
 | 
			
		||||
    pub uuid: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, Debug, PartialEq, Default)]
 | 
			
		||||
pub struct EditAttachmentOption {
 | 
			
		||||
    pub name: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone, Copy)]
 | 
			
		||||
pub enum State {
 | 
			
		||||
    #[serde(rename = "open")]
 | 
			
		||||
    Open,
 | 
			
		||||
    #[serde(rename = "closed")]
 | 
			
		||||
    Closed,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl State {
 | 
			
		||||
    pub(crate) fn as_str(&self) -> &'static str {
 | 
			
		||||
        match self {
 | 
			
		||||
            State::Open => "open",
 | 
			
		||||
            State::Closed => "closed",
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct Comment {
 | 
			
		||||
    pub assets: Vec<Attachment>,
 | 
			
		||||
    pub body: String,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339")]
 | 
			
		||||
    pub created_at: time::OffsetDateTime,
 | 
			
		||||
    pub html_url: Url,
 | 
			
		||||
    pub id: u64,
 | 
			
		||||
    pub issue_url: Url,
 | 
			
		||||
    pub original_author: String,
 | 
			
		||||
    pub original_author_id: u64,
 | 
			
		||||
    #[serde(deserialize_with = "crate::none_if_blank_url")]
 | 
			
		||||
    pub pull_request_url: Option<Url>,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339")]
 | 
			
		||||
    pub updated_at: time::OffsetDateTime,
 | 
			
		||||
    pub user: User,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Default, Debug)]
 | 
			
		||||
pub struct IssueQuery {
 | 
			
		||||
    pub state: Option<State>,
 | 
			
		||||
    pub labels: Vec<String>,
 | 
			
		||||
    pub query: Option<String>,
 | 
			
		||||
    pub _type: Option<IssueQueryType>,
 | 
			
		||||
    pub milestones: Vec<String>,
 | 
			
		||||
    pub since: Option<time::OffsetDateTime>,
 | 
			
		||||
    pub before: Option<time::OffsetDateTime>,
 | 
			
		||||
    pub created_by: Option<String>,
 | 
			
		||||
    pub assigned_by: Option<String>,
 | 
			
		||||
    pub mentioned_by: Option<String>,
 | 
			
		||||
    pub page: Option<u32>,
 | 
			
		||||
    pub limit: Option<u32>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl IssueQuery {
 | 
			
		||||
    fn to_string(&self, owner: &str, repo: &str) -> String {
 | 
			
		||||
        format!("repos/{owner}/{repo}/issues?state={}&labels={}&q={}&type={}&milestones={}&since={}&before={}&created_by={}&assigned_by={}&mentioned_by={}&page={}&limit={}",
 | 
			
		||||
            self.state.map(|s| s.as_str()).unwrap_or_default(),
 | 
			
		||||
            self.labels.join(","),
 | 
			
		||||
            self.query.as_deref().unwrap_or_default(),
 | 
			
		||||
            self._type.map(|t| t.as_str()).unwrap_or_default(),
 | 
			
		||||
            self.milestones.join(","),
 | 
			
		||||
            self.since.map(|t| t.format(&time::format_description::well_known::Rfc3339).unwrap()).unwrap_or_default(),
 | 
			
		||||
            self.before.map(|t| t.format(&time::format_description::well_known::Rfc3339).unwrap()).unwrap_or_default(),
 | 
			
		||||
            self.created_by.as_deref().unwrap_or_default(),
 | 
			
		||||
            self.assigned_by.as_deref().unwrap_or_default(),
 | 
			
		||||
            self.mentioned_by.as_deref().unwrap_or_default(),
 | 
			
		||||
            self.page.map(|page| page.to_string()).unwrap_or_default(),
 | 
			
		||||
            self.limit.map(|page| page.to_string()).unwrap_or_default(),
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, PartialEq, Clone, Copy)]
 | 
			
		||||
pub enum IssueQueryType {
 | 
			
		||||
    Issues,
 | 
			
		||||
    Pulls,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl IssueQueryType {
 | 
			
		||||
    fn as_str(&self) -> &'static str {
 | 
			
		||||
        match self {
 | 
			
		||||
            IssueQueryType::Issues => "issues",
 | 
			
		||||
            IssueQueryType::Pulls => "pulls",
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Default, Debug)]
 | 
			
		||||
pub struct IssueCommentQuery {
 | 
			
		||||
    pub since: Option<time::OffsetDateTime>,
 | 
			
		||||
    pub before: Option<time::OffsetDateTime>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl IssueCommentQuery {
 | 
			
		||||
    fn to_string(&self, owner: &str, repo: &str, issue_id: u64) -> String {
 | 
			
		||||
        format!(
 | 
			
		||||
            "repos/{owner}/{repo}/issues/{issue_id}/comments?since={}&before={}",
 | 
			
		||||
            self.since
 | 
			
		||||
                .map(|t| t
 | 
			
		||||
                    .format(&time::format_description::well_known::Rfc3339)
 | 
			
		||||
                    .unwrap())
 | 
			
		||||
                .unwrap_or_default(),
 | 
			
		||||
            self.before
 | 
			
		||||
                .map(|t| t
 | 
			
		||||
                    .format(&time::format_description::well_known::Rfc3339)
 | 
			
		||||
                    .unwrap())
 | 
			
		||||
                .unwrap_or_default(),
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Default, Debug)]
 | 
			
		||||
pub struct RepoCommentQuery {
 | 
			
		||||
    pub since: Option<time::OffsetDateTime>,
 | 
			
		||||
    pub before: Option<time::OffsetDateTime>,
 | 
			
		||||
    pub page: Option<u32>,
 | 
			
		||||
    pub limit: Option<u32>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl RepoCommentQuery {
 | 
			
		||||
    fn to_string(&self, owner: &str, repo: &str) -> String {
 | 
			
		||||
        format!(
 | 
			
		||||
            "repos/{owner}/{repo}/issues/comments?since={}&before={}&page={}&limit={}",
 | 
			
		||||
            self.since
 | 
			
		||||
                .map(|t| t
 | 
			
		||||
                    .format(&time::format_description::well_known::Rfc3339)
 | 
			
		||||
                    .unwrap())
 | 
			
		||||
                .unwrap_or_default(),
 | 
			
		||||
            self.before
 | 
			
		||||
                .map(|t| t
 | 
			
		||||
                    .format(&time::format_description::well_known::Rfc3339)
 | 
			
		||||
                    .unwrap())
 | 
			
		||||
                .unwrap_or_default(),
 | 
			
		||||
            self.page.map(|page| page.to_string()).unwrap_or_default(),
 | 
			
		||||
            self.limit.map(|page| page.to_string()).unwrap_or_default(),
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, Debug, PartialEq, Default)]
 | 
			
		||||
pub struct CreateIssueOption {
 | 
			
		||||
    pub assignees: Vec<String>,
 | 
			
		||||
    pub body: Option<String>,
 | 
			
		||||
    pub closed: Option<bool>,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339::option")]
 | 
			
		||||
    pub due_date: Option<time::OffsetDateTime>,
 | 
			
		||||
    pub labels: Vec<u64>,
 | 
			
		||||
    pub milestone: Option<u64>,
 | 
			
		||||
    pub _ref: Option<String>,
 | 
			
		||||
    pub title: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, Debug, PartialEq, Default)]
 | 
			
		||||
pub struct EditIssueOption {
 | 
			
		||||
    pub assignees: Vec<String>,
 | 
			
		||||
    pub body: Option<String>,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339::option")]
 | 
			
		||||
    pub due_date: Option<time::OffsetDateTime>,
 | 
			
		||||
    pub labels: Vec<u64>,
 | 
			
		||||
    pub milestone: Option<u64>,
 | 
			
		||||
    pub _ref: Option<String>,
 | 
			
		||||
    pub state: Option<State>,
 | 
			
		||||
    pub title: Option<String>,
 | 
			
		||||
    pub unset_due_date: Option<bool>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, Debug, PartialEq, Default)]
 | 
			
		||||
pub struct CreateIssueCommentOption {
 | 
			
		||||
    pub body: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, Debug, PartialEq, Default)]
 | 
			
		||||
pub struct EditIssueCommentOption {
 | 
			
		||||
    pub body: String,
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										257
									
								
								src/lib.rs
									
										
									
									
									
								
							
							
						
						
									
										257
									
								
								src/lib.rs
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,5 +1,4 @@
 | 
			
		|||
use reqwest::{Client, Request, StatusCode};
 | 
			
		||||
use serde::{de::DeserializeOwned, Serialize};
 | 
			
		||||
use soft_assert::*;
 | 
			
		||||
use url::Url;
 | 
			
		||||
use zeroize::Zeroize;
 | 
			
		||||
| 
						 | 
				
			
			@ -9,23 +8,9 @@ pub struct Forgejo {
 | 
			
		|||
    client: Client,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
mod admin;
 | 
			
		||||
mod issue;
 | 
			
		||||
mod misc;
 | 
			
		||||
mod notification;
 | 
			
		||||
mod organization;
 | 
			
		||||
mod package;
 | 
			
		||||
mod repository;
 | 
			
		||||
mod user;
 | 
			
		||||
mod generated;
 | 
			
		||||
 | 
			
		||||
pub use admin::*;
 | 
			
		||||
pub use issue::*;
 | 
			
		||||
pub use misc::*;
 | 
			
		||||
pub use notification::*;
 | 
			
		||||
pub use organization::*;
 | 
			
		||||
pub use package::*;
 | 
			
		||||
pub use repository::*;
 | 
			
		||||
pub use user::*;
 | 
			
		||||
pub use generated::structs;
 | 
			
		||||
 | 
			
		||||
#[derive(thiserror::Error, Debug)]
 | 
			
		||||
pub enum ForgejoError {
 | 
			
		||||
| 
						 | 
				
			
			@ -38,15 +23,30 @@ pub enum ForgejoError {
 | 
			
		|||
    #[error("API key should be ascii")]
 | 
			
		||||
    KeyNotAscii,
 | 
			
		||||
    #[error("the response from forgejo was not properly structured")]
 | 
			
		||||
    BadStructure(#[source] serde_json::Error, String),
 | 
			
		||||
    BadStructure(#[from] StructureError),
 | 
			
		||||
    #[error("unexpected status code {} {}", .0.as_u16(), .0.canonical_reason().unwrap_or(""))]
 | 
			
		||||
    UnexpectedStatusCode(StatusCode),
 | 
			
		||||
    #[error("{} {}: {}", .0.as_u16(), .0.canonical_reason().unwrap_or(""), .1)]
 | 
			
		||||
    ApiError(StatusCode, String),
 | 
			
		||||
    #[error("{} {}{}", .0.as_u16(), .0.canonical_reason().unwrap_or(""), .1.as_ref().map(|s| format!(": {s}")).unwrap_or_default())]
 | 
			
		||||
    ApiError(StatusCode, Option<String>),
 | 
			
		||||
    #[error("the provided authorization was too long to accept")]
 | 
			
		||||
    AuthTooLong,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(thiserror::Error, Debug)]
 | 
			
		||||
pub enum StructureError {
 | 
			
		||||
    #[error("{contents}")]
 | 
			
		||||
    Serde {
 | 
			
		||||
        e: serde_json::Error,
 | 
			
		||||
        contents: String,
 | 
			
		||||
    },
 | 
			
		||||
    #[error("failed to find header `{0}`")]
 | 
			
		||||
    HeaderMissing(&'static str),
 | 
			
		||||
    #[error("header was not ascii")]
 | 
			
		||||
    HeaderNotAscii,
 | 
			
		||||
    #[error("failed to parse header")]
 | 
			
		||||
    HeaderParseFailed,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Method of authentication to connect to the Forgejo host with.
 | 
			
		||||
pub enum Auth<'a> {
 | 
			
		||||
    /// Application Access Token. Grants access to scope enabled for the
 | 
			
		||||
| 
						 | 
				
			
			@ -134,218 +134,67 @@ impl Forgejo {
 | 
			
		|||
        Ok(Self { url, client })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T, ForgejoError> {
 | 
			
		||||
        let url = self.url.join("api/v1/").unwrap().join(path).unwrap();
 | 
			
		||||
        let request = self.client.get(url).build()?;
 | 
			
		||||
        self.execute(request).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn get_opt<T: DeserializeOwned>(&self, path: &str) -> Result<Option<T>, ForgejoError> {
 | 
			
		||||
        let url = self.url.join("api/v1/").unwrap().join(path).unwrap();
 | 
			
		||||
        let request = self.client.get(url).build()?;
 | 
			
		||||
        self.execute_opt(request).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn get_str(&self, path: &str) -> Result<String, ForgejoError> {
 | 
			
		||||
        let url = self.url.join("api/v1/").unwrap().join(path).unwrap();
 | 
			
		||||
        let request = self.client.get(url).build()?;
 | 
			
		||||
        self.execute_str(request).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn get_exists(&self, path: &str) -> Result<bool, ForgejoError> {
 | 
			
		||||
        let url = self.url.join("api/v1/").unwrap().join(path).unwrap();
 | 
			
		||||
        let request = self.client.get(url).build()?;
 | 
			
		||||
        self.execute_exists(request).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn post<T: Serialize, U: DeserializeOwned>(
 | 
			
		||||
    pub async fn download_release_attachment(
 | 
			
		||||
        &self,
 | 
			
		||||
        path: &str,
 | 
			
		||||
        body: &T,
 | 
			
		||||
    ) -> Result<U, ForgejoError> {
 | 
			
		||||
        let url = self.url.join("api/v1/").unwrap().join(path).unwrap();
 | 
			
		||||
        let request = self.client.post(url).json(body).build()?;
 | 
			
		||||
        self.execute(request).await
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        release: u64,
 | 
			
		||||
        attach: u64,
 | 
			
		||||
    ) -> Result<bytes::Bytes, ForgejoError> {
 | 
			
		||||
        let release = self
 | 
			
		||||
            .repo_get_release_attachment(owner, repo, release, attach)
 | 
			
		||||
            .await?;
 | 
			
		||||
        let request = self
 | 
			
		||||
            .client
 | 
			
		||||
            .get(format!("/attachments/{}", release.uuid.unwrap()))
 | 
			
		||||
            .build()?;
 | 
			
		||||
        Ok(self.execute(request).await?.bytes().await?)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn post_multipart<T: DeserializeOwned>(
 | 
			
		||||
        &self,
 | 
			
		||||
        path: &str,
 | 
			
		||||
        body: reqwest::multipart::Form,
 | 
			
		||||
    ) -> Result<T, ForgejoError> {
 | 
			
		||||
    fn get(&self, path: &str) -> reqwest::RequestBuilder {
 | 
			
		||||
        let url = self.url.join("api/v1/").unwrap().join(path).unwrap();
 | 
			
		||||
        let request = self.client.post(url).multipart(body).build()?;
 | 
			
		||||
        self.execute(request).await
 | 
			
		||||
        self.client.get(url)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn post_str_out<T: Serialize>(
 | 
			
		||||
        &self,
 | 
			
		||||
        path: &str,
 | 
			
		||||
        body: &T,
 | 
			
		||||
    ) -> Result<String, ForgejoError> {
 | 
			
		||||
    fn put(&self, path: &str) -> reqwest::RequestBuilder {
 | 
			
		||||
        let url = self.url.join("api/v1/").unwrap().join(path).unwrap();
 | 
			
		||||
        let request = self.client.post(url).json(body).build()?;
 | 
			
		||||
        self.execute_str(request).await
 | 
			
		||||
        self.client.put(url)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn post_unit<T: Serialize>(&self, path: &str, body: &T) -> Result<(), ForgejoError> {
 | 
			
		||||
    fn post(&self, path: &str) -> reqwest::RequestBuilder {
 | 
			
		||||
        let url = self.url.join("api/v1/").unwrap().join(path).unwrap();
 | 
			
		||||
        let request = self.client.post(url).json(body).build()?;
 | 
			
		||||
        self.execute_unit(request).await
 | 
			
		||||
        self.client.post(url)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn post_raw(&self, path: &str, body: String) -> Result<String, ForgejoError> {
 | 
			
		||||
    fn delete(&self, path: &str) -> reqwest::RequestBuilder {
 | 
			
		||||
        let url = self.url.join("api/v1/").unwrap().join(path).unwrap();
 | 
			
		||||
        let request = self.client.post(url).body(body).build()?;
 | 
			
		||||
        self.execute_str(request).await
 | 
			
		||||
        self.client.delete(url)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn delete(&self, path: &str) -> Result<(), ForgejoError> {
 | 
			
		||||
    fn patch(&self, path: &str) -> reqwest::RequestBuilder {
 | 
			
		||||
        let url = self.url.join("api/v1/").unwrap().join(path).unwrap();
 | 
			
		||||
        let request = self.client.delete(url).build()?;
 | 
			
		||||
        self.execute_unit(request).await
 | 
			
		||||
        self.client.patch(url)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn patch<T: Serialize, U: DeserializeOwned>(
 | 
			
		||||
        &self,
 | 
			
		||||
        path: &str,
 | 
			
		||||
        body: &T,
 | 
			
		||||
    ) -> Result<U, ForgejoError> {
 | 
			
		||||
        let url = self.url.join("api/v1/").unwrap().join(path).unwrap();
 | 
			
		||||
        let request = self.client.patch(url).json(body).build()?;
 | 
			
		||||
        self.execute(request).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn put<T: DeserializeOwned>(&self, path: &str) -> Result<T, ForgejoError> {
 | 
			
		||||
        let url = self.url.join("api/v1/").unwrap().join(path).unwrap();
 | 
			
		||||
        let request = self.client.put(url).build()?;
 | 
			
		||||
        self.execute(request).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn execute<T: DeserializeOwned>(&self, request: Request) -> Result<T, ForgejoError> {
 | 
			
		||||
    async fn execute(&self, request: Request) -> Result<reqwest::Response, ForgejoError> {
 | 
			
		||||
        let response = self.client.execute(request).await?;
 | 
			
		||||
        match response.status() {
 | 
			
		||||
            status if status.is_success() => {
 | 
			
		||||
                let body = response.text().await?;
 | 
			
		||||
                let out =
 | 
			
		||||
                    serde_json::from_str(&body).map_err(|e| ForgejoError::BadStructure(e, body))?;
 | 
			
		||||
                Ok(out)
 | 
			
		||||
            status if status.is_success() => Ok(response),
 | 
			
		||||
            status if status.is_client_error() => {
 | 
			
		||||
                Err(ForgejoError::ApiError(status, maybe_err(response).await))
 | 
			
		||||
            }
 | 
			
		||||
            status if status.is_client_error() => Err(ForgejoError::ApiError(
 | 
			
		||||
                status,
 | 
			
		||||
                response
 | 
			
		||||
                    .json::<ErrorMessage>()
 | 
			
		||||
                    .await?
 | 
			
		||||
                    .message
 | 
			
		||||
                    .unwrap_or_else(|| String::from("[no message]")),
 | 
			
		||||
            )),
 | 
			
		||||
            status => Err(ForgejoError::UnexpectedStatusCode(status)),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Like `execute`, but returns a `String`.
 | 
			
		||||
    async fn execute_opt_raw(
 | 
			
		||||
        &self,
 | 
			
		||||
        request: Request,
 | 
			
		||||
    ) -> Result<Option<bytes::Bytes>, ForgejoError> {
 | 
			
		||||
        let response = self.client.execute(request).await?;
 | 
			
		||||
        match response.status() {
 | 
			
		||||
            status if status.is_success() => Ok(Some(response.bytes().await?)),
 | 
			
		||||
            StatusCode::NOT_FOUND => Ok(None),
 | 
			
		||||
            status if status.is_client_error() => Err(ForgejoError::ApiError(
 | 
			
		||||
                status,
 | 
			
		||||
                response
 | 
			
		||||
                    .json::<ErrorMessage>()
 | 
			
		||||
                    .await?
 | 
			
		||||
                    .message
 | 
			
		||||
                    .unwrap_or_else(|| String::from("[no message]")),
 | 
			
		||||
            )),
 | 
			
		||||
            status => Err(ForgejoError::UnexpectedStatusCode(status)),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Like `execute`, but returns a `String`.
 | 
			
		||||
    async fn execute_str(&self, request: Request) -> Result<String, ForgejoError> {
 | 
			
		||||
        let response = self.client.execute(request).await?;
 | 
			
		||||
        match response.status() {
 | 
			
		||||
            status if status.is_success() => Ok(response.text().await?),
 | 
			
		||||
            status if status.is_client_error() => Err(ForgejoError::ApiError(
 | 
			
		||||
                status,
 | 
			
		||||
                response
 | 
			
		||||
                    .json::<ErrorMessage>()
 | 
			
		||||
                    .await?
 | 
			
		||||
                    .message
 | 
			
		||||
                    .unwrap_or_else(|| String::from("[no message]")),
 | 
			
		||||
            )),
 | 
			
		||||
            status => Err(ForgejoError::UnexpectedStatusCode(status)),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Like `execute`, but returns unit.
 | 
			
		||||
    async fn execute_unit(&self, request: Request) -> Result<(), ForgejoError> {
 | 
			
		||||
        let response = self.client.execute(request).await?;
 | 
			
		||||
        match response.status() {
 | 
			
		||||
            status if status.is_success() => Ok(()),
 | 
			
		||||
            status if status.is_client_error() => Err(ForgejoError::ApiError(
 | 
			
		||||
                status,
 | 
			
		||||
                response
 | 
			
		||||
                    .json::<ErrorMessage>()
 | 
			
		||||
                    .await?
 | 
			
		||||
                    .message
 | 
			
		||||
                    .unwrap_or_else(|| String::from("[no message]")),
 | 
			
		||||
            )),
 | 
			
		||||
            status => Err(ForgejoError::UnexpectedStatusCode(status)),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Like `execute`, but returns `Ok(None)` on 404.
 | 
			
		||||
    async fn execute_opt<T: DeserializeOwned>(
 | 
			
		||||
        &self,
 | 
			
		||||
        request: Request,
 | 
			
		||||
    ) -> Result<Option<T>, ForgejoError> {
 | 
			
		||||
        let response = self.client.execute(request).await?;
 | 
			
		||||
        match response.status() {
 | 
			
		||||
            status if status.is_success() => {
 | 
			
		||||
                let body = response.text().await?;
 | 
			
		||||
                let out =
 | 
			
		||||
                    serde_json::from_str(&body).map_err(|e| ForgejoError::BadStructure(e, body))?;
 | 
			
		||||
                Ok(out)
 | 
			
		||||
            }
 | 
			
		||||
            StatusCode::NOT_FOUND => Ok(None),
 | 
			
		||||
            status if status.is_client_error() => Err(ForgejoError::ApiError(
 | 
			
		||||
                status,
 | 
			
		||||
                response
 | 
			
		||||
                    .json::<ErrorMessage>()
 | 
			
		||||
                    .await?
 | 
			
		||||
                    .message
 | 
			
		||||
                    .unwrap_or_else(|| String::from("[no message]")),
 | 
			
		||||
            )),
 | 
			
		||||
            status => Err(ForgejoError::UnexpectedStatusCode(status)),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Like `execute`, but returns `false` on 404.
 | 
			
		||||
    async fn execute_exists(&self, request: Request) -> Result<bool, ForgejoError> {
 | 
			
		||||
        let response = self.client.execute(request).await?;
 | 
			
		||||
        match response.status() {
 | 
			
		||||
            status if status.is_success() => Ok(true),
 | 
			
		||||
            StatusCode::NOT_FOUND => Ok(false),
 | 
			
		||||
            status if status.is_client_error() => Err(ForgejoError::ApiError(
 | 
			
		||||
                status,
 | 
			
		||||
                response
 | 
			
		||||
                    .json::<ErrorMessage>()
 | 
			
		||||
                    .await?
 | 
			
		||||
                    .message
 | 
			
		||||
                    .unwrap_or_else(|| String::from("[no message]")),
 | 
			
		||||
            )),
 | 
			
		||||
            status => Err(ForgejoError::UnexpectedStatusCode(status)),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn maybe_err(res: reqwest::Response) -> Option<String> {
 | 
			
		||||
    res.json::<ErrorMessage>().await.ok().map(|e| e.message)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize)]
 | 
			
		||||
struct ErrorMessage {
 | 
			
		||||
    message: Option<String>,
 | 
			
		||||
    message: String,
 | 
			
		||||
    // intentionally ignored, no need for now
 | 
			
		||||
    // url: Url
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										162
									
								
								src/misc.rs
									
										
									
									
									
								
							
							
						
						
									
										162
									
								
								src/misc.rs
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,162 +0,0 @@
 | 
			
		|||
use super::*;
 | 
			
		||||
 | 
			
		||||
impl Forgejo {
 | 
			
		||||
    pub async fn get_gitignore_templates(&self) -> Result<Vec<String>, ForgejoError> {
 | 
			
		||||
        self.get("gitignore/templates").await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn get_gitignore_template(
 | 
			
		||||
        &self,
 | 
			
		||||
        name: &str,
 | 
			
		||||
    ) -> Result<Option<GitignoreTemplateInfo>, ForgejoError> {
 | 
			
		||||
        self.get_opt(&format!("gitignore/templates/{name}")).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn get_label_templates(&self) -> Result<Vec<String>, ForgejoError> {
 | 
			
		||||
        self.get("label/templates").await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn get_label_template(&self, name: &str) -> Result<Vec<LabelTemplate>, ForgejoError> {
 | 
			
		||||
        self.get(&format!("label/templates/{name}")).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn get_licenses(&self) -> Result<Vec<LicenseTemplateListEntry>, ForgejoError> {
 | 
			
		||||
        self.get("licenses").await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn get_license(
 | 
			
		||||
        &self,
 | 
			
		||||
        name: &str,
 | 
			
		||||
    ) -> Result<Option<GitignoreTemplateInfo>, ForgejoError> {
 | 
			
		||||
        self.get_opt(&format!("license/{name}")).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn render_markdown(&self, opt: MarkdownOption) -> Result<String, ForgejoError> {
 | 
			
		||||
        self.post_str_out("markdown", &opt).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn render_markdown_raw(&self, body: String) -> Result<String, ForgejoError> {
 | 
			
		||||
        self.post_raw("markdown/raw", body).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn render_markup(&self, opt: MarkupOption) -> Result<String, ForgejoError> {
 | 
			
		||||
        self.post_str_out("markup", &opt).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn nodeinfo(&self) -> Result<NodeInfo, ForgejoError> {
 | 
			
		||||
        self.get("nodeinfo").await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn signing_key(&self) -> Result<String, ForgejoError> {
 | 
			
		||||
        self.get_str("signing-key.gpg").await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn version(&self) -> Result<ServerVersion, ForgejoError> {
 | 
			
		||||
        self.get("version").await
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct GitignoreTemplateInfo {
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub source: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct LabelTemplate {
 | 
			
		||||
    pub color: String,
 | 
			
		||||
    pub description: String,
 | 
			
		||||
    pub exclusive: bool,
 | 
			
		||||
    pub name: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct LicenseTemplateListEntry {
 | 
			
		||||
    pub key: String,
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub url: Url,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct LicenseTemplateInfo {
 | 
			
		||||
    pub body: String,
 | 
			
		||||
    pub implementation: String,
 | 
			
		||||
    pub key: String,
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub url: Url,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, Debug, PartialEq, Default)]
 | 
			
		||||
pub struct MarkdownOption {
 | 
			
		||||
    #[serde(rename = "Context")]
 | 
			
		||||
    pub context: String,
 | 
			
		||||
    #[serde(rename = "Mode")]
 | 
			
		||||
    pub mode: String,
 | 
			
		||||
    #[serde(rename = "Text")]
 | 
			
		||||
    pub text: String,
 | 
			
		||||
    #[serde(rename = "Wiki")]
 | 
			
		||||
    pub wiki: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, Debug, PartialEq, Default)]
 | 
			
		||||
pub struct MarkupOption {
 | 
			
		||||
    #[serde(rename = "Context")]
 | 
			
		||||
    pub context: String,
 | 
			
		||||
    #[serde(rename = "FilePath")]
 | 
			
		||||
    pub file_path: String,
 | 
			
		||||
    #[serde(rename = "Mode")]
 | 
			
		||||
    pub mode: String,
 | 
			
		||||
    #[serde(rename = "Text")]
 | 
			
		||||
    pub text: String,
 | 
			
		||||
    #[serde(rename = "Wiki")]
 | 
			
		||||
    pub wiki: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct NodeInfo {
 | 
			
		||||
    pub metadata: std::collections::BTreeMap<String, String>,
 | 
			
		||||
    #[serde(rename = "openRegistrations")]
 | 
			
		||||
    pub open_registrations: bool,
 | 
			
		||||
    pub protocols: Vec<String>,
 | 
			
		||||
    pub services: NodeInfoServices,
 | 
			
		||||
    pub software: NodeInfoSoftware,
 | 
			
		||||
    pub usage: NodeInfoUsage,
 | 
			
		||||
    pub version: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct NodeInfoServices {
 | 
			
		||||
    pub inbound: Vec<String>,
 | 
			
		||||
    pub outbound: Vec<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct NodeInfoSoftware {
 | 
			
		||||
    pub homepage: Url,
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub repository: Url,
 | 
			
		||||
    pub version: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct NodeInfoUsage {
 | 
			
		||||
    #[serde(rename = "localComments")]
 | 
			
		||||
    pub local_comments: u64,
 | 
			
		||||
    #[serde(rename = "localPosts")]
 | 
			
		||||
    pub local_posts: u64,
 | 
			
		||||
    pub users: NodeInfoUsageUsers,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct NodeInfoUsageUsers {
 | 
			
		||||
    #[serde(rename = "activeHalfYear")]
 | 
			
		||||
    pub active_half_year: u64,
 | 
			
		||||
    #[serde(rename = "activeMonth")]
 | 
			
		||||
    pub active_month: u64,
 | 
			
		||||
    pub total: u64,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct ServerVersion {
 | 
			
		||||
    pub version: String,
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,273 +0,0 @@
 | 
			
		|||
use super::*;
 | 
			
		||||
 | 
			
		||||
impl Forgejo {
 | 
			
		||||
    pub async fn notifications(
 | 
			
		||||
        &self,
 | 
			
		||||
        query: NotificationQuery,
 | 
			
		||||
    ) -> Result<Vec<NotificationThread>, ForgejoError> {
 | 
			
		||||
        self.get(&format!("notifications?{}", query.query_string()))
 | 
			
		||||
            .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn set_notifications_state(
 | 
			
		||||
        &self,
 | 
			
		||||
        query: NotificationPutQuery,
 | 
			
		||||
    ) -> Result<Vec<NotificationThread>, ForgejoError> {
 | 
			
		||||
        self.put(&format!("notifications?{}", query.query_string()))
 | 
			
		||||
            .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn notification_count(&self) -> Result<Vec<NotificationCount>, ForgejoError> {
 | 
			
		||||
        self.get("notifications/new").await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn get_notification(
 | 
			
		||||
        &self,
 | 
			
		||||
        id: u64,
 | 
			
		||||
    ) -> Result<Option<NotificationThread>, ForgejoError> {
 | 
			
		||||
        self.get_opt(&format!("notifications/threads/{id}")).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn set_notification_state(
 | 
			
		||||
        &self,
 | 
			
		||||
        id: u64,
 | 
			
		||||
        to_status: ToStatus,
 | 
			
		||||
    ) -> Result<Option<NotificationThread>, ForgejoError> {
 | 
			
		||||
        self.patch(
 | 
			
		||||
            &format!(
 | 
			
		||||
                "notifications/threads/{id}?to-status={}",
 | 
			
		||||
                to_status.as_str()
 | 
			
		||||
            ),
 | 
			
		||||
            &(),
 | 
			
		||||
        )
 | 
			
		||||
        .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn get_repo_notifications(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        name: &str,
 | 
			
		||||
        query: NotificationQuery,
 | 
			
		||||
    ) -> Result<Vec<NotificationThread>, ForgejoError> {
 | 
			
		||||
        self.get(&format!(
 | 
			
		||||
            "repos/{owner}/{name}/notifications?{}",
 | 
			
		||||
            query.query_string()
 | 
			
		||||
        ))
 | 
			
		||||
        .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn set_repo_notifications_state(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        name: &str,
 | 
			
		||||
        query: NotificationPutQuery,
 | 
			
		||||
    ) -> Result<Vec<NotificationThread>, ForgejoError> {
 | 
			
		||||
        self.put(&format!(
 | 
			
		||||
            "repos/{owner}/{name}/notifications?{}",
 | 
			
		||||
            query.query_string()
 | 
			
		||||
        ))
 | 
			
		||||
        .await
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub struct NotificationQuery {
 | 
			
		||||
    pub all: bool,
 | 
			
		||||
    pub include_unread: bool,
 | 
			
		||||
    pub include_read: bool,
 | 
			
		||||
    pub include_pinned: bool,
 | 
			
		||||
    pub subject_type: Option<NotificationSubjectType>,
 | 
			
		||||
    pub since: Option<time::OffsetDateTime>,
 | 
			
		||||
    pub before: Option<time::OffsetDateTime>,
 | 
			
		||||
    pub page: Option<u32>,
 | 
			
		||||
    pub limit: Option<u32>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Default for NotificationQuery {
 | 
			
		||||
    fn default() -> Self {
 | 
			
		||||
        NotificationQuery {
 | 
			
		||||
            all: false,
 | 
			
		||||
            include_unread: true,
 | 
			
		||||
            include_read: false,
 | 
			
		||||
            include_pinned: true,
 | 
			
		||||
            subject_type: None,
 | 
			
		||||
            since: None,
 | 
			
		||||
            before: None,
 | 
			
		||||
            page: None,
 | 
			
		||||
            limit: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl NotificationQuery {
 | 
			
		||||
    fn query_string(&self) -> String {
 | 
			
		||||
        use std::fmt::Write;
 | 
			
		||||
        let mut s = String::new();
 | 
			
		||||
        if self.all {
 | 
			
		||||
            s.push_str("all=true&");
 | 
			
		||||
        }
 | 
			
		||||
        if self.include_unread {
 | 
			
		||||
            s.push_str("status-types=unread&");
 | 
			
		||||
        }
 | 
			
		||||
        if self.include_read {
 | 
			
		||||
            s.push_str("status-types=read&");
 | 
			
		||||
        }
 | 
			
		||||
        if self.include_pinned {
 | 
			
		||||
            s.push_str("status-types=pinned&");
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(subject_type) = self.subject_type {
 | 
			
		||||
            s.push_str("subject-type=");
 | 
			
		||||
            s.push_str(subject_type.as_str());
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(since) = &self.since {
 | 
			
		||||
            s.push_str("since=");
 | 
			
		||||
            s.push_str(
 | 
			
		||||
                &since
 | 
			
		||||
                    .format(&time::format_description::well_known::Rfc3339)
 | 
			
		||||
                    .unwrap(),
 | 
			
		||||
            );
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(before) = &self.before {
 | 
			
		||||
            s.push_str("before=");
 | 
			
		||||
            s.push_str(
 | 
			
		||||
                &before
 | 
			
		||||
                    .format(&time::format_description::well_known::Rfc3339)
 | 
			
		||||
                    .unwrap(),
 | 
			
		||||
            );
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(page) = self.page {
 | 
			
		||||
            s.push_str("page=");
 | 
			
		||||
            s.write_fmt(format_args!("{page}"))
 | 
			
		||||
                .expect("writing to a string never fails");
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(limit) = self.limit {
 | 
			
		||||
            s.push_str("limit=");
 | 
			
		||||
            s.write_fmt(format_args!("{limit}"))
 | 
			
		||||
                .expect("writing to a string never fails");
 | 
			
		||||
        }
 | 
			
		||||
        s
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Copy)]
 | 
			
		||||
pub enum NotificationSubjectType {
 | 
			
		||||
    Issue,
 | 
			
		||||
    Pull,
 | 
			
		||||
    Commit,
 | 
			
		||||
    Repository,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl NotificationSubjectType {
 | 
			
		||||
    fn as_str(&self) -> &'static str {
 | 
			
		||||
        match self {
 | 
			
		||||
            NotificationSubjectType::Issue => "issue",
 | 
			
		||||
            NotificationSubjectType::Pull => "pull",
 | 
			
		||||
            NotificationSubjectType::Commit => "commit",
 | 
			
		||||
            NotificationSubjectType::Repository => "repository",
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct NotificationThread {
 | 
			
		||||
    pub id: u64,
 | 
			
		||||
    pub pinned: bool,
 | 
			
		||||
    pub repository: Repository,
 | 
			
		||||
    pub subject: NotificationSubject,
 | 
			
		||||
    pub unread: bool,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339")]
 | 
			
		||||
    pub updated_at: time::OffsetDateTime,
 | 
			
		||||
    pub url: Url,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct NotificationSubject {
 | 
			
		||||
    pub html_url: Url,
 | 
			
		||||
    pub latest_comment_html_url: Url,
 | 
			
		||||
    pub latest_comment_url: Url,
 | 
			
		||||
    pub state: String,
 | 
			
		||||
    pub title: String,
 | 
			
		||||
    #[serde(rename = "type")]
 | 
			
		||||
    pub _type: String,
 | 
			
		||||
    pub url: Url,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub struct NotificationPutQuery {
 | 
			
		||||
    pub last_read_at: Option<time::OffsetDateTime>,
 | 
			
		||||
    pub all: bool,
 | 
			
		||||
    pub include_unread: bool,
 | 
			
		||||
    pub include_read: bool,
 | 
			
		||||
    pub include_pinned: bool,
 | 
			
		||||
    pub to_status: ToStatus,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Default for NotificationPutQuery {
 | 
			
		||||
    fn default() -> Self {
 | 
			
		||||
        NotificationPutQuery {
 | 
			
		||||
            last_read_at: None,
 | 
			
		||||
            all: false,
 | 
			
		||||
            include_unread: true,
 | 
			
		||||
            include_read: false,
 | 
			
		||||
            include_pinned: false,
 | 
			
		||||
            to_status: ToStatus::default(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl NotificationPutQuery {
 | 
			
		||||
    fn query_string(&self) -> String {
 | 
			
		||||
        let mut s = String::new();
 | 
			
		||||
        if let Some(last_read_at) = &self.last_read_at {
 | 
			
		||||
            s.push_str("since=");
 | 
			
		||||
            s.push_str(
 | 
			
		||||
                &last_read_at
 | 
			
		||||
                    .format(&time::format_description::well_known::Rfc3339)
 | 
			
		||||
                    .unwrap(),
 | 
			
		||||
            );
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        if self.all {
 | 
			
		||||
            s.push_str("all=true&");
 | 
			
		||||
        }
 | 
			
		||||
        if self.include_unread {
 | 
			
		||||
            s.push_str("status-types=unread&");
 | 
			
		||||
        }
 | 
			
		||||
        if self.include_read {
 | 
			
		||||
            s.push_str("status-types=read&");
 | 
			
		||||
        }
 | 
			
		||||
        if self.include_pinned {
 | 
			
		||||
            s.push_str("status-types=pinned&");
 | 
			
		||||
        }
 | 
			
		||||
        s.push_str("subject-type=");
 | 
			
		||||
        s.push_str(self.to_status.as_str());
 | 
			
		||||
        s
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Default, Debug)]
 | 
			
		||||
pub enum ToStatus {
 | 
			
		||||
    #[default]
 | 
			
		||||
    Read,
 | 
			
		||||
    Unread,
 | 
			
		||||
    Pinned,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ToStatus {
 | 
			
		||||
    fn as_str(&self) -> &'static str {
 | 
			
		||||
        match self {
 | 
			
		||||
            ToStatus::Read => "read",
 | 
			
		||||
            ToStatus::Unread => "unread",
 | 
			
		||||
            ToStatus::Pinned => "pinned",
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct NotificationCount {
 | 
			
		||||
    pub new: u64,
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,30 +0,0 @@
 | 
			
		|||
use crate::*;
 | 
			
		||||
use std::collections::BTreeMap;
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct Organization {
 | 
			
		||||
    #[serde(deserialize_with = "crate::none_if_blank_url")]
 | 
			
		||||
    pub avatar_url: Option<Url>,
 | 
			
		||||
    pub description: String,
 | 
			
		||||
    pub full_name: String,
 | 
			
		||||
    pub id: u64,
 | 
			
		||||
    pub location: Option<String>,
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub repo_admin_change_team_access: bool,
 | 
			
		||||
    pub visibility: String,
 | 
			
		||||
    #[serde(deserialize_with = "crate::none_if_blank_url")]
 | 
			
		||||
    pub website: Option<Url>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct Team {
 | 
			
		||||
    pub can_create_org_repo: bool,
 | 
			
		||||
    pub description: String,
 | 
			
		||||
    pub id: u64,
 | 
			
		||||
    pub includes_all_repositories: bool,
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub organization: Organization,
 | 
			
		||||
    pub permission: String,
 | 
			
		||||
    pub units: Vec<String>,
 | 
			
		||||
    pub units_map: BTreeMap<String, String>,
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										174
									
								
								src/package.rs
									
										
									
									
									
								
							
							
						
						
									
										174
									
								
								src/package.rs
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,174 +0,0 @@
 | 
			
		|||
use std::fmt::Write;
 | 
			
		||||
 | 
			
		||||
use super::*;
 | 
			
		||||
 | 
			
		||||
impl Forgejo {
 | 
			
		||||
    pub async fn get_user_packages(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        query: PackagesQuery,
 | 
			
		||||
    ) -> Result<Vec<Package>, ForgejoError> {
 | 
			
		||||
        self.get(&query.path(owner)).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn get_package(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        _type: PackageType,
 | 
			
		||||
        name: &str,
 | 
			
		||||
        version: &str,
 | 
			
		||||
    ) -> Result<Option<Package>, ForgejoError> {
 | 
			
		||||
        self.get_opt(&format!(
 | 
			
		||||
            "packages/{owner}/{}/{name}/{version}",
 | 
			
		||||
            _type.as_str()
 | 
			
		||||
        ))
 | 
			
		||||
        .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn delete_package(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        _type: PackageType,
 | 
			
		||||
        name: &str,
 | 
			
		||||
        version: &str,
 | 
			
		||||
    ) -> Result<(), ForgejoError> {
 | 
			
		||||
        self.delete(&format!(
 | 
			
		||||
            "packages/{owner}/{}/{name}/{version}",
 | 
			
		||||
            _type.as_str()
 | 
			
		||||
        ))
 | 
			
		||||
        .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn get_package_files(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        _type: PackageType,
 | 
			
		||||
        name: &str,
 | 
			
		||||
        version: &str,
 | 
			
		||||
    ) -> Result<Vec<PackageFile>, ForgejoError> {
 | 
			
		||||
        self.get(&format!(
 | 
			
		||||
            "packages/{owner}/{}/{name}/{version}",
 | 
			
		||||
            _type.as_str()
 | 
			
		||||
        ))
 | 
			
		||||
        .await
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Default, Debug)]
 | 
			
		||||
pub struct PackagesQuery {
 | 
			
		||||
    pub page: Option<u32>,
 | 
			
		||||
    pub limit: Option<u32>,
 | 
			
		||||
    pub kind: Option<PackageType>,
 | 
			
		||||
    pub query: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl PackagesQuery {
 | 
			
		||||
    fn path(&self, owner: &str) -> String {
 | 
			
		||||
        let mut s = String::from("packages/");
 | 
			
		||||
        s.push_str(owner);
 | 
			
		||||
        s.push('?');
 | 
			
		||||
        if let Some(page) = self.page {
 | 
			
		||||
            s.push_str("page=");
 | 
			
		||||
            s.write_fmt(format_args!("{page}"))
 | 
			
		||||
                .expect("writing to string can't fail");
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(limit) = self.limit {
 | 
			
		||||
            s.push_str("limit=");
 | 
			
		||||
            s.write_fmt(format_args!("{limit}"))
 | 
			
		||||
                .expect("writing to string can't fail");
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(kind) = self.kind {
 | 
			
		||||
            s.push_str("type=");
 | 
			
		||||
            s.push_str(kind.as_str());
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        if !self.query.is_empty() {
 | 
			
		||||
            s.push_str("q=");
 | 
			
		||||
            s.push_str(&self.query);
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        s
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq, Eq, Clone, Copy)]
 | 
			
		||||
#[serde(rename_all = "lowercase")]
 | 
			
		||||
#[non_exhaustive]
 | 
			
		||||
pub enum PackageType {
 | 
			
		||||
    Alpine,
 | 
			
		||||
    Cargo,
 | 
			
		||||
    Chef,
 | 
			
		||||
    Composer,
 | 
			
		||||
    Conan,
 | 
			
		||||
    Conda,
 | 
			
		||||
    Container,
 | 
			
		||||
    Cran,
 | 
			
		||||
    Debian,
 | 
			
		||||
    Generic,
 | 
			
		||||
    Go,
 | 
			
		||||
    Helm,
 | 
			
		||||
    Maven,
 | 
			
		||||
    Npm,
 | 
			
		||||
    Nuget,
 | 
			
		||||
    Pub,
 | 
			
		||||
    Pypi,
 | 
			
		||||
    Rpm,
 | 
			
		||||
    RubyGems,
 | 
			
		||||
    Swift,
 | 
			
		||||
    Vagrant,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl PackageType {
 | 
			
		||||
    fn as_str(&self) -> &'static str {
 | 
			
		||||
        match self {
 | 
			
		||||
            PackageType::Alpine => "alpine",
 | 
			
		||||
            PackageType::Cargo => "cargo",
 | 
			
		||||
            PackageType::Chef => "chef",
 | 
			
		||||
            PackageType::Composer => "composer",
 | 
			
		||||
            PackageType::Conan => "conan",
 | 
			
		||||
            PackageType::Conda => "conda",
 | 
			
		||||
            PackageType::Container => "container",
 | 
			
		||||
            PackageType::Cran => "cran",
 | 
			
		||||
            PackageType::Debian => "debian",
 | 
			
		||||
            PackageType::Generic => "generic",
 | 
			
		||||
            PackageType::Go => "go",
 | 
			
		||||
            PackageType::Helm => "helm",
 | 
			
		||||
            PackageType::Maven => "maven",
 | 
			
		||||
            PackageType::Npm => "npm",
 | 
			
		||||
            PackageType::Nuget => "nuget",
 | 
			
		||||
            PackageType::Pub => "pub",
 | 
			
		||||
            PackageType::Pypi => "pypi",
 | 
			
		||||
            PackageType::Rpm => "rpm",
 | 
			
		||||
            PackageType::RubyGems => "rubygems",
 | 
			
		||||
            PackageType::Swift => "swift",
 | 
			
		||||
            PackageType::Vagrant => "vagrant",
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct Package {
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339")]
 | 
			
		||||
    pub created_at: time::OffsetDateTime,
 | 
			
		||||
    pub creator: User,
 | 
			
		||||
    pub id: u64,
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub owner: User,
 | 
			
		||||
    pub repository: Option<Repository>,
 | 
			
		||||
    pub _type: PackageType,
 | 
			
		||||
    pub version: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct PackageFile {
 | 
			
		||||
    #[serde(rename = "Size")]
 | 
			
		||||
    pub size: u64,
 | 
			
		||||
    pub id: u64,
 | 
			
		||||
    pub md5: String,
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub sha1: String,
 | 
			
		||||
    pub sha256: String,
 | 
			
		||||
    pub sha512: String,
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,743 +0,0 @@
 | 
			
		|||
use super::*;
 | 
			
		||||
 | 
			
		||||
/// Repository operations.
 | 
			
		||||
impl Forgejo {
 | 
			
		||||
    /// Gets info about the specified repository.
 | 
			
		||||
    pub async fn get_repo(
 | 
			
		||||
        &self,
 | 
			
		||||
        user: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
    ) -> Result<Option<Repository>, ForgejoError> {
 | 
			
		||||
        self.get_opt(&format!("repos/{user}/{repo}/")).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Creates a repository.
 | 
			
		||||
    pub async fn create_repo(&self, repo: CreateRepoOption) -> Result<Repository, ForgejoError> {
 | 
			
		||||
        self.post("user/repos", &repo).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn get_pulls(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        query: PullQuery,
 | 
			
		||||
    ) -> Result<Vec<PullRequest>, ForgejoError> {
 | 
			
		||||
        self.get(&query.to_string(owner, repo)).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn create_pr(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        opts: CreatePullRequestOption,
 | 
			
		||||
    ) -> Result<PullRequest, ForgejoError> {
 | 
			
		||||
        self.post(&format!("repos/{owner}/{repo}/pulls"), &opts)
 | 
			
		||||
            .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn is_merged(&self, owner: &str, repo: &str, pr: u64) -> Result<bool, ForgejoError> {
 | 
			
		||||
        self.get_exists(&format!("repos/{owner}/{repo}/pulls/{pr}/merge"))
 | 
			
		||||
            .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn merge_pr(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        pr: u64,
 | 
			
		||||
        opts: MergePullRequestOption,
 | 
			
		||||
    ) -> Result<(), ForgejoError> {
 | 
			
		||||
        self.post_unit(&format!("repos/{owner}/{repo}/pulls/{pr}/merge"), &opts)
 | 
			
		||||
            .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn cancel_merge(&self, owner: &str, repo: &str, pr: u64) -> Result<(), ForgejoError> {
 | 
			
		||||
        self.delete(&format!("repos/{owner}/{repo}/pulls/{pr}/merge"))
 | 
			
		||||
            .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn get_releases(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        query: ReleaseQuery,
 | 
			
		||||
    ) -> Result<Vec<Release>, ForgejoError> {
 | 
			
		||||
        self.get(&query.to_string(owner, repo)).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn get_release(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        id: u64,
 | 
			
		||||
    ) -> Result<Option<Release>, ForgejoError> {
 | 
			
		||||
        self.get_opt(&format!("repos/{owner}/{repo}/releases/{id}"))
 | 
			
		||||
            .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn get_release_by_tag(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        tag: &str,
 | 
			
		||||
    ) -> Result<Option<Release>, ForgejoError> {
 | 
			
		||||
        self.get_opt(&format!("repos/{owner}/{repo}/releases/tags/{tag}"))
 | 
			
		||||
            .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn delete_release(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        id: u64,
 | 
			
		||||
    ) -> Result<(), ForgejoError> {
 | 
			
		||||
        self.delete(&format!("repos/{owner}/{repo}/releases/{id}"))
 | 
			
		||||
            .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn delete_release_by_tag(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        tag: &str,
 | 
			
		||||
    ) -> Result<(), ForgejoError> {
 | 
			
		||||
        self.delete(&format!("repos/{owner}/{repo}/releases/tags/{tag}"))
 | 
			
		||||
            .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn edit_release(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        id: u64,
 | 
			
		||||
        opts: EditReleaseOption,
 | 
			
		||||
    ) -> Result<Release, ForgejoError> {
 | 
			
		||||
        self.patch(&format!("repos/{owner}/{repo}/releases/{id}"), &opts)
 | 
			
		||||
            .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn get_release_attachments(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        id: u64,
 | 
			
		||||
    ) -> Result<Vec<Attachment>, ForgejoError> {
 | 
			
		||||
        self.get(&format!("repos/{owner}/{repo}/releases/{id}/assets"))
 | 
			
		||||
            .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn get_release_attachment(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        release_id: u64,
 | 
			
		||||
        attachment_id: u64,
 | 
			
		||||
    ) -> Result<Attachment, ForgejoError> {
 | 
			
		||||
        self.get(&format!(
 | 
			
		||||
            "repos/{owner}/{repo}/releases/{release_id}/assets/{attachment_id}"
 | 
			
		||||
        ))
 | 
			
		||||
        .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn create_release_attachment(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        id: u64,
 | 
			
		||||
        name: &str,
 | 
			
		||||
        file: Vec<u8>,
 | 
			
		||||
    ) -> Result<Attachment, ForgejoError> {
 | 
			
		||||
        self.post_multipart(
 | 
			
		||||
            &format!("repos/{owner}/{repo}/releases/{id}/assets?name={name}"),
 | 
			
		||||
            reqwest::multipart::Form::new().part(
 | 
			
		||||
                "attachment",
 | 
			
		||||
                reqwest::multipart::Part::bytes(file)
 | 
			
		||||
                    .file_name("file")
 | 
			
		||||
                    .mime_str("*/*")
 | 
			
		||||
                    .unwrap(),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn delete_release_attachment(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        release_id: u64,
 | 
			
		||||
        attachment_id: u64,
 | 
			
		||||
    ) -> Result<(), ForgejoError> {
 | 
			
		||||
        self.delete(&format!(
 | 
			
		||||
            "repos/{owner}/{repo}/releases/{release_id}/assets/{attachment_id}"
 | 
			
		||||
        ))
 | 
			
		||||
        .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn edit_release_attachment(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        release_id: u64,
 | 
			
		||||
        attachment_id: u64,
 | 
			
		||||
        opts: EditAttachmentOption,
 | 
			
		||||
    ) -> Result<Attachment, ForgejoError> {
 | 
			
		||||
        self.patch(
 | 
			
		||||
            &format!("repos/{owner}/{repo}/releases/{release_id}/assets/{attachment_id}"),
 | 
			
		||||
            &opts,
 | 
			
		||||
        )
 | 
			
		||||
        .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn create_release(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        opts: CreateReleaseOption,
 | 
			
		||||
    ) -> Result<Release, ForgejoError> {
 | 
			
		||||
        self.post(&format!("repos/{owner}/{repo}/releases"), &opts)
 | 
			
		||||
            .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn latest_release(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
    ) -> Result<Option<Release>, ForgejoError> {
 | 
			
		||||
        self.get_opt(&format!("repos/{owner}/{repo}/releases/latest"))
 | 
			
		||||
            .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn download_zip_archive(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        target: &str,
 | 
			
		||||
    ) -> Result<Option<bytes::Bytes>, ForgejoError> {
 | 
			
		||||
        let request = self
 | 
			
		||||
            .client
 | 
			
		||||
            .get(
 | 
			
		||||
                self.url
 | 
			
		||||
                    .join(&format!("api/v1/repos/{owner}/{repo}/archive/{target}.zip"))
 | 
			
		||||
                    .unwrap(),
 | 
			
		||||
            )
 | 
			
		||||
            .build()?;
 | 
			
		||||
        self.execute_opt_raw(request).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn download_tarball_archive(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        target: &str,
 | 
			
		||||
    ) -> Result<Option<bytes::Bytes>, ForgejoError> {
 | 
			
		||||
        let request = self
 | 
			
		||||
            .client
 | 
			
		||||
            .get(
 | 
			
		||||
                self.url
 | 
			
		||||
                    .join(&format!(
 | 
			
		||||
                        "api/v1/repos/{owner}/{repo}/archive/{target}.tar.gz"
 | 
			
		||||
                    ))
 | 
			
		||||
                    .unwrap(),
 | 
			
		||||
            )
 | 
			
		||||
            .build()?;
 | 
			
		||||
        self.execute_opt_raw(request).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn download_release_attachment(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        release: u64,
 | 
			
		||||
        attach: u64,
 | 
			
		||||
    ) -> Result<Option<bytes::Bytes>, ForgejoError> {
 | 
			
		||||
        let release = self
 | 
			
		||||
            .get_release_attachment(owner, repo, release, attach)
 | 
			
		||||
            .await?;
 | 
			
		||||
        let request = self.client.get(release.browser_download_url).build()?;
 | 
			
		||||
        self.execute_opt_raw(request).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn get_tags(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        query: TagQuery,
 | 
			
		||||
    ) -> Result<Vec<Tag>, ForgejoError> {
 | 
			
		||||
        self.get(&query.to_string(owner, repo)).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn create_tag(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        opts: CreateTagOption,
 | 
			
		||||
    ) -> Result<Tag, ForgejoError> {
 | 
			
		||||
        self.post(&format!("repos/{owner}/{repo}/tags"), &opts)
 | 
			
		||||
            .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn get_tag(
 | 
			
		||||
        &self,
 | 
			
		||||
        owner: &str,
 | 
			
		||||
        repo: &str,
 | 
			
		||||
        tag: &str,
 | 
			
		||||
    ) -> Result<Option<Tag>, ForgejoError> {
 | 
			
		||||
        self.get_opt(&format!("repos/{owner}/{repo}/tags/{tag}"))
 | 
			
		||||
            .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn delete_tag(&self, owner: &str, repo: &str, tag: &str) -> Result<(), ForgejoError> {
 | 
			
		||||
        self.delete(&format!("repos/{owner}/{repo}/tags/{tag}"))
 | 
			
		||||
            .await
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct Repository {
 | 
			
		||||
    pub allow_merge_commits: bool,
 | 
			
		||||
    pub allow_rebase: bool,
 | 
			
		||||
    pub allow_rebase_explicit: bool,
 | 
			
		||||
    pub allow_rebase_update: bool,
 | 
			
		||||
    pub allow_squash_merge: bool,
 | 
			
		||||
    pub archived: bool,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339::option")]
 | 
			
		||||
    pub archived_at: Option<time::OffsetDateTime>,
 | 
			
		||||
    #[serde(deserialize_with = "crate::none_if_blank_url")]
 | 
			
		||||
    pub avatar_url: Option<Url>,
 | 
			
		||||
    pub clone_url: Url,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339")]
 | 
			
		||||
    pub created_at: time::OffsetDateTime,
 | 
			
		||||
    pub default_allow_maintainer_edit: bool,
 | 
			
		||||
    pub default_branch: String,
 | 
			
		||||
    pub default_delete_branch_after_merge: bool,
 | 
			
		||||
    pub default_merge_style: String,
 | 
			
		||||
    pub description: String,
 | 
			
		||||
    pub empty: bool,
 | 
			
		||||
    pub external_tracker: Option<ExternalTracker>,
 | 
			
		||||
    pub external_wiki: Option<ExternalWiki>,
 | 
			
		||||
    pub fork: bool,
 | 
			
		||||
    pub forks_count: u64,
 | 
			
		||||
    pub full_name: String,
 | 
			
		||||
    pub has_actions: bool,
 | 
			
		||||
    pub has_issues: bool,
 | 
			
		||||
    pub has_packages: bool,
 | 
			
		||||
    pub has_projects: bool,
 | 
			
		||||
    pub has_pull_requests: bool,
 | 
			
		||||
    pub has_releases: bool,
 | 
			
		||||
    pub has_wiki: bool,
 | 
			
		||||
    pub html_url: Url,
 | 
			
		||||
    pub id: u64,
 | 
			
		||||
    pub ignore_whitespace_conflicts: bool,
 | 
			
		||||
    pub internal: bool,
 | 
			
		||||
    pub internal_tracker: Option<InternalTracker>,
 | 
			
		||||
    pub language: String,
 | 
			
		||||
    pub languages_url: Url,
 | 
			
		||||
    pub link: String,
 | 
			
		||||
    pub mirror: bool,
 | 
			
		||||
    pub mirror_interval: Option<String>,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339::option")]
 | 
			
		||||
    pub mirror_updated: Option<time::OffsetDateTime>,
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub open_issues_count: u64,
 | 
			
		||||
    pub open_pr_counter: u64,
 | 
			
		||||
    #[serde(deserialize_with = "crate::none_if_blank_url")]
 | 
			
		||||
    pub original_url: Option<Url>,
 | 
			
		||||
    pub owner: User,
 | 
			
		||||
    pub parent: Option<Box<Repository>>,
 | 
			
		||||
    pub permissions: Permission,
 | 
			
		||||
    pub private: bool,
 | 
			
		||||
    pub release_counter: u64,
 | 
			
		||||
    pub repo_transfer: Option<RepoTransfer>,
 | 
			
		||||
    pub size: u64,
 | 
			
		||||
    pub ssh_url: String,
 | 
			
		||||
    pub stars_count: u64,
 | 
			
		||||
    pub template: bool,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339")]
 | 
			
		||||
    pub updated_at: time::OffsetDateTime,
 | 
			
		||||
    pub url: Url,
 | 
			
		||||
    pub watchers_count: u64,
 | 
			
		||||
    pub website: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct RepositoryMeta {
 | 
			
		||||
    pub full_name: String,
 | 
			
		||||
    pub id: u64,
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub owner: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, Debug, PartialEq)]
 | 
			
		||||
pub struct CreateRepoOption {
 | 
			
		||||
    pub auto_init: bool,
 | 
			
		||||
    pub default_branch: String,
 | 
			
		||||
    pub description: Option<String>,
 | 
			
		||||
    pub gitignores: String,
 | 
			
		||||
    pub issue_labels: String,
 | 
			
		||||
    pub license: String,
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub private: bool,
 | 
			
		||||
    pub readme: String,
 | 
			
		||||
    pub template: bool,
 | 
			
		||||
    pub trust_model: TrustModel,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, Debug, PartialEq)]
 | 
			
		||||
pub enum TrustModel {
 | 
			
		||||
    Default,
 | 
			
		||||
    Collaborator,
 | 
			
		||||
    Committer,
 | 
			
		||||
    #[serde(rename = "collaboratorcommiter")]
 | 
			
		||||
    CollaboratorCommitter,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct Milestone {
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339::option")]
 | 
			
		||||
    pub closed_at: Option<time::OffsetDateTime>,
 | 
			
		||||
    pub closed_issues: u64,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339")]
 | 
			
		||||
    pub created_at: time::OffsetDateTime,
 | 
			
		||||
    pub description: String,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339::option")]
 | 
			
		||||
    pub due_on: Option<time::OffsetDateTime>,
 | 
			
		||||
    pub id: u64,
 | 
			
		||||
    pub open_issues: u64,
 | 
			
		||||
    pub state: State,
 | 
			
		||||
    pub title: String,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339")]
 | 
			
		||||
    pub updated_at: time::OffsetDateTime,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct PullRequest {
 | 
			
		||||
    pub allow_maintainer_edit: bool,
 | 
			
		||||
    pub assignee: User,
 | 
			
		||||
    pub assignees: Vec<User>,
 | 
			
		||||
    pub base: PrBranchInfo,
 | 
			
		||||
    pub body: String,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339::option")]
 | 
			
		||||
    pub closed_at: Option<time::OffsetDateTime>,
 | 
			
		||||
    pub comments: u64,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339")]
 | 
			
		||||
    pub created_at: time::OffsetDateTime,
 | 
			
		||||
    pub diff_url: Url,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339::option")]
 | 
			
		||||
    pub due_date: Option<time::OffsetDateTime>,
 | 
			
		||||
    pub head: PrBranchInfo,
 | 
			
		||||
    pub html_url: Url,
 | 
			
		||||
    pub id: u64,
 | 
			
		||||
    pub is_locked: bool,
 | 
			
		||||
    pub labels: Vec<Label>,
 | 
			
		||||
    pub merge_base: String,
 | 
			
		||||
    pub merge_commit_sha: Option<String>,
 | 
			
		||||
    pub mergeable: bool,
 | 
			
		||||
    pub merged: bool,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339::option")]
 | 
			
		||||
    pub merged_at: Option<time::OffsetDateTime>,
 | 
			
		||||
    pub merged_by: Option<User>,
 | 
			
		||||
    pub milestone: Option<Milestone>,
 | 
			
		||||
    pub number: u64,
 | 
			
		||||
    pub patch_url: Url,
 | 
			
		||||
    pub pin_order: u64,
 | 
			
		||||
    pub requested_reviewers: Option<Vec<User>>,
 | 
			
		||||
    pub state: State,
 | 
			
		||||
    pub title: String,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339")]
 | 
			
		||||
    pub updated_at: time::OffsetDateTime,
 | 
			
		||||
    pub url: Url,
 | 
			
		||||
    pub user: User,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct PrBranchInfo {
 | 
			
		||||
    pub label: String,
 | 
			
		||||
    #[serde(rename = "ref")]
 | 
			
		||||
    pub _ref: String,
 | 
			
		||||
    pub repo: Repository,
 | 
			
		||||
    pub repo_id: u64,
 | 
			
		||||
    pub sha: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct PullRequestMeta {
 | 
			
		||||
    pub merged: bool,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339::option")]
 | 
			
		||||
    pub merged_at: Option<time::OffsetDateTime>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub struct PullQuery {
 | 
			
		||||
    pub state: Option<State>,
 | 
			
		||||
    pub sort: Option<PullQuerySort>,
 | 
			
		||||
    pub milestone: Option<u64>,
 | 
			
		||||
    pub labels: Vec<u64>,
 | 
			
		||||
    pub page: Option<u32>,
 | 
			
		||||
    pub limit: Option<u32>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl PullQuery {
 | 
			
		||||
    fn to_string(&self, owner: &str, repo: &str) -> String {
 | 
			
		||||
        use std::fmt::Write;
 | 
			
		||||
        // This is different to other query struct serialization because
 | 
			
		||||
        // `labels` is serialized so strangely
 | 
			
		||||
        let mut s = String::new();
 | 
			
		||||
        s.push_str("repos/");
 | 
			
		||||
        s.push_str(owner);
 | 
			
		||||
        s.push('/');
 | 
			
		||||
        s.push_str(repo);
 | 
			
		||||
        s.push_str("/pulls?");
 | 
			
		||||
        if let Some(state) = self.state {
 | 
			
		||||
            s.push_str("state=");
 | 
			
		||||
            s.push_str(state.as_str());
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(sort) = self.sort {
 | 
			
		||||
            s.push_str("sort=");
 | 
			
		||||
            s.push_str(sort.as_str());
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(milestone) = self.milestone {
 | 
			
		||||
            s.push_str("sort=");
 | 
			
		||||
            s.write_fmt(format_args!("{milestone}"))
 | 
			
		||||
                .expect("writing to a string never fails");
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        for label in &self.labels {
 | 
			
		||||
            s.push_str("labels=");
 | 
			
		||||
            s.write_fmt(format_args!("{label}"))
 | 
			
		||||
                .expect("writing to a string never fails");
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(page) = self.page {
 | 
			
		||||
            s.push_str("page=");
 | 
			
		||||
            s.write_fmt(format_args!("{page}"))
 | 
			
		||||
                .expect("writing to a string never fails");
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(limit) = self.limit {
 | 
			
		||||
            s.push_str("limit=");
 | 
			
		||||
            s.write_fmt(format_args!("{limit}"))
 | 
			
		||||
                .expect("writing to a string never fails");
 | 
			
		||||
            s.push('&');
 | 
			
		||||
        }
 | 
			
		||||
        s
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Copy, Debug)]
 | 
			
		||||
pub enum PullQuerySort {
 | 
			
		||||
    Oldest,
 | 
			
		||||
    RecentUpdate,
 | 
			
		||||
    LeastUpdate,
 | 
			
		||||
    MostComment,
 | 
			
		||||
    LeastComment,
 | 
			
		||||
    Priority,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl PullQuerySort {
 | 
			
		||||
    fn as_str(&self) -> &'static str {
 | 
			
		||||
        match self {
 | 
			
		||||
            PullQuerySort::Oldest => "oldest",
 | 
			
		||||
            PullQuerySort::RecentUpdate => "recentupdate",
 | 
			
		||||
            PullQuerySort::LeastUpdate => "leastupdate",
 | 
			
		||||
            PullQuerySort::MostComment => "mostcomment",
 | 
			
		||||
            PullQuerySort::LeastComment => "leastcomment",
 | 
			
		||||
            PullQuerySort::Priority => "priority",
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, Debug, PartialEq, Default)]
 | 
			
		||||
pub struct CreatePullRequestOption {
 | 
			
		||||
    pub assignee: Option<String>,
 | 
			
		||||
    pub assignees: Vec<String>,
 | 
			
		||||
    pub base: String,
 | 
			
		||||
    pub body: String,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339::option")]
 | 
			
		||||
    pub due_date: Option<time::OffsetDateTime>,
 | 
			
		||||
    pub head: String,
 | 
			
		||||
    pub labels: Vec<u64>,
 | 
			
		||||
    pub milestone: Option<u64>,
 | 
			
		||||
    pub title: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, Debug, PartialEq, Default)]
 | 
			
		||||
pub struct MergePullRequestOption {
 | 
			
		||||
    #[serde(rename = "Do")]
 | 
			
		||||
    pub act: MergePrAction,
 | 
			
		||||
    #[serde(rename = "MergeCommitId")]
 | 
			
		||||
    pub merge_commit_id: Option<String>,
 | 
			
		||||
    #[serde(rename = "MergeMessageField")]
 | 
			
		||||
    pub merge_message_field: Option<String>,
 | 
			
		||||
    #[serde(rename = "MergeTitleField")]
 | 
			
		||||
    pub merge_title_field: Option<String>,
 | 
			
		||||
    pub delete_branch_after_merge: Option<bool>,
 | 
			
		||||
    pub force_merge: Option<bool>,
 | 
			
		||||
    pub head_commit_id: Option<String>,
 | 
			
		||||
    pub merge_when_checks_succeed: Option<bool>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, Debug, PartialEq, Default)]
 | 
			
		||||
pub enum MergePrAction {
 | 
			
		||||
    #[serde(rename = "merge")]
 | 
			
		||||
    #[default]
 | 
			
		||||
    Merge,
 | 
			
		||||
    #[serde(rename = "rebase")]
 | 
			
		||||
    Rebase,
 | 
			
		||||
    #[serde(rename = "rebase-merge")]
 | 
			
		||||
    RebaseMerge,
 | 
			
		||||
    #[serde(rename = "squash")]
 | 
			
		||||
    Squash,
 | 
			
		||||
    #[serde(rename = "manually-merged")]
 | 
			
		||||
    ManuallyMerged,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct Release {
 | 
			
		||||
    pub assets: Vec<Attachment>,
 | 
			
		||||
    pub author: User,
 | 
			
		||||
    pub body: String,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339")]
 | 
			
		||||
    pub created_at: time::OffsetDateTime,
 | 
			
		||||
    pub draft: bool,
 | 
			
		||||
    pub html_url: Url,
 | 
			
		||||
    pub id: u64,
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub prerelease: bool,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339")]
 | 
			
		||||
    pub published_at: time::OffsetDateTime,
 | 
			
		||||
    pub tag_name: String,
 | 
			
		||||
    pub tarball_url: Url,
 | 
			
		||||
    pub target_commitish: String,
 | 
			
		||||
    pub url: Url,
 | 
			
		||||
    pub zipball_url: Url,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, Debug, PartialEq, Default)]
 | 
			
		||||
pub struct CreateReleaseOption {
 | 
			
		||||
    pub body: String,
 | 
			
		||||
    pub draft: bool,
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub prerelease: bool,
 | 
			
		||||
    pub tag_name: String,
 | 
			
		||||
    pub target_commitish: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, Debug, PartialEq, Default)]
 | 
			
		||||
pub struct EditReleaseOption {
 | 
			
		||||
    pub body: Option<String>,
 | 
			
		||||
    pub draft: Option<bool>,
 | 
			
		||||
    pub name: Option<String>,
 | 
			
		||||
    pub prerelease: Option<bool>,
 | 
			
		||||
    pub tag_name: Option<String>,
 | 
			
		||||
    pub target_commitish: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Default, Debug)]
 | 
			
		||||
pub struct ReleaseQuery {
 | 
			
		||||
    pub draft: Option<bool>,
 | 
			
		||||
    pub prerelease: Option<bool>,
 | 
			
		||||
    pub page: Option<u32>,
 | 
			
		||||
    pub limit: Option<u32>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ReleaseQuery {
 | 
			
		||||
    fn to_string(&self, owner: &str, repo: &str) -> String {
 | 
			
		||||
        format!(
 | 
			
		||||
            "repos/{owner}/{repo}/releases?draft={}&pre-release={}&page={}&limit={}",
 | 
			
		||||
            opt_bool_s(self.draft),
 | 
			
		||||
            opt_bool_s(self.prerelease),
 | 
			
		||||
            self.page.map(|page| page.to_string()).unwrap_or_default(),
 | 
			
		||||
            self.limit.map(|page| page.to_string()).unwrap_or_default(),
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn opt_bool_s(b: Option<bool>) -> &'static str {
 | 
			
		||||
    match b {
 | 
			
		||||
        Some(true) => "true",
 | 
			
		||||
        Some(false) => "false",
 | 
			
		||||
        None => "",
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct Tag {
 | 
			
		||||
    pub commit: CommitMeta,
 | 
			
		||||
    pub id: String,
 | 
			
		||||
    pub message: String,
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub tarball_url: Url,
 | 
			
		||||
    pub zipball_url: Url,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, Debug, PartialEq, Default)]
 | 
			
		||||
pub struct CreateTagOption {
 | 
			
		||||
    pub message: Option<String>,
 | 
			
		||||
    pub tag_name: String,
 | 
			
		||||
    pub target: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Default, Debug)]
 | 
			
		||||
pub struct TagQuery {
 | 
			
		||||
    pub page: Option<u32>,
 | 
			
		||||
    pub limit: Option<u32>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl TagQuery {
 | 
			
		||||
    fn to_string(&self, owner: &str, repo: &str) -> String {
 | 
			
		||||
        format!(
 | 
			
		||||
            "repos/{owner}/{repo}/tags?page={}&limit={}",
 | 
			
		||||
            self.page.map(|page| page.to_string()).unwrap_or_default(),
 | 
			
		||||
            self.limit.map(|page| page.to_string()).unwrap_or_default(),
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct CommitMeta {
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339")]
 | 
			
		||||
    pub created: time::OffsetDateTime,
 | 
			
		||||
    pub url: Url,
 | 
			
		||||
    pub sha: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct ExternalTracker {
 | 
			
		||||
    #[serde(rename = "external_tracker_format")]
 | 
			
		||||
    pub format: String,
 | 
			
		||||
    #[serde(rename = "external_tracker_regexp_pattern")]
 | 
			
		||||
    pub regexp_pattern: String,
 | 
			
		||||
    #[serde(rename = "external_tracker_style")]
 | 
			
		||||
    pub style: String,
 | 
			
		||||
    #[serde(rename = "external_tracker_url")]
 | 
			
		||||
    pub url: Url,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct InternalTracker {
 | 
			
		||||
    pub allow_only_contributors_to_track_time: bool,
 | 
			
		||||
    pub enable_issue_dependencies: bool,
 | 
			
		||||
    pub enable_time_tracker: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct ExternalWiki {
 | 
			
		||||
    #[serde(rename = "external_wiki_url")]
 | 
			
		||||
    pub url: Url,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct Permission {
 | 
			
		||||
    pub admin: bool,
 | 
			
		||||
    pub pull: bool,
 | 
			
		||||
    pub push: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct RepoTransfer {
 | 
			
		||||
    pub doer: User,
 | 
			
		||||
    pub recipient: User,
 | 
			
		||||
    pub teams: Vec<Team>,
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										59
									
								
								src/user.rs
									
										
									
									
									
								
							
							
						
						
									
										59
									
								
								src/user.rs
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,59 +0,0 @@
 | 
			
		|||
use super::*;
 | 
			
		||||
 | 
			
		||||
/// User operations.
 | 
			
		||||
impl Forgejo {
 | 
			
		||||
    /// Returns info about the authorized user.
 | 
			
		||||
    pub async fn myself(&self) -> Result<User, ForgejoError> {
 | 
			
		||||
        self.get("user").await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Returns info about the specified user.
 | 
			
		||||
    pub async fn get_user(&self, user: &str) -> Result<Option<User>, ForgejoError> {
 | 
			
		||||
        self.get_opt(&format!("users/{user}/")).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Gets the list of users that follow the specified user.
 | 
			
		||||
    pub async fn get_followers(&self, user: &str) -> Result<Option<Vec<User>>, ForgejoError> {
 | 
			
		||||
        self.get_opt(&format!("users/{user}/followers/")).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Gets the list of users the specified user is following.
 | 
			
		||||
    pub async fn get_following(&self, user: &str) -> Result<Option<Vec<User>>, ForgejoError> {
 | 
			
		||||
        self.get_opt(&format!("users/{user}/following/")).await
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub struct User {
 | 
			
		||||
    pub active: bool,
 | 
			
		||||
    pub avatar_url: Url,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339")]
 | 
			
		||||
    pub created: time::OffsetDateTime,
 | 
			
		||||
    pub description: String,
 | 
			
		||||
    pub email: String,
 | 
			
		||||
    pub followers_count: u64,
 | 
			
		||||
    pub following_count: u64,
 | 
			
		||||
    pub full_name: String,
 | 
			
		||||
    pub id: u64,
 | 
			
		||||
    pub is_admin: bool,
 | 
			
		||||
    pub language: String,
 | 
			
		||||
    #[serde(with = "time::serde::rfc3339")]
 | 
			
		||||
    pub last_login: time::OffsetDateTime,
 | 
			
		||||
    pub location: String,
 | 
			
		||||
    pub login: String,
 | 
			
		||||
    pub login_name: String,
 | 
			
		||||
    pub prohibit_login: bool,
 | 
			
		||||
    pub restricted: bool,
 | 
			
		||||
    pub starred_repos_count: u64,
 | 
			
		||||
    pub website: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug, PartialEq)]
 | 
			
		||||
pub enum UserVisibility {
 | 
			
		||||
    #[serde(rename = "public")]
 | 
			
		||||
    Public,
 | 
			
		||||
    #[serde(rename = "limited")]
 | 
			
		||||
    Limited,
 | 
			
		||||
    #[serde(rename = "private")]
 | 
			
		||||
    Private,
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										24321
									
								
								swagger.v1.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24321
									
								
								swagger.v1.json
									
										
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										507
									
								
								tests/ci_test.rs
									
										
									
									
									
								
							
							
						
						
									
										507
									
								
								tests/ci_test.rs
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,60 +1,50 @@
 | 
			
		|||
use eyre::{ensure, eyre, WrapErr};
 | 
			
		||||
use forgejo_api::Forgejo;
 | 
			
		||||
use forgejo_api::{structs::*, Forgejo};
 | 
			
		||||
 | 
			
		||||
#[tokio::test]
 | 
			
		||||
async fn ci() -> eyre::Result<()> {
 | 
			
		||||
    let url = url::Url::parse(&std::env::var("FORGEJO_API_CI_INSTANCE_URL")?)?;
 | 
			
		||||
    let token = std::env::var("FORGEJO_API_CI_TOKEN")?;
 | 
			
		||||
    let api = Forgejo::new(forgejo_api::Auth::Token(&token), url)?;
 | 
			
		||||
 | 
			
		||||
    let mut results = Vec::new();
 | 
			
		||||
 | 
			
		||||
    results.push(user(&api).await.wrap_err("user error"));
 | 
			
		||||
    results.push(repo(&api).await.wrap_err("repo error"));
 | 
			
		||||
    results.push(admin(&api).await.wrap_err("admin error"));
 | 
			
		||||
 | 
			
		||||
    let mut errors = 0;
 | 
			
		||||
    for report in results.into_iter().filter_map(Result::err) {
 | 
			
		||||
        errors += 1;
 | 
			
		||||
        for (i, err) in report.chain().enumerate() {
 | 
			
		||||
            println!("{i}. {err}");
 | 
			
		||||
            if let Some(err) = err.downcast_ref::<forgejo_api::ForgejoError>() {
 | 
			
		||||
                if let forgejo_api::ForgejoError::BadStructure(_, body) = err {
 | 
			
		||||
                    println!("BODY: {body}");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if errors > 0 {
 | 
			
		||||
        eyre::bail!("test failed");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
fn get_api() -> Forgejo {
 | 
			
		||||
    let url = url::Url::parse(&std::env::var("FORGEJO_API_CI_INSTANCE_URL").unwrap()).unwrap();
 | 
			
		||||
    let token = std::env::var("FORGEJO_API_CI_TOKEN").unwrap();
 | 
			
		||||
    Forgejo::new(forgejo_api::Auth::Token(&token), url).unwrap()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn user(api: &forgejo_api::Forgejo) -> eyre::Result<()> {
 | 
			
		||||
    let myself = api.myself().await?;
 | 
			
		||||
    ensure!(myself.is_admin, "user should be admin");
 | 
			
		||||
    ensure!(
 | 
			
		||||
        myself.login == "TestingAdmin",
 | 
			
		||||
#[tokio::test]
 | 
			
		||||
async fn user() {
 | 
			
		||||
    let api = get_api();
 | 
			
		||||
 | 
			
		||||
    let myself = api.user_get_current().await.unwrap();
 | 
			
		||||
    assert!(myself.is_admin.unwrap(), "user should be admin");
 | 
			
		||||
    assert_eq!(
 | 
			
		||||
        myself.login.as_ref().unwrap(),
 | 
			
		||||
        "TestingAdmin",
 | 
			
		||||
        "user should be named \"TestingAdmin\""
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    let myself_indirect = api
 | 
			
		||||
        .get_user("TestingAdmin")
 | 
			
		||||
        .await?
 | 
			
		||||
        .ok_or_else(|| eyre!("\"TestingAdmin\" not found, but should have been."))?;
 | 
			
		||||
    ensure!(
 | 
			
		||||
        myself == myself_indirect,
 | 
			
		||||
    let myself_indirect = api.user_get("TestingAdmin").await.unwrap();
 | 
			
		||||
    assert_eq!(
 | 
			
		||||
        myself, myself_indirect,
 | 
			
		||||
        "result of `myself` does not match result of `get_user`"
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    let following = api.get_following("TestingAdmin").await?;
 | 
			
		||||
    ensure!(following == Some(Vec::new()), "following list not empty");
 | 
			
		||||
    let followers = api.get_followers("TestingAdmin").await?;
 | 
			
		||||
    ensure!(followers == Some(Vec::new()), "follower list not empty");
 | 
			
		||||
    let query = UserListFollowingQuery {
 | 
			
		||||
        page: None,
 | 
			
		||||
        limit: None,
 | 
			
		||||
    };
 | 
			
		||||
    let following = api
 | 
			
		||||
        .user_list_following("TestingAdmin", query)
 | 
			
		||||
        .await
 | 
			
		||||
        .unwrap();
 | 
			
		||||
    assert_eq!(following, Vec::new(), "following list not empty");
 | 
			
		||||
 | 
			
		||||
    let url = url::Url::parse(&std::env::var("FORGEJO_API_CI_INSTANCE_URL")?)?;
 | 
			
		||||
    let query = UserListFollowersQuery {
 | 
			
		||||
        page: None,
 | 
			
		||||
        limit: None,
 | 
			
		||||
    };
 | 
			
		||||
    let followers = api
 | 
			
		||||
        .user_list_followers("TestingAdmin", query)
 | 
			
		||||
        .await
 | 
			
		||||
        .unwrap();
 | 
			
		||||
    assert_eq!(followers, Vec::new(), "follower list not empty");
 | 
			
		||||
 | 
			
		||||
    let url = url::Url::parse(&std::env::var("FORGEJO_API_CI_INSTANCE_URL").unwrap()).unwrap();
 | 
			
		||||
    let password_api = Forgejo::new(
 | 
			
		||||
        forgejo_api::Auth::Password {
 | 
			
		||||
            username: "TestingAdmin",
 | 
			
		||||
| 
						 | 
				
			
			@ -63,103 +53,133 @@ async fn user(api: &forgejo_api::Forgejo) -> eyre::Result<()> {
 | 
			
		|||
        },
 | 
			
		||||
        url,
 | 
			
		||||
    )
 | 
			
		||||
    .wrap_err("failed to log in using username and password")?;
 | 
			
		||||
    .expect("failed to log in using username and password");
 | 
			
		||||
 | 
			
		||||
    ensure!(
 | 
			
		||||
        api.myself().await? == password_api.myself().await?,
 | 
			
		||||
    assert!(
 | 
			
		||||
        api.user_get_current().await.unwrap() == password_api.user_get_current().await.unwrap(),
 | 
			
		||||
        "users not equal comparing token-auth and pass-auth"
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn repo(api: &forgejo_api::Forgejo) -> eyre::Result<()> {
 | 
			
		||||
    tokio::fs::create_dir("/test_repo").await?;
 | 
			
		||||
#[tokio::test]
 | 
			
		||||
async fn repo() {
 | 
			
		||||
    let api = get_api();
 | 
			
		||||
 | 
			
		||||
    tokio::fs::create_dir("./test_repo").await.unwrap();
 | 
			
		||||
    let git = || {
 | 
			
		||||
        let mut cmd = std::process::Command::new("git");
 | 
			
		||||
        cmd.current_dir("/test_repo");
 | 
			
		||||
        cmd.current_dir("./test_repo");
 | 
			
		||||
        cmd
 | 
			
		||||
    };
 | 
			
		||||
    let _ = git()
 | 
			
		||||
        .args(["config", "--global", "init.defaultBranch", "main"])
 | 
			
		||||
        .status()?;
 | 
			
		||||
    let _ = git().args(["init"]).status()?;
 | 
			
		||||
        .status()
 | 
			
		||||
        .unwrap();
 | 
			
		||||
    let _ = git().args(["init"]).status().unwrap();
 | 
			
		||||
    let _ = git()
 | 
			
		||||
        .args(["config", "user.name", "TestingAdmin"])
 | 
			
		||||
        .status()?;
 | 
			
		||||
        .status()
 | 
			
		||||
        .unwrap();
 | 
			
		||||
    let _ = git()
 | 
			
		||||
        .args(["config", "user.email", "admin@noreply.example.org"])
 | 
			
		||||
        .status()?;
 | 
			
		||||
    tokio::fs::write("/test_repo/README.md", "# Test\nThis is a test repo").await?;
 | 
			
		||||
    let _ = git().args(["add", "."]).status()?;
 | 
			
		||||
    let _ = git().args(["commit", "-m", "initial commit"]).status()?;
 | 
			
		||||
        .status()
 | 
			
		||||
        .unwrap();
 | 
			
		||||
    tokio::fs::write("./test_repo/README.md", "# Test\nThis is a test repo")
 | 
			
		||||
        .await
 | 
			
		||||
        .unwrap();
 | 
			
		||||
    let _ = git().args(["add", "."]).status().unwrap();
 | 
			
		||||
    let _ = git()
 | 
			
		||||
        .args(["commit", "-m", "initial commit"])
 | 
			
		||||
        .status()
 | 
			
		||||
        .unwrap();
 | 
			
		||||
 | 
			
		||||
    let repo_opt = forgejo_api::CreateRepoOption {
 | 
			
		||||
        auto_init: false,
 | 
			
		||||
        default_branch: "main".into(),
 | 
			
		||||
    let repo_opt = CreateRepoOption {
 | 
			
		||||
        auto_init: Some(false),
 | 
			
		||||
        default_branch: Some("main".into()),
 | 
			
		||||
        description: Some("Test Repo".into()),
 | 
			
		||||
        gitignores: "".into(),
 | 
			
		||||
        issue_labels: "".into(),
 | 
			
		||||
        license: "".into(),
 | 
			
		||||
        gitignores: Some("".into()),
 | 
			
		||||
        issue_labels: Some("".into()),
 | 
			
		||||
        license: Some("".into()),
 | 
			
		||||
        name: "test".into(),
 | 
			
		||||
        private: false,
 | 
			
		||||
        readme: "".into(),
 | 
			
		||||
        template: false,
 | 
			
		||||
        trust_model: forgejo_api::TrustModel::Default,
 | 
			
		||||
        private: Some(false),
 | 
			
		||||
        readme: None,
 | 
			
		||||
        template: Some(false),
 | 
			
		||||
        trust_model: Some(CreateRepoOptionTrustModel::Default),
 | 
			
		||||
    };
 | 
			
		||||
    let remote_repo = api.create_repo(repo_opt).await?;
 | 
			
		||||
    ensure!(
 | 
			
		||||
        remote_repo.has_pull_requests,
 | 
			
		||||
    let remote_repo = api.create_current_user_repo(repo_opt).await.unwrap();
 | 
			
		||||
    assert!(
 | 
			
		||||
        remote_repo.has_pull_requests.unwrap(),
 | 
			
		||||
        "repo does not accept pull requests"
 | 
			
		||||
    );
 | 
			
		||||
    ensure!(
 | 
			
		||||
        remote_repo.owner.login == "TestingAdmin",
 | 
			
		||||
    assert!(
 | 
			
		||||
        remote_repo.owner.as_ref().unwrap().login.as_ref().unwrap() == "TestingAdmin",
 | 
			
		||||
        "repo owner is not \"TestingAdmin\""
 | 
			
		||||
    );
 | 
			
		||||
    ensure!(remote_repo.name == "test", "repo owner is not \"test\"");
 | 
			
		||||
    assert!(
 | 
			
		||||
        remote_repo.name.as_ref().unwrap() == "test",
 | 
			
		||||
        "repo owner is not \"test\""
 | 
			
		||||
    );
 | 
			
		||||
    tokio::time::sleep(std::time::Duration::from_secs(3)).await;
 | 
			
		||||
 | 
			
		||||
    let mut remote_url = remote_repo.clone_url.clone();
 | 
			
		||||
    let mut remote_url = remote_repo.clone_url.clone().unwrap();
 | 
			
		||||
    remote_url.set_username("TestingAdmin").unwrap();
 | 
			
		||||
    remote_url.set_password(Some("password")).unwrap();
 | 
			
		||||
    let _ = git()
 | 
			
		||||
        .args(["remote", "add", "origin", remote_url.as_str()])
 | 
			
		||||
        .status()?;
 | 
			
		||||
    let _ = git().args(["push", "-u", "origin", "main"]).status()?;
 | 
			
		||||
        .status()
 | 
			
		||||
        .unwrap();
 | 
			
		||||
    let _ = git()
 | 
			
		||||
        .args(["push", "-u", "origin", "main"])
 | 
			
		||||
        .status()
 | 
			
		||||
        .unwrap();
 | 
			
		||||
 | 
			
		||||
    let _ = git().args(["switch", "-c", "test"]).status()?;
 | 
			
		||||
    let _ = git().args(["switch", "-c", "test"]).status().unwrap();
 | 
			
		||||
    tokio::fs::write(
 | 
			
		||||
        "/test_repo/example.rs",
 | 
			
		||||
        "./test_repo/example.rs",
 | 
			
		||||
        "fn add_one(x: u32) -> u32 { x + 1 }",
 | 
			
		||||
    )
 | 
			
		||||
    .await?;
 | 
			
		||||
    let _ = git().args(["add", "."]).status()?;
 | 
			
		||||
    let _ = git().args(["commit", "-m", "egg"]).status()?;
 | 
			
		||||
    let _ = git().args(["push", "-u", "origin", "test"]).status()?;
 | 
			
		||||
    .await
 | 
			
		||||
    .unwrap();
 | 
			
		||||
    let _ = git().args(["add", "."]).status().unwrap();
 | 
			
		||||
    let _ = git().args(["commit", "-m", "egg"]).status().unwrap();
 | 
			
		||||
    let _ = git()
 | 
			
		||||
        .args(["push", "-u", "origin", "test"])
 | 
			
		||||
        .status()
 | 
			
		||||
        .unwrap();
 | 
			
		||||
 | 
			
		||||
    let pr_opt = forgejo_api::CreatePullRequestOption {
 | 
			
		||||
    let pr_opt = CreatePullRequestOption {
 | 
			
		||||
        assignee: None,
 | 
			
		||||
        assignees: vec!["TestingAdmin".into()],
 | 
			
		||||
        base: "main".into(),
 | 
			
		||||
        body: "This is a test PR".into(),
 | 
			
		||||
        assignees: Some(vec!["TestingAdmin".into()]),
 | 
			
		||||
        base: Some("main".into()),
 | 
			
		||||
        body: Some("This is a test PR".into()),
 | 
			
		||||
        due_date: None,
 | 
			
		||||
        head: "test".into(),
 | 
			
		||||
        labels: Vec::new(),
 | 
			
		||||
        head: Some("test".into()),
 | 
			
		||||
        labels: None,
 | 
			
		||||
        milestone: None,
 | 
			
		||||
        title: "test pr".into(),
 | 
			
		||||
        title: Some("test pr".into()),
 | 
			
		||||
    };
 | 
			
		||||
    let pr = api
 | 
			
		||||
        .create_pr("TestingAdmin", "test", pr_opt)
 | 
			
		||||
        .repo_create_pull_request("TestingAdmin", "test", pr_opt)
 | 
			
		||||
        .await
 | 
			
		||||
        .wrap_err("couldn't create pr")?;
 | 
			
		||||
        .expect("couldn't create pr");
 | 
			
		||||
    tokio::time::sleep(std::time::Duration::from_secs(3)).await;
 | 
			
		||||
    let is_merged = api
 | 
			
		||||
        .is_merged("TestingAdmin", "test", pr.number)
 | 
			
		||||
        .repo_pull_request_is_merged("TestingAdmin", "test", pr.number.unwrap())
 | 
			
		||||
        .await
 | 
			
		||||
        .wrap_err_with(|| eyre!("couldn't find unmerged pr {}", pr.number))?;
 | 
			
		||||
    ensure!(!is_merged, "pr should not yet be merged");
 | 
			
		||||
    let merge_opt = forgejo_api::MergePullRequestOption {
 | 
			
		||||
        act: forgejo_api::MergePrAction::Merge,
 | 
			
		||||
        .is_ok();
 | 
			
		||||
    assert!(!is_merged, "pr should not yet be merged");
 | 
			
		||||
    let pr_files_query = RepoGetPullRequestFilesQuery {
 | 
			
		||||
        skip_to: None,
 | 
			
		||||
        whitespace: None,
 | 
			
		||||
        page: None,
 | 
			
		||||
        limit: None,
 | 
			
		||||
    };
 | 
			
		||||
    let (_, _) = api
 | 
			
		||||
        .repo_get_pull_request_files("TestingAdmin", "test", pr.number.unwrap(), pr_files_query)
 | 
			
		||||
        .await
 | 
			
		||||
        .unwrap();
 | 
			
		||||
    let merge_opt = MergePullRequestOption {
 | 
			
		||||
        r#do: MergePullRequestOptionDo::Merge,
 | 
			
		||||
        merge_commit_id: None,
 | 
			
		||||
        merge_message_field: None,
 | 
			
		||||
        merge_title_field: None,
 | 
			
		||||
| 
						 | 
				
			
			@ -168,230 +188,279 @@ async fn repo(api: &forgejo_api::Forgejo) -> eyre::Result<()> {
 | 
			
		|||
        head_commit_id: None,
 | 
			
		||||
        merge_when_checks_succeed: None,
 | 
			
		||||
    };
 | 
			
		||||
    api.merge_pr("TestingAdmin", "test", pr.number, merge_opt)
 | 
			
		||||
    api.repo_merge_pull_request("TestingAdmin", "test", pr.number.unwrap(), merge_opt)
 | 
			
		||||
        .await
 | 
			
		||||
        .wrap_err_with(|| eyre!("couldn't merge pr {}", pr.number))?;
 | 
			
		||||
        .expect("couldn't merge pr");
 | 
			
		||||
    let is_merged = api
 | 
			
		||||
        .is_merged("TestingAdmin", "test", pr.number)
 | 
			
		||||
        .repo_pull_request_is_merged("TestingAdmin", "test", pr.number.unwrap())
 | 
			
		||||
        .await
 | 
			
		||||
        .wrap_err_with(|| eyre!("couldn't find merged pr {}", pr.number))?;
 | 
			
		||||
    ensure!(is_merged, "pr should be merged");
 | 
			
		||||
    let _ = git().args(["fetch"]).status()?;
 | 
			
		||||
    let _ = git().args(["pull"]).status()?;
 | 
			
		||||
        .is_ok();
 | 
			
		||||
    assert!(is_merged, "pr should be merged");
 | 
			
		||||
    let _ = git().args(["fetch"]).status().unwrap();
 | 
			
		||||
    let _ = git().args(["pull"]).status().unwrap();
 | 
			
		||||
 | 
			
		||||
    ensure!(
 | 
			
		||||
        api.get_releases("TestingAdmin", "test", forgejo_api::ReleaseQuery::default())
 | 
			
		||||
    let query = RepoListReleasesQuery {
 | 
			
		||||
        draft: None,
 | 
			
		||||
        pre_release: None,
 | 
			
		||||
        per_page: None,
 | 
			
		||||
        page: None,
 | 
			
		||||
        limit: None,
 | 
			
		||||
    };
 | 
			
		||||
    assert!(
 | 
			
		||||
        api.repo_list_releases("TestingAdmin", "test", query)
 | 
			
		||||
            .await
 | 
			
		||||
            .wrap_err("releases list not found")?
 | 
			
		||||
            .unwrap()
 | 
			
		||||
            .is_empty(),
 | 
			
		||||
        "there should be no releases yet"
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    let tag_opt = forgejo_api::CreateTagOption {
 | 
			
		||||
    let tag_opt = CreateTagOption {
 | 
			
		||||
        message: Some("This is a tag!".into()),
 | 
			
		||||
        tag_name: "v1.0".into(),
 | 
			
		||||
        target: None,
 | 
			
		||||
    };
 | 
			
		||||
    api.create_tag("TestingAdmin", "test", tag_opt)
 | 
			
		||||
    api.repo_create_tag("TestingAdmin", "test", tag_opt)
 | 
			
		||||
        .await
 | 
			
		||||
        .wrap_err("failed to create tag")?;
 | 
			
		||||
        .expect("failed to create tag");
 | 
			
		||||
 | 
			
		||||
    let release_opt = forgejo_api::CreateReleaseOption {
 | 
			
		||||
        body: "This is a release!".into(),
 | 
			
		||||
        draft: true,
 | 
			
		||||
        name: "v1.0".into(),
 | 
			
		||||
        prerelease: false,
 | 
			
		||||
    let release_opt = CreateReleaseOption {
 | 
			
		||||
        body: Some("This is a release!".into()),
 | 
			
		||||
        draft: Some(true),
 | 
			
		||||
        name: Some("v1.0".into()),
 | 
			
		||||
        prerelease: Some(false),
 | 
			
		||||
        tag_name: "v1.0".into(),
 | 
			
		||||
        target_commitish: None,
 | 
			
		||||
    };
 | 
			
		||||
    let release = api
 | 
			
		||||
        .create_release("TestingAdmin", "test", release_opt)
 | 
			
		||||
        .repo_create_release("TestingAdmin", "test", release_opt)
 | 
			
		||||
        .await
 | 
			
		||||
        .wrap_err("failed to create release")?;
 | 
			
		||||
    let edit_release = forgejo_api::EditReleaseOption {
 | 
			
		||||
        .expect("failed to create release");
 | 
			
		||||
    let edit_release = EditReleaseOption {
 | 
			
		||||
        body: None,
 | 
			
		||||
        draft: Some(false),
 | 
			
		||||
        ..Default::default()
 | 
			
		||||
        name: None,
 | 
			
		||||
        prerelease: None,
 | 
			
		||||
        tag_name: None,
 | 
			
		||||
        target_commitish: None,
 | 
			
		||||
    };
 | 
			
		||||
    api.edit_release("TestingAdmin", "test", release.id, edit_release)
 | 
			
		||||
    api.repo_edit_release("TestingAdmin", "test", release.id.unwrap(), edit_release)
 | 
			
		||||
        .await
 | 
			
		||||
        .wrap_err("failed to edit release")?;
 | 
			
		||||
        .expect("failed to edit release");
 | 
			
		||||
 | 
			
		||||
    let release_by_tag = api
 | 
			
		||||
        .get_release_by_tag("TestingAdmin", "test", "v1.0")
 | 
			
		||||
        .repo_get_release_by_tag("TestingAdmin", "test", "v1.0")
 | 
			
		||||
        .await
 | 
			
		||||
        .wrap_err("failed to find release")?;
 | 
			
		||||
        .expect("failed to find release");
 | 
			
		||||
    let release_latest = api
 | 
			
		||||
        .latest_release("TestingAdmin", "test")
 | 
			
		||||
        .repo_get_latest_release("TestingAdmin", "test")
 | 
			
		||||
        .await
 | 
			
		||||
        .wrap_err("failed to find latest release")?;
 | 
			
		||||
    ensure!(release_by_tag == release_latest, "releases not equal");
 | 
			
		||||
        .expect("failed to find latest release");
 | 
			
		||||
    assert!(release_by_tag == release_latest, "releases not equal");
 | 
			
		||||
 | 
			
		||||
    let attachment = api
 | 
			
		||||
        .create_release_attachment(
 | 
			
		||||
        .repo_create_release_attachment(
 | 
			
		||||
            "TestingAdmin",
 | 
			
		||||
            "test",
 | 
			
		||||
            release.id,
 | 
			
		||||
            "test.txt",
 | 
			
		||||
            release.id.unwrap(),
 | 
			
		||||
            b"This is a file!".to_vec(),
 | 
			
		||||
            RepoCreateReleaseAttachmentQuery {
 | 
			
		||||
                name: Some("test.txt".into()),
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
        .await
 | 
			
		||||
        .wrap_err("failed to create release attachment")?;
 | 
			
		||||
    ensure!(
 | 
			
		||||
        api.download_release_attachment("TestingAdmin", "test", release.id, attachment.id)
 | 
			
		||||
            .await?
 | 
			
		||||
            .as_deref()
 | 
			
		||||
            == Some(b"This is a file!"),
 | 
			
		||||
        .expect("failed to create release attachment");
 | 
			
		||||
    assert!(
 | 
			
		||||
        &*api
 | 
			
		||||
            .download_release_attachment(
 | 
			
		||||
                "TestingAdmin",
 | 
			
		||||
                "test",
 | 
			
		||||
                release.id.unwrap(),
 | 
			
		||||
                attachment.id.unwrap()
 | 
			
		||||
            )
 | 
			
		||||
            .await
 | 
			
		||||
            .unwrap()
 | 
			
		||||
            == b"This is a file!",
 | 
			
		||||
        "couldn't download attachment"
 | 
			
		||||
    );
 | 
			
		||||
    ensure!(
 | 
			
		||||
        api.download_zip_archive("TestingAdmin", "test", "v1.0")
 | 
			
		||||
            .await?
 | 
			
		||||
            .is_some(),
 | 
			
		||||
        "couldn't download zip archive"
 | 
			
		||||
    );
 | 
			
		||||
    ensure!(
 | 
			
		||||
        api.download_tarball_archive("TestingAdmin", "test", "v1.0")
 | 
			
		||||
            .await?
 | 
			
		||||
            .is_some(),
 | 
			
		||||
        "couldn't download tape archive"
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    api.delete_release_attachment("TestingAdmin", "test", release.id, attachment.id)
 | 
			
		||||
    let _zip_archive = api
 | 
			
		||||
        .repo_get_archive("TestingAdmin", "test", "v1.0.zip")
 | 
			
		||||
        .await
 | 
			
		||||
        .wrap_err("failed to deleted attachment")?;
 | 
			
		||||
 | 
			
		||||
    api.delete_release("TestingAdmin", "test", release.id)
 | 
			
		||||
        .unwrap();
 | 
			
		||||
    let _tar_archive = api
 | 
			
		||||
        .repo_get_archive("TestingAdmin", "test", "v1.0.tar.gz")
 | 
			
		||||
        .await
 | 
			
		||||
        .wrap_err("failed to delete release")?;
 | 
			
		||||
        .unwrap();
 | 
			
		||||
    // check these contents when their return value is fixed
 | 
			
		||||
 | 
			
		||||
    api.delete_tag("TestingAdmin", "test", "v1.0")
 | 
			
		||||
    api.repo_delete_release_attachment(
 | 
			
		||||
        "TestingAdmin",
 | 
			
		||||
        "test",
 | 
			
		||||
        release.id.unwrap(),
 | 
			
		||||
        attachment.id.unwrap(),
 | 
			
		||||
    )
 | 
			
		||||
    .await
 | 
			
		||||
    .expect("failed to deleted attachment");
 | 
			
		||||
 | 
			
		||||
    api.repo_delete_release("TestingAdmin", "test", release.id.unwrap())
 | 
			
		||||
        .await
 | 
			
		||||
        .wrap_err("failed to delete release")?;
 | 
			
		||||
        .expect("failed to delete release");
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
    api.repo_delete_tag("TestingAdmin", "test", "v1.0")
 | 
			
		||||
        .await
 | 
			
		||||
        .expect("failed to delete release");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn admin(api: &forgejo_api::Forgejo) -> eyre::Result<()> {
 | 
			
		||||
    let user_opt = forgejo_api::CreateUserOption {
 | 
			
		||||
#[tokio::test]
 | 
			
		||||
async fn admin() {
 | 
			
		||||
    let api = get_api();
 | 
			
		||||
 | 
			
		||||
    let user_opt = CreateUserOption {
 | 
			
		||||
        created_at: None,
 | 
			
		||||
        email: "user@noreply.example.org".into(),
 | 
			
		||||
        email: "pipis@noreply.example.org".into(),
 | 
			
		||||
        full_name: None,
 | 
			
		||||
        login_name: None,
 | 
			
		||||
        must_change_password: false,
 | 
			
		||||
        password: "userpass".into(),
 | 
			
		||||
        restricted: false,
 | 
			
		||||
        send_notify: true,
 | 
			
		||||
        must_change_password: None,
 | 
			
		||||
        password: Some("userpass".into()),
 | 
			
		||||
        restricted: Some(false),
 | 
			
		||||
        send_notify: Some(true),
 | 
			
		||||
        source_id: None,
 | 
			
		||||
        username: "Pipis".into(),
 | 
			
		||||
        visibility: "public".into(),
 | 
			
		||||
        visibility: Some("public".into()),
 | 
			
		||||
    };
 | 
			
		||||
    let _ = api
 | 
			
		||||
        .admin_create_user(user_opt)
 | 
			
		||||
        .await
 | 
			
		||||
        .wrap_err("failed to create user")?;
 | 
			
		||||
        .expect("failed to create user");
 | 
			
		||||
 | 
			
		||||
    let query = AdminSearchUsersQuery {
 | 
			
		||||
        source_id: None,
 | 
			
		||||
        login_name: None,
 | 
			
		||||
        page: None,
 | 
			
		||||
        limit: None,
 | 
			
		||||
    };
 | 
			
		||||
    let users = api
 | 
			
		||||
        .admin_users(forgejo_api::AdminUserQuery::default())
 | 
			
		||||
        .admin_search_users(query)
 | 
			
		||||
        .await
 | 
			
		||||
        .wrap_err("failed to search users")?;
 | 
			
		||||
    ensure!(
 | 
			
		||||
        users.iter().find(|u| u.login == "Pipis").is_some(),
 | 
			
		||||
        "could not find new user"
 | 
			
		||||
    );
 | 
			
		||||
    let users = api
 | 
			
		||||
        .admin_get_emails(forgejo_api::EmailListQuery::default())
 | 
			
		||||
        .await
 | 
			
		||||
        .wrap_err("failed to search emails")?;
 | 
			
		||||
    ensure!(
 | 
			
		||||
        .expect("failed to search users");
 | 
			
		||||
    assert!(
 | 
			
		||||
        users
 | 
			
		||||
            .iter()
 | 
			
		||||
            .find(|u| u.email == "user@noreply.example.org")
 | 
			
		||||
            .find(|u| u.login.as_ref().unwrap() == "Pipis")
 | 
			
		||||
            .is_some(),
 | 
			
		||||
        "could not find new user"
 | 
			
		||||
    );
 | 
			
		||||
    let query = AdminGetAllEmailsQuery {
 | 
			
		||||
        page: None,
 | 
			
		||||
        limit: None,
 | 
			
		||||
    };
 | 
			
		||||
    let users = api
 | 
			
		||||
        .admin_get_all_emails(query)
 | 
			
		||||
        .await
 | 
			
		||||
        .expect("failed to search emails");
 | 
			
		||||
    assert!(
 | 
			
		||||
        users
 | 
			
		||||
            .iter()
 | 
			
		||||
            .find(|u| u.email.as_ref().unwrap() == "pipis@noreply.example.org")
 | 
			
		||||
            .is_some(),
 | 
			
		||||
        "could not find new user"
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    let org_opt = forgejo_api::CreateOrgOption {
 | 
			
		||||
    let org_opt = CreateOrgOption {
 | 
			
		||||
        description: None,
 | 
			
		||||
        email: None,
 | 
			
		||||
        full_name: None,
 | 
			
		||||
        location: None,
 | 
			
		||||
        repo_admin_change_team_access: None,
 | 
			
		||||
        username: "test-org".into(),
 | 
			
		||||
        visibility: forgejo_api::OrgVisibility::Public,
 | 
			
		||||
        visibility: Some(CreateOrgOptionVisibility::Public),
 | 
			
		||||
        website: None,
 | 
			
		||||
    };
 | 
			
		||||
    let _ = api
 | 
			
		||||
        .admin_create_org("Pipis", org_opt)
 | 
			
		||||
        .await
 | 
			
		||||
        .wrap_err("failed to create org")?;
 | 
			
		||||
    ensure!(
 | 
			
		||||
        !api.admin_get_orgs(forgejo_api::AdminOrganizationQuery::default())
 | 
			
		||||
            .await?
 | 
			
		||||
            .is_empty(),
 | 
			
		||||
        .expect("failed to create org");
 | 
			
		||||
    let query = AdminGetAllOrgsQuery {
 | 
			
		||||
        page: None,
 | 
			
		||||
        limit: None,
 | 
			
		||||
    };
 | 
			
		||||
    assert!(
 | 
			
		||||
        !api.admin_get_all_orgs(query).await.unwrap().is_empty(),
 | 
			
		||||
        "org list empty"
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    let key_opt = forgejo_api::CreateKeyOption {
 | 
			
		||||
    let key_opt = CreateKeyOption {
 | 
			
		||||
        key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN68ehQAsbGEwlXPa2AxbAh1QxFQrtRel2jeC0hRlPc1 user@noreply.example.org".into(),
 | 
			
		||||
        read_only: None,
 | 
			
		||||
        title: "Example Key".into(),
 | 
			
		||||
    };
 | 
			
		||||
    let key = api
 | 
			
		||||
        .admin_add_key("Pipis", key_opt)
 | 
			
		||||
        .admin_create_public_key("Pipis", key_opt)
 | 
			
		||||
        .await
 | 
			
		||||
        .wrap_err("failed to create key")?;
 | 
			
		||||
    api.admin_delete_key("Pipis", key.id)
 | 
			
		||||
        .expect("failed to create key");
 | 
			
		||||
    api.admin_delete_user_public_key("Pipis", key.id.unwrap())
 | 
			
		||||
        .await
 | 
			
		||||
        .wrap_err("failed to delete key")?;
 | 
			
		||||
        .expect("failed to delete key");
 | 
			
		||||
 | 
			
		||||
    let rename_opt = forgejo_api::RenameUserOption {
 | 
			
		||||
    let rename_opt = RenameUserOption {
 | 
			
		||||
        new_username: "Bepis".into(),
 | 
			
		||||
    };
 | 
			
		||||
    api.admin_rename_user("Pipis", rename_opt)
 | 
			
		||||
        .await
 | 
			
		||||
        .wrap_err("failed to rename user")?;
 | 
			
		||||
    api.admin_delete_user("Bepis", true)
 | 
			
		||||
        .expect("failed to rename user");
 | 
			
		||||
    let query = AdminDeleteUserQuery { purge: Some(true) };
 | 
			
		||||
    api.admin_delete_user("Bepis", query)
 | 
			
		||||
        .await
 | 
			
		||||
        .wrap_err("failed to delete user")?;
 | 
			
		||||
    ensure!(
 | 
			
		||||
        api.admin_delete_user("Ghost", true).await.is_err(),
 | 
			
		||||
        .expect("failed to delete user");
 | 
			
		||||
    let query = AdminDeleteUserQuery { purge: Some(true) };
 | 
			
		||||
    assert!(
 | 
			
		||||
        api.admin_delete_user("Ghost", query).await.is_err(),
 | 
			
		||||
        "deleting fake user should fail"
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    let query = AdminCronListQuery {
 | 
			
		||||
        page: None,
 | 
			
		||||
        limit: None,
 | 
			
		||||
    };
 | 
			
		||||
    let crons = api
 | 
			
		||||
        .admin_get_crons(forgejo_api::CronQuery::default())
 | 
			
		||||
        .admin_cron_list(query)
 | 
			
		||||
        .await
 | 
			
		||||
        .wrap_err("failed to get crons list")?;
 | 
			
		||||
    api.admin_run_cron(&crons.get(0).ok_or_else(|| eyre!("no crons"))?.name)
 | 
			
		||||
        .expect("failed to get crons list");
 | 
			
		||||
    api.admin_cron_run(&crons.get(0).expect("no crons").name.as_ref().unwrap())
 | 
			
		||||
        .await
 | 
			
		||||
        .wrap_err("failed to run cron")?;
 | 
			
		||||
        .expect("failed to run cron");
 | 
			
		||||
 | 
			
		||||
    let hook_opt = forgejo_api::CreateHookOption {
 | 
			
		||||
    let hook_opt = CreateHookOption {
 | 
			
		||||
        active: None,
 | 
			
		||||
        authorization_header: None,
 | 
			
		||||
        branch_filter: None,
 | 
			
		||||
        config: forgejo_api::CreateHookOptionConfig {
 | 
			
		||||
            content_type: "json".into(),
 | 
			
		||||
            url: url::Url::parse("http://test.local/").unwrap(),
 | 
			
		||||
            other: Default::default(),
 | 
			
		||||
        config: CreateHookOptionConfig {
 | 
			
		||||
            // content_type: "json".into(),
 | 
			
		||||
            // url: url::Url::parse("http://test.local/").unwrap(),
 | 
			
		||||
            additional: [
 | 
			
		||||
                ("content_type".into(), "json".into()),
 | 
			
		||||
                ("url".into(), "http://test.local/".into()),
 | 
			
		||||
            ]
 | 
			
		||||
            .into(),
 | 
			
		||||
        },
 | 
			
		||||
        events: Vec::new(),
 | 
			
		||||
        _type: forgejo_api::HookType::Forgejo,
 | 
			
		||||
        events: Some(Vec::new()),
 | 
			
		||||
        r#type: CreateHookOptionType::Gitea,
 | 
			
		||||
    };
 | 
			
		||||
    // yarr har har me matey this is me hook
 | 
			
		||||
    let hook = api
 | 
			
		||||
        .admin_create_hook(hook_opt)
 | 
			
		||||
        .await
 | 
			
		||||
        .wrap_err("failed to create hook")?;
 | 
			
		||||
    let edit_hook = forgejo_api::EditHookOption {
 | 
			
		||||
        .expect("failed to create hook");
 | 
			
		||||
    let edit_hook = EditHookOption {
 | 
			
		||||
        active: Some(true),
 | 
			
		||||
        ..Default::default()
 | 
			
		||||
        authorization_header: None,
 | 
			
		||||
        branch_filter: None,
 | 
			
		||||
        config: None,
 | 
			
		||||
        events: None,
 | 
			
		||||
    };
 | 
			
		||||
    api.admin_edit_hook(hook.id, edit_hook)
 | 
			
		||||
    api.admin_edit_hook(hook.id.unwrap(), edit_hook)
 | 
			
		||||
        .await
 | 
			
		||||
        .wrap_err("failed to edit hook")?;
 | 
			
		||||
    api.admin_delete_hook(hook.id)
 | 
			
		||||
        .expect("failed to edit hook");
 | 
			
		||||
    api.admin_delete_hook(hook.id.unwrap())
 | 
			
		||||
        .await
 | 
			
		||||
        .wrap_err("failed to delete hook")?;
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
        .expect("failed to delete hook");
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue