From f4b21c182fd9747e161d3c432f72bd79469b846b Mon Sep 17 00:00:00 2001 From: Origami404 Date: Wed, 8 Apr 2026 22:47:17 +0800 Subject: [PATCH] feat: add normalized document model and output writer --- Cargo.lock | 84 ++++++++++++++++++++++++++++++++++++++ Cargo.toml | 3 ++ src/error.rs | 11 +++++ src/lib.rs | 3 ++ src/model.rs | 73 +++++++++++++++++++++++++++++++++ src/output.rs | 14 +++++++ tests/render_json_tests.rs | 48 ++++++++++++++++++++++ 7 files changed, 236 insertions(+) create mode 100644 src/error.rs create mode 100644 src/model.rs create mode 100644 src/output.rs create mode 100644 tests/render_json_tests.rs diff --git a/Cargo.lock b/Cargo.lock index 622aa2d..9fd8dfb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,6 +110,9 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", + "serde", + "serde_json", + "thiserror", ] [[package]] @@ -124,6 +127,18 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + [[package]] name = "once_cell_polyfill" version = "1.70.2" @@ -148,6 +163,49 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "strsim" version = "0.11.1" @@ -165,6 +223,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "unicode-ident" version = "1.0.24" @@ -191,3 +269,9 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 0c0330c..a12c835 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,6 @@ edition = "2024" [dependencies] anyhow = "1.0" clap = { version = "4.5", features = ["derive"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "1.0" diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..97f39a3 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,11 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum AppError { + #[error("missing required environment variable: {0}")] + MissingEnv(&'static str), + #[error("api error: {0}")] + Api(String), + #[error("invalid input json: {0}")] + InvalidInputJson(String), +} diff --git a/src/lib.rs b/src/lib.rs index ecbf2c1..8c0f8ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,7 @@ pub mod cli; +pub mod error; +pub mod model; +pub mod output; pub fn run() -> anyhow::Result<()> { Ok(()) diff --git a/src/model.rs b/src/model.rs new file mode 100644 index 0000000..6b884b0 --- /dev/null +++ b/src/model.rs @@ -0,0 +1,73 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PrReviewDocument { + pub meta: PrMeta, + pub commits: Vec, + pub diff_stat: DiffStat, + pub reviews: Vec, + pub threads: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PrMeta { + pub repo: String, + pub pr_index: i64, + pub title: String, + pub state: String, + pub author: String, + pub base_branch: String, + pub head_branch: String, + pub created_at: String, + pub updated_at: String, + pub merged_at: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CommitItem { + pub sha: String, + pub short_sha: String, + pub title: String, + pub author: String, + pub date: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DiffStat { + pub files_changed: usize, + pub additions: i64, + pub deletions: i64, + pub files: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FileStat { + pub path: String, + pub additions: i64, + pub deletions: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ReviewItem { + pub id: i64, + pub state: String, + pub reviewer: String, + pub submitted_at: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CommentThread { + pub thread_id: String, + pub file_path: Option, + pub line: Option, + pub root_comment: CommentItem, + pub replies: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CommentItem { + pub id: i64, + pub user: String, + pub created_at: String, + pub body: String, +} diff --git a/src/output.rs b/src/output.rs new file mode 100644 index 0000000..380e880 --- /dev/null +++ b/src/output.rs @@ -0,0 +1,14 @@ +use std::io::Write; +use std::path::Path; + +pub fn write_output(out: Option<&Path>, content: &str) -> anyhow::Result<()> { + if let Some(path) = out { + std::fs::write(path, content)?; + return Ok(()); + } + + let mut stdout = std::io::stdout().lock(); + stdout.write_all(content.as_bytes())?; + stdout.flush()?; + Ok(()) +} diff --git a/tests/render_json_tests.rs b/tests/render_json_tests.rs new file mode 100644 index 0000000..9e77cea --- /dev/null +++ b/tests/render_json_tests.rs @@ -0,0 +1,48 @@ +use gitea_pr_review::model::{CommentItem, CommentThread, DiffStat, FileStat, PrMeta, PrReviewDocument}; + +#[test] +fn model_json_roundtrip() { + let doc = PrReviewDocument { + meta: PrMeta { + repo: "org/repo".into(), + pr_index: 9, + title: "feat: x".into(), + state: "open".into(), + author: "alice".into(), + base_branch: "main".into(), + head_branch: "feature/x".into(), + created_at: "2026-04-08T10:00:00Z".into(), + updated_at: "2026-04-08T11:00:00Z".into(), + merged_at: None, + }, + commits: vec![], + diff_stat: DiffStat { + files_changed: 1, + additions: 2, + deletions: 1, + files: vec![FileStat { + path: "src/main.rs".into(), + additions: 2, + deletions: 1, + }], + }, + reviews: vec![], + threads: vec![CommentThread { + thread_id: "t1".into(), + file_path: Some("src/main.rs".into()), + line: Some(10), + root_comment: CommentItem { + id: 100, + user: "reviewer".into(), + created_at: "2026-04-08T12:00:00Z".into(), + body: "hello".into(), + }, + replies: vec![], + }], + }; + + let encoded = serde_json::to_string(&doc).unwrap(); + let decoded: PrReviewDocument = serde_json::from_str(&encoded).unwrap(); + assert_eq!(decoded.meta.repo, "org/repo"); + assert_eq!(decoded.threads[0].root_comment.body, "hello"); +}