feat(status): add status type

This commit was merged in pull request #69.
This commit is contained in:
2026-09-17 14:42:32 -04:00
parent 37539682d4
commit 49cf043035
10 changed files with 188 additions and 14 deletions

12
Cargo.lock generated
View File

@@ -52,12 +52,6 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "anyhow"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]] [[package]]
name = "ariadne" name = "ariadne"
version = "0.1.0" version = "0.1.0"
@@ -213,6 +207,10 @@ dependencies = [
"zmij", "zmij",
] ]
[[package]]
name = "status"
version = "0.1.0"
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.11.1" version = "0.11.1"
@@ -261,9 +259,9 @@ dependencies = [
name = "xatu" name = "xatu"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow",
"clap", "clap",
"monorepo", "monorepo",
"status",
] ]
[[package]] [[package]]

View File

@@ -3,6 +3,7 @@ members = [
"src/ariadne", "src/ariadne",
"src/machoke", "src/machoke",
"src/monorepo", "src/monorepo",
"src/status",
"src/xatu", "src/xatu",
] ]
resolver = "3" resolver = "3"
@@ -12,4 +13,5 @@ anyhow = "1.0"
clap = "4" clap = "4"
monorepo = { path = "src/monorepo" } monorepo = { path = "src/monorepo" }
serde = "1.0" serde = "1.0"
status = { path = "src/status" }
serde_json = "1.0" serde_json = "1.0"

14
src/status/BUILD Normal file
View File

@@ -0,0 +1,14 @@
load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test")
rust_library(
name = "status",
crate_name = "status",
srcs = ["status.rs"],
visibility = ["//visibility:public"],
)
rust_test(
name = "status_test",
srcs = ["status_test.rs"],
deps = [":status"],
)

9
src/status/Cargo.toml Normal file
View File

@@ -0,0 +1,9 @@
[package]
name = "status"
version = "0.1.0"
edition = "2024"
[lib]
path = "status.rs"
[dependencies]

84
src/status/status.rs Normal file
View File

@@ -0,0 +1,84 @@
pub type Result<T> = std::result::Result<T, Status>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Code {
InvalidArgument,
NotFound,
PermissionDenied,
Internal,
}
#[derive(Debug)]
pub struct Status {
code: Code,
message: String,
source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
}
impl Status {
pub fn new(code: Code, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
source: None,
}
}
pub fn not_found(message: impl Into<String>) -> Self {
Self::new(Code::NotFound, message)
}
pub fn invalid_argument(message: impl Into<String>) -> Self {
Self::new(Code::InvalidArgument, message)
}
pub fn permission_denied(message: impl Into<String>) -> Self {
Self::new(Code::PermissionDenied, message)
}
pub fn internal(message: impl Into<String>) -> Self {
Self::new(Code::Internal, message)
}
pub fn with_source<E>(mut self, source: E) -> Self
where
E: std::error::Error + Send + Sync + 'static,
{
self.source = Some(Box::new(source));
self
}
pub fn code(&self) -> Code {
self.code
}
pub fn message(&self) -> &str {
&self.message
}
}
impl std::fmt::Display for Status {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "{:?}: {}", self.code, self.message)
}
}
impl std::error::Error for Status {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source
.as_deref()
.map(|source| source as &(dyn std::error::Error + 'static))
}
}
impl From<std::io::Error> for Status {
fn from(error: std::io::Error) -> Self {
let code = match error.kind() {
std::io::ErrorKind::NotFound => Code::NotFound,
std::io::ErrorKind::PermissionDenied => Code::PermissionDenied,
_ => Code::Internal,
};
Self::new(code, error.to_string()).with_source(error)
}
}

67
src/status/status_test.rs Normal file
View File

@@ -0,0 +1,67 @@
use std::error::Error;
use status::{Code, Status};
#[test]
fn constructors_set_code_and_message() {
let cases = [
(Status::invalid_argument("bad input"), Code::InvalidArgument),
(Status::not_found("missing input"), Code::NotFound),
(Status::permission_denied("denied"), Code::PermissionDenied),
(Status::internal("failed"), Code::Internal),
];
for (status, code) in cases {
assert_eq!(code, status.code());
assert!(!status.message().is_empty());
}
}
#[test]
fn display_includes_code_and_message() {
let status = Status::not_found("missing input");
assert_eq!("NotFound: missing input", status.to_string());
}
#[test]
fn with_source_preserves_source_error() {
let status = Status::internal("read failed").with_source(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"denied",
));
assert_eq!("denied", status.source().unwrap().to_string());
}
#[test]
fn io_error_maps_not_found() {
let status = Status::from(std::io::Error::new(
std::io::ErrorKind::NotFound,
"missing",
));
assert_eq!(Code::NotFound, status.code());
assert_eq!("missing", status.message());
assert_eq!("missing", status.source().unwrap().to_string());
}
#[test]
fn io_error_maps_permission_denied() {
let status = Status::from(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"denied",
));
assert_eq!(Code::PermissionDenied, status.code());
}
#[test]
fn io_error_maps_other_errors_to_internal() {
let status = Status::from(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"invalid data",
));
assert_eq!(Code::Internal, status.code());
}

View File

@@ -4,6 +4,7 @@ rust_library(
name = "xatu_lib", name = "xatu_lib",
crate_name = "xatu", crate_name = "xatu",
srcs = ["xatu.rs"], srcs = ["xatu.rs"],
deps = ["//src/status:status"],
) )
rust_binary( rust_binary(
@@ -13,6 +14,7 @@ rust_binary(
deps = [ deps = [
":xatu_lib", ":xatu_lib",
"//src/monorepo:monorepo", "//src/monorepo:monorepo",
"//src/status:status",
"//third_party/crates:clap", "//third_party/crates:clap",
], ],
) )

View File

@@ -11,6 +11,6 @@ name = "xatu"
path = "xatu_main.rs" path = "xatu_main.rs"
[dependencies] [dependencies]
anyhow = { workspace = true }
clap = { workspace = true, features = ["derive"] } clap = { workspace = true, features = ["derive"] }
monorepo = { workspace = true } monorepo = { workspace = true }
status = { workspace = true }

View File

@@ -1,14 +1,12 @@
use std::path::PathBuf; use std::path::PathBuf;
pub fn run(files: Vec<SourceFile>) -> Result<()> { pub fn run(files: Vec<SourceFile>) -> status::Result<()> {
for file in files { for file in files {
println!("{}", file.path.display()); println!("{}", file.path.display());
} }
Ok(()) Ok(())
} }
pub type Result<T> = std::result::Result<T, std::io::Error>;
#[derive(Debug)] #[derive(Debug)]
pub struct SourceFile { pub struct SourceFile {
pub path: PathBuf, pub path: PathBuf,

View File

@@ -25,7 +25,7 @@ fn is_excluded_dir(path: &Path) -> bool {
}) })
} }
fn collect_source_files(path: &Path) -> xatu::Result<Vec<SourceFile>> { fn collect_source_files(path: &Path) -> status::Result<Vec<SourceFile>> {
let metadata = std::fs::symlink_metadata(path)?; let metadata = std::fs::symlink_metadata(path)?;
if metadata.file_type().is_dir() { if metadata.file_type().is_dir() {
@@ -59,7 +59,7 @@ struct Args {
paths: Vec<PathBuf>, paths: Vec<PathBuf>,
} }
fn main() -> xatu::Result<()> { fn main() -> status::Result<()> {
let args = Args::parse(); let args = Args::parse();
let paths: Vec<PathBuf> = args let paths: Vec<PathBuf> = args
@@ -71,7 +71,7 @@ fn main() -> xatu::Result<()> {
let files: Vec<SourceFile> = paths let files: Vec<SourceFile> = paths
.iter() .iter()
.map(|path| collect_source_files(path)) .map(|path| collect_source_files(path))
.collect::<xatu::Result<Vec<_>>>()? .collect::<status::Result<Vec<_>>>()?
.into_iter() .into_iter()
.flatten() .flatten()
.collect(); .collect();