From dfdf001b59cef9f1a82533e038570cce681ba58d Mon Sep 17 00:00:00 2001 From: RAprogramm Date: Tue, 30 Dec 2025 09:41:09 +0700 Subject: [PATCH 1/5] refactor: apply cargo qual code quality rules - Remove 1400+ empty lines from function bodies - Move inline comments to doc blocks where appropriate - Simplify path imports using local use statements - Clean up test files by removing redundant step comments All tests pass, clippy clean. --- benches/comparison.rs | 21 --- benches/error_paths.rs | 8 -- build.rs | 25 +--- build/readme.rs | 23 ---- build/render.rs | 10 -- examples/axum-rest-api/src/lib.rs | 20 --- examples/axum-rest-api/src/main.rs | 10 +- .../axum-rest-api/tests/integration_tests.rs | 47 ------- examples/basic-async/src/main.rs | 31 +---- examples/basic_usage.rs | 17 --- examples/colored_cli.rs | 14 -- examples/custom-domain-errors/src/main.rs | 25 ---- examples/derive_error.rs | 9 -- examples/migrate_from_anyhow.rs | 4 - examples/migrate_from_thiserror.rs | 2 - examples/redaction.rs | 1 - examples/sqlx-database/src/main.rs | 43 ------ examples/structured_metadata.rs | 6 - masterror-derive/src/app_error_impl.rs | 12 -- masterror-derive/src/display/enum_impl.rs | 22 ---- masterror-derive/src/display/format_args.rs | 39 ------ masterror-derive/src/display/formatter.rs | 1 - masterror-derive/src/display/struct_impl.rs | 15 --- masterror-derive/src/display/template.rs | 22 ---- masterror-derive/src/error_trait.rs | 6 - masterror-derive/src/error_trait/backtrace.rs | 12 -- masterror-derive/src/error_trait/provide.rs | 42 ------ masterror-derive/src/error_trait/source.rs | 13 -- masterror-derive/src/from_impl.rs | 37 ------ masterror-derive/src/input/parse.rs | 11 -- masterror-derive/src/input/parse_attr.rs | 47 ------- masterror-derive/src/input/parse_format.rs | 20 --- masterror-derive/src/input/types.rs | 15 +-- masterror-derive/src/input/utils.rs | 33 +---- masterror-derive/src/lib.rs | 2 - masterror-derive/src/masterror_impl.rs | 4 - .../src/masterror_impl/attachment.rs | 21 --- .../src/masterror_impl/binding.rs | 15 --- .../src/masterror_impl/conversion.rs | 17 +-- .../src/masterror_impl/mapping.rs | 18 --- masterror-derive/src/span.rs | 27 +--- masterror-derive/src/template_support.rs | 15 +-- masterror-template/src/template.rs | 20 --- masterror-template/src/template/parser.rs | 43 ------ src/app_error/context.rs | 9 -- src/app_error/core.rs | 20 --- src/app_error/core/backtrace.rs | 11 -- src/app_error/core/builder.rs | 1 - src/app_error/core/display.rs | 123 +++--------------- src/app_error/core/error.rs | 6 - src/app_error/core/introspection.rs | 5 +- src/app_error/core/telemetry.rs | 6 - src/app_error/metadata.rs | 14 -- src/app_error/tests.rs | 88 +------------ src/code/app_code.rs | 19 +-- src/convert.rs | 6 - src/convert/actix.rs | 3 - src/convert/axum.rs | 14 -- src/convert/init_data.rs | 1 - src/convert/multipart.rs | 29 ----- src/convert/redis.rs | 6 - src/convert/reqwest.rs | 54 -------- src/convert/serde_json.rs | 5 - src/convert/sqlx.rs | 9 -- src/convert/teloxide.rs | 2 - src/convert/tokio.rs | 2 - src/convert/tonic.rs | 7 - src/convert/validator.rs | 6 - src/frontend/browser_console_error.rs | 2 - src/frontend/browser_console_ext.rs | 8 -- src/frontend/tests.rs | 7 - src/kind.rs | 7 - src/response/actix_impl.rs | 12 -- src/response/axum_impl.rs | 11 -- src/response/core.rs | 2 - src/response/details.rs | 13 -- src/response/internal.rs | 19 --- src/response/legacy.rs | 4 - src/response/mapping.rs | 10 +- src/response/problem_json.rs | 118 +++++++---------- src/response/tests.rs | 66 ---------- src/result_ext.rs | 15 --- src/turnkey/classifier.rs | 8 +- src/turnkey/domain.rs | 2 - src/turnkey/tests.rs | 1 - tests/app_code_reuse.rs | 4 - tests/build_test.rs | 61 --------- tests/enforce_app_result_alias.rs | 4 - tests/ensure_fail.rs | 8 -- tests/error_derive.rs | 70 ++-------- tests/masterror_macro.rs | 15 --- tests/readme_sync.rs | 8 -- tests/ui/app_error/pass/enum.rs | 2 - tests/ui/app_error/pass/struct.rs | 1 - tests/ui/formatter/pass/all_formatters.rs | 4 +- .../formatter/pass/display_dynamic_specs.rs | 1 - tests/ui/formatter/pass/fmt_path.rs | 1 - tests/ui/formatter/pass/format_arguments.rs | 2 - .../formatter/pass/individual_formatters.rs | 3 +- 99 files changed, 119 insertions(+), 1661 deletions(-) diff --git a/benches/comparison.rs b/benches/comparison.rs index b472726..6306d49 100644 --- a/benches/comparison.rs +++ b/benches/comparison.rs @@ -31,7 +31,6 @@ struct MasterrorError { fn bench_error_creation(c: &mut Criterion) { let mut group = c.benchmark_group("error_creation"); - group.bench_function("thiserror", |b| { b.iter(|| { let err = ThiserrorError { @@ -40,21 +39,18 @@ fn bench_error_creation(c: &mut Criterion) { black_box(err) }); }); - group.bench_function("anyhow", |b| { b.iter(|| { let err = anyhow::anyhow!("IO operation failed"); black_box(err) }); }); - group.bench_function("masterror", |b| { b.iter(|| { let err = masterror::AppError::internal("IO operation failed"); black_box(err) }); }); - group.bench_function("masterror_with_source", |b| { b.iter(|| { let err = masterror::AppError::internal("IO operation failed") @@ -62,13 +58,11 @@ fn bench_error_creation(c: &mut Criterion) { black_box(err) }); }); - group.finish(); } fn bench_error_with_context(c: &mut Criterion) { let mut group = c.benchmark_group("error_with_context"); - group.bench_function("anyhow_context", |b| { b.iter(|| { let result: Result<(), IoError> = Err(IoError::other("disk offline")); @@ -76,7 +70,6 @@ fn bench_error_with_context(c: &mut Criterion) { black_box(err) }); }); - group.bench_function("masterror_context", |b| { b.iter(|| { let err = masterror::AppError::internal("IO operation failed") @@ -84,13 +77,11 @@ fn bench_error_with_context(c: &mut Criterion) { black_box(err) }); }); - group.finish(); } fn bench_error_chain_traversal(c: &mut Criterion) { let mut group = c.benchmark_group("error_chain_traversal"); - group.bench_function("anyhow_chain", |b| { b.iter_batched( || { @@ -104,7 +95,6 @@ fn bench_error_chain_traversal(c: &mut Criterion) { BatchSize::SmallInput ); }); - group.bench_function("masterror_chain", |b| { b.iter_batched( || { @@ -118,13 +108,11 @@ fn bench_error_chain_traversal(c: &mut Criterion) { BatchSize::SmallInput ); }); - group.finish(); } fn bench_root_cause(c: &mut Criterion) { let mut group = c.benchmark_group("root_cause"); - group.bench_function("anyhow", |b| { b.iter_batched( || { @@ -138,7 +126,6 @@ fn bench_root_cause(c: &mut Criterion) { BatchSize::SmallInput ); }); - group.bench_function("masterror", |b| { b.iter_batched( || { @@ -152,13 +139,11 @@ fn bench_root_cause(c: &mut Criterion) { BatchSize::SmallInput ); }); - group.finish(); } fn bench_is_type_check(c: &mut Criterion) { let mut group = c.benchmark_group("is_type_check"); - group.bench_function("anyhow", |b| { b.iter_batched( || { @@ -172,7 +157,6 @@ fn bench_is_type_check(c: &mut Criterion) { BatchSize::SmallInput ); }); - group.bench_function("masterror", |b| { b.iter_batched( || masterror::AppError::internal("error").with_context(IoError::other("disk offline")), @@ -183,13 +167,11 @@ fn bench_is_type_check(c: &mut Criterion) { BatchSize::SmallInput ); }); - group.finish(); } fn bench_display(c: &mut Criterion) { let mut group = c.benchmark_group("display"); - group.bench_function("thiserror", |b| { b.iter_batched( || ThiserrorError { @@ -202,7 +184,6 @@ fn bench_display(c: &mut Criterion) { BatchSize::SmallInput ); }); - group.bench_function("anyhow", |b| { b.iter_batched( || anyhow::anyhow!("IO operation failed"), @@ -213,7 +194,6 @@ fn bench_display(c: &mut Criterion) { BatchSize::SmallInput ); }); - group.bench_function("masterror", |b| { b.iter_batched( || masterror::AppError::internal("IO operation failed"), @@ -224,7 +204,6 @@ fn bench_display(c: &mut Criterion) { BatchSize::SmallInput ); }); - group.finish(); } diff --git a/benches/error_paths.rs b/benches/error_paths.rs index 1d59807..a3b10f1 100644 --- a/benches/error_paths.rs +++ b/benches/error_paths.rs @@ -27,7 +27,6 @@ impl std::error::Error for DummyError {} fn context_into_error(c: &mut Criterion) { let mut group = c.benchmark_group("context_into_error"); - group.bench_function("non_redacted", |b| { b.iter(|| { let context = build_context(false); @@ -35,7 +34,6 @@ fn context_into_error(c: &mut Criterion) { black_box(err) }); }); - group.bench_function("redacted", |b| { b.iter(|| { let context = build_context(true); @@ -43,13 +41,11 @@ fn context_into_error(c: &mut Criterion) { black_box(err) }); }); - group.finish(); } fn problem_json_from_app_error(c: &mut Criterion) { let mut group = c.benchmark_group("problem_json_from_app_error"); - group.bench_function("non_redacted", |b| { b.iter_batched( || promote_error(build_context(false)), @@ -60,7 +56,6 @@ fn problem_json_from_app_error(c: &mut Criterion) { BatchSize::SmallInput ); }); - group.bench_function("redacted", |b| { b.iter_batched( || promote_error(build_context(true)), @@ -71,7 +66,6 @@ fn problem_json_from_app_error(c: &mut Criterion) { BatchSize::SmallInput ); }); - group.finish(); } @@ -82,7 +76,6 @@ fn build_context(redacted: bool) -> Context { .with(field::duration("elapsed", Duration::from_millis(275))) .with(field::bool("idempotent", true)) .with(field::ip("peer", IpAddr::from(Ipv4Addr::LOCALHOST))); - if redacted { context = context .with(field::str("token", "secret-token")) @@ -92,7 +85,6 @@ fn build_context(redacted: bool) -> Context { } else { context = context.with(field::str("token", "secret-token")); } - context } diff --git a/build.rs b/build.rs index 9136321..737f670 100644 --- a/build.rs +++ b/build.rs @@ -21,6 +21,13 @@ fn main() { } } +/// Runs build script with README sync and feature detection. +/// +/// # Modes +/// +/// - **Drift allowed**: Skip all checks if `MASTERROR_DISABLE_README_SYNC=1` +/// - **Packaged/no-git**: Relaxed verification (warning only, no failure) +/// - **Git working tree**: Strict sync mode fn run() -> Result<(), Box> { println!("cargo:rustc-check-cfg=cfg(masterror_has_error_generic_member_access)"); println!("cargo:rustc-check-cfg=cfg(masterror_requires_error_generic_feature)"); @@ -28,34 +35,23 @@ fn run() -> Result<(), Box> { println!("cargo:rerun-if-changed=README.template.md"); println!("cargo:rerun-if-changed=build/readme.rs"); println!("cargo:rerun-if-env-changed=MASTERROR_DISABLE_ERROR_GENERIC_MEMBER_ACCESS"); - let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?); - - // Явный флаг, чтобы где угодно ослабить проверку (ремень безопасности для - // CI/verify) if allow_readme_drift() { return Ok(()); } - - // В tarball-е (cargo package --verify) или вообще без .git — проверяем мягко и - // НЕ валимся. if is_packaged_manifest(&manifest_dir) || !has_git_anywhere(&manifest_dir) { if let Err(err) = verify_readme_relaxed(&manifest_dir) { println!("cargo:warning={err}"); } return Ok(()); } - - // В нормальном git-рабочем дереве — синхронизируем (жёсткий режим). sync_readme(&manifest_dir)?; - if let Some(support) = detect_error_generic_member_access()? { if support.requires_feature_attr { println!("cargo:rustc-cfg=masterror_requires_error_generic_feature"); } println!("cargo:rustc-cfg=masterror_has_error_generic_member_access"); } - Ok(()) } @@ -109,10 +105,8 @@ fn detect_error_generic_member_access() if has_env("MASTERROR_DISABLE_ERROR_GENERIC_MEMBER_ACCESS") { return Ok(None); } - let out_dir = PathBuf::from(env::var("OUT_DIR")?); fs::create_dir_all(&out_dir)?; - let stable_check = out_dir.join("check_error_generic_stable.rs"); fs::write(&stable_check, STABLE_SNIPPET)?; if compile_probe(&stable_check, &out_dir)?.success() { @@ -120,7 +114,6 @@ fn detect_error_generic_member_access() requires_feature_attr: false })); } - let nightly_check = out_dir.join("check_error_generic_nightly.rs"); fs::write(&nightly_check, NIGHTLY_SNIPPET)?; if compile_probe(&nightly_check, &out_dir)?.success() { @@ -128,7 +121,6 @@ fn detect_error_generic_member_access() requires_feature_attr: true })); } - Ok(None) } @@ -222,11 +214,9 @@ mod tests { env::remove_var("MASTERROR_ALLOW_README_DRIFT"); env::remove_var("MASTERROR_SKIP_README_CHECK"); assert!(!allow_readme_drift()); - env::set_var("MASTERROR_ALLOW_README_DRIFT", "1"); assert!(allow_readme_drift()); env::remove_var("MASTERROR_ALLOW_README_DRIFT"); - env::set_var("MASTERROR_SKIP_README_CHECK", "1"); assert!(allow_readme_drift()); env::remove_var("MASTERROR_SKIP_README_CHECK"); @@ -238,7 +228,6 @@ mod tests { requires_feature_attr: true }; assert!(support.requires_feature_attr); - let support = ErrorGenericSupport { requires_feature_attr: false }; diff --git a/build/readme.rs b/build/readme.rs index a9e4fb6..25bd38c 100644 --- a/build/readme.rs +++ b/build/readme.rs @@ -47,20 +47,17 @@ pub fn generate_readme(manifest_path: &Path, template_path: &Path) -> Result = readme_meta .features .keys() @@ -215,7 +210,6 @@ fn collect_feature_docs( if !unknown_metadata.is_empty() { return Err(ReadmeError::UnknownMetadataFeature(unknown_metadata)); } - let mut ordered = Vec::new(); for name in &readme_meta.feature_order { if name == "default" { @@ -286,7 +280,6 @@ mod tests { features.insert("feat1".to_string(), vec![]); features.insert("feat2".to_string(), vec![]); features.insert("default".to_string(), vec![]); - let mut feature_meta = BTreeMap::new(); feature_meta.insert( "feat1".to_string(), @@ -302,14 +295,12 @@ mod tests { extra: vec!["Extra note".to_string()] } ); - let readme_meta = ReadmeMetadata { feature_order: vec!["feat1".to_string()], feature_snippet_group: Some(4), conversion_lines: vec![], features: feature_meta }; - let result = collect_feature_docs(&features, &readme_meta); assert!(result.is_ok()); let docs = result.unwrap(); @@ -322,14 +313,12 @@ mod tests { fn collect_feature_docs_errors_on_missing_metadata() { let mut features = BTreeMap::new(); features.insert("feat1".to_string(), vec![]); - let readme_meta = ReadmeMetadata { feature_order: vec![], feature_snippet_group: Some(4), conversion_lines: vec![], features: BTreeMap::new() }; - let result = collect_feature_docs(&features, &readme_meta); assert!(result.is_err()); assert!(matches!( @@ -341,7 +330,6 @@ mod tests { #[test] fn collect_feature_docs_errors_on_unknown_metadata() { let features = BTreeMap::new(); - let mut feature_meta = BTreeMap::new(); feature_meta.insert( "unknown".to_string(), @@ -350,14 +338,12 @@ mod tests { extra: vec![] } ); - let readme_meta = ReadmeMetadata { feature_order: vec![], feature_snippet_group: Some(4), conversion_lines: vec![], features: feature_meta }; - let result = collect_feature_docs(&features, &readme_meta); assert!(result.is_err()); assert!(matches!( @@ -370,7 +356,6 @@ mod tests { fn collect_feature_docs_errors_on_unknown_feature_in_order() { let mut features = BTreeMap::new(); features.insert("feat1".to_string(), vec![]); - let mut feature_meta = BTreeMap::new(); feature_meta.insert( "feat1".to_string(), @@ -379,14 +364,12 @@ mod tests { extra: vec![] } ); - let readme_meta = ReadmeMetadata { feature_order: vec!["unknown".to_string()], feature_snippet_group: Some(4), conversion_lines: vec![], features: feature_meta }; - let result = collect_feature_docs(&features, &readme_meta); assert!(result.is_err()); assert!(matches!( @@ -399,7 +382,6 @@ mod tests { fn collect_feature_docs_errors_on_duplicate_in_order() { let mut features = BTreeMap::new(); features.insert("feat1".to_string(), vec![]); - let mut feature_meta = BTreeMap::new(); feature_meta.insert( "feat1".to_string(), @@ -408,14 +390,12 @@ mod tests { extra: vec![] } ); - let readme_meta = ReadmeMetadata { feature_order: vec!["feat1".to_string(), "feat1".to_string()], feature_snippet_group: Some(4), conversion_lines: vec![], features: feature_meta }; - let result = collect_feature_docs(&features, &readme_meta); assert!(result.is_err()); assert!(matches!( @@ -429,7 +409,6 @@ mod tests { let mut features = BTreeMap::new(); features.insert("feat1".to_string(), vec![]); features.insert("default".to_string(), vec![]); - let mut feature_meta = BTreeMap::new(); feature_meta.insert( "feat1".to_string(), @@ -438,14 +417,12 @@ mod tests { extra: vec![] } ); - let readme_meta = ReadmeMetadata { feature_order: vec!["default".to_string(), "feat1".to_string()], feature_snippet_group: Some(4), conversion_lines: vec![], features: feature_meta }; - let result = collect_feature_docs(&features, &readme_meta); assert!(result.is_ok()); let docs = result.unwrap(); diff --git a/build/render.rs b/build/render.rs index ef46caf..981faba 100644 --- a/build/render.rs +++ b/build/render.rs @@ -37,13 +37,11 @@ pub(crate) fn render_readme( let feature_bullets = render_feature_bullets(features); let feature_snippet = render_feature_snippet(features, snippet_group); let conversion_bullets = render_conversion_bullets(conversions); - let mut rendered = template.replace("{{CRATE_VERSION}}", version); rendered = rendered.replace("{{MSRV}}", rust_version); rendered = rendered.replace("{{FEATURE_BULLETS}}", &feature_bullets); rendered = rendered.replace("{{FEATURE_SNIPPET}}", &feature_snippet); rendered = rendered.replace("{{CONVERSION_BULLETS}}", &conversion_bullets); - if let Some(name) = find_placeholder(&rendered) { return Err(ReadmeError::UnresolvedPlaceholder(name)); } @@ -189,9 +187,7 @@ mod tests { extra: vec!["Requires Tokio runtime".to_string()] }, ]; - let result = render_feature_bullets(&features); - assert!(result.contains("- `actix` — Actix-web integration")); assert!(result.contains("- `axum` — Axum integration")); assert!(result.contains(" - Requires Tokio runtime")); @@ -210,9 +206,7 @@ mod tests { "std::io::Error → AppError::Internal".to_string(), "String → AppError::BadRequest".to_string(), ]; - let result = render_conversion_bullets(&conversions); - assert_eq!( result, "- std::io::Error → AppError::Internal\n- String → AppError::BadRequest" @@ -245,9 +239,7 @@ mod tests { extra: vec![] }, ]; - let result = render_feature_snippet(&features, 2); - assert!(result.contains("\"feat1\", \"feat2\",")); assert!(result.contains("\"feat3\"")); } @@ -268,7 +260,6 @@ mod tests { extra: vec![] }]; let conversions = vec!["Error → AppError".to_string()]; - let result = render_readme(template, "1.0.0", "1.70", &features, 4, &conversions); assert!(result.is_ok()); let rendered = result.unwrap(); @@ -284,7 +275,6 @@ mod tests { let template = "{{CRATE_VERSION}} {{UNKNOWN}}"; let features = vec![]; let conversions = vec![]; - let result = render_readme(template, "1.0.0", "1.70", &features, 4, &conversions); assert!(result.is_err()); assert!(matches!( diff --git a/examples/axum-rest-api/src/lib.rs b/examples/axum-rest-api/src/lib.rs index 6123f2d..9d30ad5 100644 --- a/examples/axum-rest-api/src/lib.rs +++ b/examples/axum-rest-api/src/lib.rs @@ -131,7 +131,6 @@ pub async fn get_user( Path(user_id): Path ) -> Result, UserError> { let users = state.users.read().unwrap(); - users .get(&user_id) .cloned() @@ -149,24 +148,17 @@ pub async fn create_user( ) -> Result<(StatusCode, axum::Json), UserError> { validate_email(&req.email)?; validate_name(&req.name)?; - let mut users = state.users.write().unwrap(); - - // Check for duplicate email if users.values().any(|u| u.email == req.email) { return Err(UserError::DuplicateEmail); } - let user = User { id: Uuid::new_v4(), name: req.name, email: req.email }; - info!(user_id = %user.id, email = %user.email, "Creating new user"); - users.insert(user.id, user.clone()); - Ok((StatusCode::CREATED, axum::Json(user))) } @@ -181,27 +173,18 @@ pub async fn update_user( ) -> Result, UserError> { validate_email(&req.email)?; validate_name(&req.name)?; - let mut users = state.users.write().unwrap(); - - // Check if user exists and get current email let current_email = users .get(&user_id) .map(|u| u.email.clone()) .ok_or(UserError::NotFound)?; - - // Check if email is being changed to existing email if req.email != current_email && users.values().any(|u| u.email == req.email) { return Err(UserError::DuplicateEmail); } - info!(user_id = %user_id, "Updating user"); - - // Now update the user let user = users.get_mut(&user_id).ok_or(UserError::NotFound)?; user.name = req.name; user.email = req.email; - Ok(axum::Json(user.clone())) } @@ -213,10 +196,7 @@ pub async fn delete_user( Path(user_id): Path ) -> Result { let mut users = state.users.write().unwrap(); - users.remove(&user_id).ok_or(UserError::NotFound)?; - info!(user_id = %user_id, "Deleted user"); - Ok(StatusCode::NO_CONTENT) } diff --git a/examples/axum-rest-api/src/main.rs b/examples/axum-rest-api/src/main.rs index 366d68c..abc582a 100644 --- a/examples/axum-rest-api/src/main.rs +++ b/examples/axum-rest-api/src/main.rs @@ -8,7 +8,7 @@ use axum::{ }; use axum_rest_api::{AppState, create_user, delete_user, get_user, update_user}; use tracing::info; -use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; +use tracing_subscriber::{EnvFilter, fmt::layer, layer::SubscriberExt, util::SubscriberInitExt}; /// Health check endpoint async fn health() -> &'static str { @@ -17,14 +17,11 @@ async fn health() -> &'static str { #[tokio::main] async fn main() { - // Initialize tracing tracing_subscriber::registry() .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into())) - .with(tracing_subscriber::fmt::layer()) + .with(layer()) .init(); - let state = AppState::new(); - let app = Router::new() .route("/health", get(health)) .route("/users/{id}", get(get_user)) @@ -32,12 +29,9 @@ async fn main() { .route("/users/{id}", put(update_user)) .route("/users/{id}", delete(delete_user)) .with_state(state); - let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") .await .unwrap(); - info!("Server listening on {}", listener.local_addr().unwrap()); - axum::serve(listener, app).await.unwrap(); } diff --git a/examples/axum-rest-api/tests/integration_tests.rs b/examples/axum-rest-api/tests/integration_tests.rs index a2e5016..65cfe3d 100644 --- a/examples/axum-rest-api/tests/integration_tests.rs +++ b/examples/axum-rest-api/tests/integration_tests.rs @@ -15,7 +15,6 @@ use uuid::Uuid; /// Helper to create test router fn create_test_router() -> Router { let state = AppState::new(); - Router::new() .route("/users/{id}", get(axum_rest_api::get_user)) .route("/users", post(axum_rest_api::create_user)) @@ -28,9 +27,7 @@ fn create_test_router() -> Router { async fn health_check_returns_ok() { let app = Router::new().route("/health", get(|| async { "OK" })); let server = TestServer::new(app).unwrap(); - let response = server.get("/health").await; - assert_eq!(response.status_code(), StatusCode::OK); assert_eq!(response.text(), "OK"); } @@ -38,7 +35,6 @@ async fn health_check_returns_ok() { #[tokio::test] async fn create_user_returns_201() { let server = TestServer::new(create_test_router()).unwrap(); - let response = server .post("/users") .json(&json!({ @@ -46,9 +42,7 @@ async fn create_user_returns_201() { "email": "alice@example.com" })) .await; - assert_eq!(response.status_code(), StatusCode::CREATED); - let user: User = response.json(); assert_eq!(user.name, "Alice"); assert_eq!(user.email, "alice@example.com"); @@ -57,7 +51,6 @@ async fn create_user_returns_201() { #[tokio::test] async fn create_user_with_invalid_email_returns_422() { let server = TestServer::new(create_test_router()).unwrap(); - let response = server .post("/users") .json(&json!({ @@ -65,9 +58,7 @@ async fn create_user_with_invalid_email_returns_422() { "email": "invalid-email" })) .await; - assert_eq!(response.status_code(), StatusCode::UNPROCESSABLE_ENTITY); - let body: serde_json::Value = response.json(); assert_eq!(body["status"], 422); assert!(body["detail"].as_str().unwrap().contains("invalid email")); @@ -76,7 +67,6 @@ async fn create_user_with_invalid_email_returns_422() { #[tokio::test] async fn create_user_with_short_name_returns_422() { let server = TestServer::new(create_test_router()).unwrap(); - let response = server .post("/users") .json(&json!({ @@ -84,9 +74,7 @@ async fn create_user_with_short_name_returns_422() { "email": "valid@example.com" })) .await; - assert_eq!(response.status_code(), StatusCode::UNPROCESSABLE_ENTITY); - let body: serde_json::Value = response.json(); assert!( body["detail"] @@ -99,8 +87,6 @@ async fn create_user_with_short_name_returns_422() { #[tokio::test] async fn create_duplicate_email_returns_409() { let server = TestServer::new(create_test_router()).unwrap(); - - // Create first user server .post("/users") .json(&json!({ @@ -108,8 +94,6 @@ async fn create_duplicate_email_returns_409() { "email": "alice@example.com" })) .await; - - // Try to create user with same email let response = server .post("/users") .json(&json!({ @@ -117,9 +101,7 @@ async fn create_duplicate_email_returns_409() { "email": "alice@example.com" })) .await; - assert_eq!(response.status_code(), StatusCode::CONFLICT); - let body: serde_json::Value = response.json(); assert_eq!(body["status"], 409); assert!(body["detail"].as_str().unwrap().contains("already exists")); @@ -128,8 +110,6 @@ async fn create_duplicate_email_returns_409() { #[tokio::test] async fn get_user_returns_200() { let server = TestServer::new(create_test_router()).unwrap(); - - // Create user let create_response = server .post("/users") .json(&json!({ @@ -137,14 +117,9 @@ async fn get_user_returns_200() { "email": "charlie@example.com" })) .await; - let created_user: User = create_response.json(); - - // Get user let response = server.get(&format!("/users/{}", created_user.id)).await; - assert_eq!(response.status_code(), StatusCode::OK); - let user: User = response.json(); assert_eq!(user.id, created_user.id); assert_eq!(user.name, "Charlie"); @@ -153,12 +128,9 @@ async fn get_user_returns_200() { #[tokio::test] async fn get_nonexistent_user_returns_404() { let server = TestServer::new(create_test_router()).unwrap(); - let fake_id = Uuid::new_v4(); let response = server.get(&format!("/users/{}", fake_id)).await; - assert_eq!(response.status_code(), StatusCode::NOT_FOUND); - let body: serde_json::Value = response.json(); assert_eq!(body["status"], 404); assert!(body["detail"].as_str().unwrap().contains("not found")); @@ -167,8 +139,6 @@ async fn get_nonexistent_user_returns_404() { #[tokio::test] async fn update_user_returns_200() { let server = TestServer::new(create_test_router()).unwrap(); - - // Create user let create_response = server .post("/users") .json(&json!({ @@ -176,10 +146,7 @@ async fn update_user_returns_200() { "email": "dave@example.com" })) .await; - let created_user: User = create_response.json(); - - // Update user let response = server .put(&format!("/users/{}", created_user.id)) .json(&json!({ @@ -187,9 +154,7 @@ async fn update_user_returns_200() { "email": "dave.updated@example.com" })) .await; - assert_eq!(response.status_code(), StatusCode::OK); - let user: User = response.json(); assert_eq!(user.name, "Dave Updated"); assert_eq!(user.email, "dave.updated@example.com"); @@ -198,7 +163,6 @@ async fn update_user_returns_200() { #[tokio::test] async fn update_nonexistent_user_returns_404() { let server = TestServer::new(create_test_router()).unwrap(); - let fake_id = Uuid::new_v4(); let response = server .put(&format!("/users/{}", fake_id)) @@ -207,15 +171,12 @@ async fn update_nonexistent_user_returns_404() { "email": "ghost@example.com" })) .await; - assert_eq!(response.status_code(), StatusCode::NOT_FOUND); } #[tokio::test] async fn delete_user_returns_204() { let server = TestServer::new(create_test_router()).unwrap(); - - // Create user let create_response = server .post("/users") .json(&json!({ @@ -223,15 +184,9 @@ async fn delete_user_returns_204() { "email": "eve@example.com" })) .await; - let created_user: User = create_response.json(); - - // Delete user let response = server.delete(&format!("/users/{}", created_user.id)).await; - assert_eq!(response.status_code(), StatusCode::NO_CONTENT); - - // Verify user is gone let get_response = server.get(&format!("/users/{}", created_user.id)).await; assert_eq!(get_response.status_code(), StatusCode::NOT_FOUND); } @@ -239,9 +194,7 @@ async fn delete_user_returns_204() { #[tokio::test] async fn delete_nonexistent_user_returns_404() { let server = TestServer::new(create_test_router()).unwrap(); - let fake_id = Uuid::new_v4(); let response = server.delete(&format!("/users/{}", fake_id)).await; - assert_eq!(response.status_code(), StatusCode::NOT_FOUND); } diff --git a/examples/basic-async/src/main.rs b/examples/basic-async/src/main.rs index a60f419..206d852 100644 --- a/examples/basic-async/src/main.rs +++ b/examples/basic-async/src/main.rs @@ -7,21 +7,17 @@ use std::time::Duration; use masterror::AppError; -use tokio::time::timeout; +use tokio::time::{sleep, timeout}; /// Simulated data fetch operation async fn fetch_data(id: u64) -> Result { if id == 0 { return Err(AppError::validation("ID cannot be zero")); } - if id > 1000 { return Err(AppError::not_found("ID not found in database")); } - - // Simulate async work - tokio::time::sleep(Duration::from_millis(100)).await; - + sleep(Duration::from_millis(100)).await; Ok(format!("Data for ID {id}")) } @@ -30,10 +26,7 @@ async fn process_data(data: &str) -> Result { if data.is_empty() { return Err(AppError::validation("Data cannot be empty")); } - - // Simulate processing - tokio::time::sleep(Duration::from_millis(50)).await; - + sleep(Duration::from_millis(50)).await; Ok(format!("Processed: {data}")) } @@ -42,10 +35,7 @@ async fn save_result(result: String) -> Result<(), AppError> { if result.len() > 1000 { return Err(AppError::bad_request("Result too large to save")); } - - // Simulate saving - tokio::time::sleep(Duration::from_millis(50)).await; - + sleep(Duration::from_millis(50)).await; println!("✓ Saved: {result}"); Ok(()) } @@ -53,29 +43,23 @@ async fn save_result(result: String) -> Result<(), AppError> { /// Complete processing pipeline async fn process_pipeline(id: u64) -> Result<(), AppError> { println!("Processing ID {id}..."); - let data = fetch_data(id).await?; let processed = process_data(&data).await?; save_result(processed).await?; - Ok(()) } /// Slow operation for timeout demonstration async fn slow_operation() -> Result { - tokio::time::sleep(Duration::from_secs(10)).await; + sleep(Duration::from_secs(10)).await; Ok("Completed".to_string()) } #[tokio::main] async fn main() -> Result<(), AppError> { println!("Basic Async Error Handling Example\\n"); - - // Successful pipeline println!("=== Successful Pipeline ==="); process_pipeline(123).await?; - - // Validation error println!("\\n=== Validation Error ==="); match process_pipeline(0).await { Ok(()) => println!("✗ Should have failed"), @@ -84,8 +68,6 @@ async fn main() -> Result<(), AppError> { println!(" → Kind: {:?}, HTTP: {}", e.kind, e.kind.http_status()); } } - - // Not found error println!("\\n=== Not Found Error ==="); match process_pipeline(9999).await { Ok(()) => println!("✗ Should have failed"), @@ -94,8 +76,6 @@ async fn main() -> Result<(), AppError> { println!(" → Kind: {:?}, HTTP: {}", e.kind, e.kind.http_status()); } } - - // Timeout error println!("\\n=== Timeout Error ==="); match timeout(Duration::from_secs(1), slow_operation()).await { Ok(Ok(result)) => println!("✓ Completed: {result}"), @@ -110,7 +90,6 @@ async fn main() -> Result<(), AppError> { ); } } - println!("\\n✓ Example completed"); Ok(()) } diff --git a/examples/basic_usage.rs b/examples/basic_usage.rs index 880a0f3..fee1722 100644 --- a/examples/basic_usage.rs +++ b/examples/basic_usage.rs @@ -21,11 +21,9 @@ fn fetch_user(id: &str) -> AppResult { if id.is_empty() { masterror::fail!(AppError::bad_request("User ID cannot be empty")); } - if id == "404" { return Err(AppError::not_found("User not found")); } - Ok(format!("User {id}")) } @@ -43,66 +41,51 @@ fn database_operation() -> AppResult<()> { "timeout", std::time::Duration::from_secs(5) )); - Err(err) } fn main() { println!("=== Basic Error Creation ===\n"); - let err = AppError::new(AppErrorKind::BadRequest, "Invalid input"); println!("Simple error: {err}"); println!("Error kind: {:?}", err.kind); println!("Error code: {:?}\n", err.code); - println!("=== Using ensure! macro ===\n"); - match validate_age(-5) { Ok(()) => println!("Age valid"), Err(e) => println!("Validation failed: {e}") } - match validate_age(200) { Ok(()) => println!("Age valid"), Err(e) => println!("Validation failed: {e}") } - match validate_age(25) { Ok(()) => println!("Age 25 is valid\n"), Err(e) => println!("Validation failed: {e}") } - println!("=== Using fail! macro ===\n"); - match fetch_user("") { Ok(user) => println!("Found: {user}"), Err(e) => println!("Fetch failed: {e}") } - match fetch_user("404") { Ok(user) => println!("Found: {user}"), Err(e) => println!("Fetch failed: {e}") } - match fetch_user("alice") { Ok(user) => println!("Found: {user}\n"), Err(e) => println!("Fetch failed: {e}") } - println!("=== Error Propagation ===\n"); - match process_request("bob", 30) { Ok(result) => println!("Success: {result}"), Err(e) => println!("Request failed: {e}") } - match process_request("404", 30) { Ok(result) => println!("Success: {result}"), Err(e) => println!("Request failed: {e}\n") } - println!("=== Structured Metadata ===\n"); - match database_operation() { Ok(()) => println!("Database operation succeeded"), Err(e) => { diff --git a/examples/colored_cli.rs b/examples/colored_cli.rs index b2c9a8b..4f58548 100644 --- a/examples/colored_cli.rs +++ b/examples/colored_cli.rs @@ -27,14 +27,12 @@ use masterror::{AppError, AppErrorKind, field}; fn main() { println!("=== Masterror Colored CLI Demo ===\n"); - demo_critical_errors(); demo_client_errors(); demo_error_with_context(); demo_error_with_metadata(); demo_error_chain(); demo_all_error_kinds(); - println!("\n=== Demo Complete ==="); println!("\nColor behavior:"); println!(" - Critical errors (5xx): Red"); @@ -47,14 +45,12 @@ fn main() { fn demo_critical_errors() { println!("--- Critical Server Errors (5xx) ---\n"); - let errors = vec![ AppError::internal("Database connection pool exhausted"), AppError::database_with_message("Failed to execute migration"), AppError::timeout("API request exceeded 30s timeout"), AppError::network("DNS resolution failed for api.example.com"), ]; - for err in errors { eprintln!("{}\n", err); } @@ -62,14 +58,12 @@ fn demo_critical_errors() { fn demo_client_errors() { println!("--- Client Errors (4xx) ---\n"); - let errors = vec![ AppError::not_found("User with ID 12345 does not exist"), AppError::bad_request("Missing required field: email"), AppError::validation("Password must be at least 8 characters"), AppError::forbidden("Insufficient permissions to access resource"), ]; - for err in errors { eprintln!("{}\n", err); } @@ -77,41 +71,34 @@ fn demo_client_errors() { fn demo_error_with_context() { println!("--- Error with Source Context ---\n"); - let io_err = IoError::other("Connection reset by peer"); let err = AppError::network("Failed to fetch user data").with_context(io_err); - eprintln!("{}\n", err); } fn demo_error_with_metadata() { println!("--- Error with Structured Metadata ---\n"); - let err = AppError::database_with_message("Query execution failed") .with_field(field::str("query", "SELECT * FROM users WHERE id = ?")) .with_field(field::u64("duration_ms", 5432)) .with_field(field::str("connection_id", "conn_abc123")) .with_field(field::u64("retry_count", 3)); - eprintln!("{}\n", err); } fn demo_error_chain() { println!("--- Deep Error Chain ---\n"); - let root = IoError::other("Disk full"); let mid = format!("Failed to write log file: {}", root); let top = AppError::internal("Application initialization failed") .with_context(IoError::other(mid)) .with_field(field::str("config_path", "/etc/app/config.toml")) .with_field(field::u64("retry_attempt", 3)); - eprintln!("{}\n", top); } fn demo_all_error_kinds() { println!("--- All Error Kinds (Sampling) ---\n"); - let kinds = vec![ (AppErrorKind::NotFound, "Resource not found"), (AppErrorKind::Validation, "Input validation failed"), @@ -124,7 +111,6 @@ fn demo_all_error_kinds() { (AppErrorKind::Network, "Network error"), (AppErrorKind::RateLimited, "Too many requests"), ]; - for (kind, msg) in kinds { let err = AppError::new(kind, msg); eprintln!("{}\n", err); diff --git a/examples/custom-domain-errors/src/main.rs b/examples/custom-domain-errors/src/main.rs index b9f5298..1ad1644 100644 --- a/examples/custom-domain-errors/src/main.rs +++ b/examples/custom-domain-errors/src/main.rs @@ -203,14 +203,12 @@ fn process_payment(amount: u64, balance: u64) -> Result { "amount must be greater than 0".to_string() )); } - if amount > balance { return Err(PaymentError::InsufficientFunds { balance, required: amount }); } - Ok(format!("Payment of ${amount} processed successfully")) } @@ -219,11 +217,9 @@ fn authenticate(username: &str, password: &str) -> Result { if username.is_empty() || password.is_empty() { return Err(AuthError::InvalidCredentials); } - if username != "admin" || password != "secret" { return Err(AuthError::InvalidCredentials); } - Ok("Authentication successful".to_string()) } @@ -234,23 +230,18 @@ fn validate_email(email: &str) -> Result<(), ValidationError> { field: "email".to_string() }); } - if !email.contains('@') { return Err(ValidationError::InvalidFormat { field: "email".to_string(), reason: "must contain @ symbol".to_string() }); } - Ok(()) } fn main() { println!("Custom Domain Errors Example\\n"); - - // Payment errors println!("=== Payment Processing ==="); - match process_payment(100, 500) { Ok(msg) => println!("✓ {msg}"), Err(e) => { @@ -258,7 +249,6 @@ fn main() { println!("✗ AppError: {app_err}"); } } - match process_payment(600, 500) { Ok(msg) => println!("✓ {msg}"), Err(e) => { @@ -271,7 +261,6 @@ fn main() { ); } } - match process_payment(0, 500) { Ok(msg) => println!("✓ {msg}"), Err(e) => { @@ -280,15 +269,11 @@ fn main() { println!(" → AppError kind: {:?}", app_err.kind); } } - - // Authentication errors println!("\\n=== Authentication ==="); - match authenticate("admin", "secret") { Ok(msg) => println!("✓ {msg}"), Err(e) => println!("✗ {e}") } - match authenticate("user", "wrong") { Ok(msg) => println!("✓ {msg}"), Err(e) => { @@ -301,7 +286,6 @@ fn main() { ); } } - let expired_err = AuthError::SessionExpired { expired_at: "2025-01-01T00:00:00Z".to_string() }; @@ -312,15 +296,11 @@ fn main() { app_err.kind, app_err.kind.http_status() ); - - // Validation errors println!("\\n=== Validation ==="); - match validate_email("user@example.com") { Ok(()) => println!("✓ Email is valid"), Err(e) => println!("✗ {e}") } - match validate_email("invalid-email") { Ok(()) => println!("✓ Email is valid"), Err(e) => { @@ -333,7 +313,6 @@ fn main() { ); } } - match validate_email("") { Ok(()) => println!("✓ Email is valid"), Err(e) => { @@ -346,10 +325,7 @@ fn main() { ); } } - - // External service errors println!("\\n=== External Service Errors ==="); - let service_err = ExternalServiceError::Timeout { service: "payment-gateway".to_string(), timeout_ms: 5000 @@ -361,7 +337,6 @@ fn main() { app_err.kind, app_err.kind.http_status() ); - let network_err = ExternalServiceError::NetworkError { service: "fraud-detection".to_string(), details: "connection refused".to_string() diff --git a/examples/derive_error.rs b/examples/derive_error.rs index 6f3c590..a91f81e 100644 --- a/examples/derive_error.rs +++ b/examples/derive_error.rs @@ -81,7 +81,6 @@ fn authenticate(valid: bool) -> Result<(), ServiceError> { fn main() { println!("=== thiserror Compatibility ===\n"); - match simulate_io_error() { Ok(()) => println!("I/O succeeded"), Err(e) => { @@ -89,9 +88,7 @@ fn main() { println!("Source: {:?}", e.source()); } } - println!("\n=== AppError Mapping ===\n"); - match find_user("") { Ok(user) => println!("Found: {user}"), Err(e) => { @@ -102,9 +99,7 @@ fn main() { println!("Message exposed: {:?}", app_error.message); } } - println!("\n=== Error Source Chain ===\n"); - match connect_database() { Ok(()) => println!("Connected"), Err(e) => { @@ -116,9 +111,7 @@ fn main() { } } } - println!("\n=== Enum Variants ===\n"); - match authenticate(false) { Ok(()) => println!("Authenticated"), Err(e) => { @@ -128,12 +121,10 @@ fn main() { println!("Code: {:?}", app_error.code); } } - let rate_limit_err = ServiceError::RateLimited; println!("\nRate limit error: {rate_limit_err}"); let app_error: AppError = rate_limit_err.into(); println!("Kind: {:?}", app_error.kind); - match connect_database().map_err(ServiceError::from) { Ok(()) => println!("Connected"), Err(e) => { diff --git a/examples/migrate_from_anyhow.rs b/examples/migrate_from_anyhow.rs index 8c71df7..b5acf4a 100644 --- a/examples/migrate_from_anyhow.rs +++ b/examples/migrate_from_anyhow.rs @@ -17,7 +17,6 @@ fn main() { println!("Same: .context() works identically"); println!("Plus: Structured metadata with field::*"); println!(); - match read_config("/tmp/config.toml") { Ok(content) => println!("Config: {content}"), Err(e) => println!("Error: {e}") @@ -27,13 +26,11 @@ fn main() { fn read_config(path: &str) -> AppResult { // .context() works exactly like anyhow let content = fs::read_to_string(path).context("Failed to read config file")?; - ensure!( !content.is_empty(), AppError::bad_request("Config file is empty") .with_field(field::str("path", path.to_string())) ); - if content.starts_with("invalid") { fail!( AppError::bad_request("Invalid config format") @@ -41,6 +38,5 @@ fn read_config(path: &str) -> AppResult { .with_field(field::u64("size", content.len() as u64)) ); } - Ok(content) } diff --git a/examples/migrate_from_thiserror.rs b/examples/migrate_from_thiserror.rs index 9c6fc0f..e6d4e90 100644 --- a/examples/migrate_from_thiserror.rs +++ b/examples/migrate_from_thiserror.rs @@ -13,8 +13,6 @@ fn main() { println!("Step 2: Add #[app_error(...)] for HTTP/gRPC mapping"); println!("Step 3: Use AppError with structured metadata"); println!(); - - // Demonstrate AppError with structured metadata match find_user("alice") { Ok(()) => println!("User found"), Err(e) => println!("Error: {e}") diff --git a/examples/redaction.rs b/examples/redaction.rs index 0d609d2..5f49027 100644 --- a/examples/redaction.rs +++ b/examples/redaction.rs @@ -11,7 +11,6 @@ fn main() { .with_field(field::str("email", "user@example.com").with_redaction(FieldRedaction::Hash)) .with_field(field::str("ip", "192.168.1.100").with_redaction(FieldRedaction::Redact)) .with_field(field::str("session_id", "abc123")); - println!("=== Redacted Metadata ===\n"); for (key, value, redaction) in err.metadata().iter_with_redaction() { println!("{key}: {value:?} [{redaction:?}]"); diff --git a/examples/sqlx-database/src/main.rs b/examples/sqlx-database/src/main.rs index 0d1c930..e242f1f 100644 --- a/examples/sqlx-database/src/main.rs +++ b/examples/sqlx-database/src/main.rs @@ -29,7 +29,6 @@ async fn init_database(pool: &SqlitePool) -> Result<(), AppError> { ) .execute(pool) .await?; - Ok(()) } @@ -40,9 +39,7 @@ async fn create_user(pool: &SqlitePool, email: &str, name: &str) -> Result Result { .bind(id) .fetch_one(pool) .await?; - Ok(User { id: row.get(0), email: row.get(1), @@ -70,7 +66,6 @@ async fn get_user_by_email(pool: &SqlitePool, email: &str) -> Result Result<(), AppEr .bind(id) .execute(pool) .await?; - if result.rows_affected() == 0 { return Err(AppError::not_found("user not found")); } - Ok(()) } @@ -99,11 +92,9 @@ async fn delete_user(pool: &SqlitePool, id: i64) -> Result<(), AppError> { .bind(id) .execute(pool) .await?; - if result.rows_affected() == 0 { return Err(AppError::not_found("user not found")); } - Ok(()) } @@ -114,53 +105,35 @@ async fn transfer_user_data( to_email: &str ) -> Result<(), AppError> { let mut tx = pool.begin().await?; - - // Get source user let row: SqliteRow = sqlx::query("SELECT name FROM users WHERE id = ?") .bind(from_id) .fetch_one(&mut *tx) .await?; - let name: String = row.get(0); - - // Update destination user let result = sqlx::query("UPDATE users SET name = ? WHERE email = ?") .bind(&name) .bind(to_email) .execute(&mut *tx) .await?; - if result.rows_affected() == 0 { return Err(AppError::not_found("destination user not found")); } - - // Commit transaction tx.commit().await?; - Ok(()) } #[tokio::main] async fn main() -> Result<(), AppError> { println!("SQLx Database Error Handling Example\\n"); - - // Connect to in-memory SQLite database let pool = SqlitePool::connect("sqlite::memory:").await?; println!("✓ Connected to database"); - - // Initialize schema init_database(&pool).await?; println!("✓ Database schema initialized\\n"); - - // Create users println!("=== Creating Users ==="); let user1 = create_user(&pool, "alice@example.com", "Alice").await?; println!("✓ Created user: {} (ID: {})", user1.name, user1.id); - let user2 = create_user(&pool, "bob@example.com", "Bob").await?; println!("✓ Created user: {} (ID: {})", user2.name, user2.id); - - // Try to create duplicate email (Conflict error) println!("\\n=== Testing Unique Constraint Violation ==="); match create_user(&pool, "alice@example.com", "Alice Duplicate").await { Ok(_) => println!("✗ Should have failed with conflict"), @@ -169,13 +142,9 @@ async fn main() -> Result<(), AppError> { println!(" → Kind: {:?}, HTTP: {}", e.kind, e.kind.http_status()); } } - - // Get existing user println!("\\n=== Retrieving User ==="); let found = get_user_by_email(&pool, "alice@example.com").await?; println!("✓ Found user: {} ({})", found.name, found.email); - - // Try to get non-existent user (NotFound error) println!("\\n=== Testing Row Not Found ==="); match get_user_by_id(&pool, 999).await { Ok(_) => println!("✗ Should have failed with not found"), @@ -184,14 +153,10 @@ async fn main() -> Result<(), AppError> { println!(" → Kind: {:?}, HTTP: {}", e.kind, e.kind.http_status()); } } - - // Update user println!("\\n=== Updating User ==="); update_user(&pool, user1.id, "Alice Updated").await?; let updated = get_user_by_id(&pool, user1.id).await?; println!("✓ Updated user name: {}", updated.name); - - // Try to update non-existent user println!("\\n=== Testing Update on Non-existent User ==="); match update_user(&pool, 999, "Ghost").await { Ok(_) => println!("✗ Should have failed with not found"), @@ -200,8 +165,6 @@ async fn main() -> Result<(), AppError> { println!(" → Kind: {:?}, HTTP: {}", e.kind, e.kind.http_status()); } } - - // Transaction example println!("\\n=== Testing Transaction ==="); transfer_user_data(&pool, user1.id, "bob@example.com").await?; let bob_updated = get_user_by_email(&pool, "bob@example.com").await?; @@ -209,13 +172,9 @@ async fn main() -> Result<(), AppError> { "✓ Transaction completed: Bob's name is now '{}'", bob_updated.name ); - - // Delete user println!("\\n=== Deleting User ==="); delete_user(&pool, user2.id).await?; println!("✓ Deleted user with ID: {}", user2.id); - - // Try to delete again (NotFound) match delete_user(&pool, user2.id).await { Ok(_) => println!("✗ Should have failed with not found"), Err(e) => { @@ -223,9 +182,7 @@ async fn main() -> Result<(), AppError> { println!(" → Kind: {:?}, HTTP: {}", e.kind, e.kind.http_status()); } } - pool.close().await; println!("\\n✓ Database connection closed"); - Ok(()) } diff --git a/examples/structured_metadata.rs b/examples/structured_metadata.rs index f12ccb1..ca3f9f5 100644 --- a/examples/structured_metadata.rs +++ b/examples/structured_metadata.rs @@ -35,7 +35,6 @@ fn api_request(endpoint: &'static str, client_ip: IpAddr, latency_ms: f64) -> Ap fn main() { println!("=== Structured Metadata with Typed Fields ===\n"); - match database_query("users", 12345, Duration::from_secs(30), 3) { Ok(_) => println!("Query succeeded"), Err(e) => { @@ -46,9 +45,7 @@ fn main() { } } } - println!("\n=== API Request with IP and Float ===\n"); - let client_ip: IpAddr = "192.168.1.100".parse().unwrap(); match api_request("/api/users", client_ip, 123.45) { Ok(()) => println!("API request succeeded"), @@ -60,15 +57,12 @@ fn main() { } } } - println!("\n=== Multiple Chained Metadata ===\n"); - let err = AppError::internal("Processing failed") .with_field(field::str("stage", "validation")) .with_field(field::u64("record_id", 999)) .with_field(field::duration("elapsed", Duration::from_millis(456))) .with_field(field::bool("retryable", true)); - println!("Error: {err}"); println!("Total metadata fields: {}", err.metadata().len()); println!("\nAll fields:"); diff --git a/masterror-derive/src/app_error_impl.rs b/masterror-derive/src/app_error_impl.rs index de12d16..c9a944c 100644 --- a/masterror-derive/src/app_error_impl.rs +++ b/masterror-derive/src/app_error_impl.rs @@ -17,25 +17,21 @@ pub fn expand(input: &ErrorInput) -> Result, Error> { fn expand_struct(input: &ErrorInput, data: &StructData) -> Result, Error> { let mut impls = Vec::new(); - if let Some(spec) = &data.app_error { impls.push(struct_app_error_impl(input, spec)); if spec.code.is_some() { impls.push(struct_app_code_impl(input, spec)); } } - Ok(impls) } fn expand_enum(input: &ErrorInput, variants: &[VariantData]) -> Result, Error> { let mut impls = Vec::new(); - if variants.iter().any(|variant| variant.app_error.is_some()) { ensure_all_have_app_error(variants)?; impls.push(enum_app_error_impl(input, variants)); } - if variants.iter().any(|variant| { variant .app_error @@ -45,7 +41,6 @@ fn expand_enum(input: &ErrorInput, variants: &[VariantData]) -> Result TokenStream let ident = &input.ident; let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); let kind = &spec.kind; - let body = if spec.expose_message { quote! { masterror::AppError::with(#kind, std::string::ToString::to_string(&value)) @@ -99,7 +93,6 @@ fn struct_app_error_impl(input: &ErrorInput, spec: &AppErrorSpec) -> TokenStream } } }; - quote! { impl #impl_generics core::convert::From<#ident #ty_generics> for masterror::AppError #where_clause { fn from(value: #ident #ty_generics) -> Self { @@ -113,7 +106,6 @@ fn struct_app_code_impl(input: &ErrorInput, spec: &AppErrorSpec) -> TokenStream let ident = &input.ident; let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); let code = spec.code.as_ref().expect("code presence checked"); - quote! { impl #impl_generics core::convert::From<#ident #ty_generics> for masterror::AppCode #where_clause { fn from(value: #ident #ty_generics) -> Self { @@ -127,7 +119,6 @@ fn struct_app_code_impl(input: &ErrorInput, spec: &AppErrorSpec) -> TokenStream fn enum_app_error_impl(input: &ErrorInput, variants: &[VariantData]) -> TokenStream { let ident = &input.ident; let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); - let mut arms = Vec::new(); for variant in variants { let spec = variant.app_error.as_ref().expect("presence checked"); @@ -147,7 +138,6 @@ fn enum_app_error_impl(input: &ErrorInput, variants: &[VariantData]) -> TokenStr }; arms.push(quote! { #pattern => #body }); } - quote! { impl #impl_generics core::convert::From<#ident #ty_generics> for masterror::AppError #where_clause { fn from(value: #ident #ty_generics) -> Self { @@ -162,7 +152,6 @@ fn enum_app_error_impl(input: &ErrorInput, variants: &[VariantData]) -> TokenStr fn enum_app_code_impl(input: &ErrorInput, variants: &[VariantData]) -> TokenStream { let ident = &input.ident; let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); - let mut arms = Vec::new(); for variant in variants { let spec = variant.app_error.as_ref().expect("presence checked"); @@ -170,7 +159,6 @@ fn enum_app_code_impl(input: &ErrorInput, variants: &[VariantData]) -> TokenStre let code = spec.code.as_ref().expect("code presence checked"); arms.push(quote! { #pattern => #code }); } - quote! { impl #impl_generics core::convert::From<#ident #ty_generics> for masterror::AppCode #where_clause { fn from(value: #ident #ty_generics) -> Self { diff --git a/masterror-derive/src/display/enum_impl.rs b/masterror-derive/src/display/enum_impl.rs index bf02253..05390c9 100644 --- a/masterror-derive/src/display/enum_impl.rs +++ b/masterror-derive/src/display/enum_impl.rs @@ -42,14 +42,11 @@ use crate::{ /// Token stream containing the complete Display trait implementation pub fn expand_enum(input: &ErrorInput, variants: &[VariantData]) -> Result { let mut arms = Vec::new(); - for variant in variants { arms.push(render_variant(variant)?); } - let ident = &input.ident; let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); - Ok(quote! { impl #impl_generics core::fmt::Display for #ident #ty_generics #where_clause { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { @@ -104,7 +101,6 @@ pub fn render_variant(variant: &VariantData) -> Result { /// Token stream containing the match arm with Display delegation pub fn render_variant_transparent(variant: &VariantData) -> Result { let variant_ident = &variant.ident; - match &variant.fields { Fields::Unit => Err(Error::new( variant.span, @@ -117,7 +113,6 @@ pub fn render_variant_transparent(variant: &VariantData) -> Result { @@ -129,7 +124,6 @@ pub fn render_variant_transparent(variant: &VariantData) -> Result unreachable!() }; - Ok(quote! { #pattern => core::fmt::Display::fmt(#binding, f) }) @@ -328,13 +322,11 @@ pub fn variant_tuple_placeholder( needs_pointer_value(&placeholder.formatter) )); } - if let Some(env) = env && let Some(resolved) = env.resolve_placeholder(placeholder)? { return Ok(resolved); } - match &placeholder.identifier { TemplateIdentifierSpec::Named(_) => { Err(placeholder_error(placeholder.span, &placeholder.identifier)) @@ -390,13 +382,11 @@ pub fn variant_named_placeholder( needs_pointer_value(&placeholder.formatter) )); } - if let Some(env) = env && let Some(resolved) = env.resolve_placeholder(placeholder)? { return Ok(resolved); } - match &placeholder.identifier { TemplateIdentifierSpec::Named(name) => { if let Some(index) = fields @@ -750,7 +740,6 @@ mod tests { use masterror_template::template::TemplateFormatter; use crate::template_support::{TemplateIdentifierSpec, TemplatePlaceholderSpec}; - let bindings = vec![format_ident!("field0")]; let placeholder = TemplatePlaceholderSpec { identifier: TemplateIdentifierSpec::Named("self".to_string()), @@ -770,7 +759,6 @@ mod tests { use masterror_template::template::TemplateFormatter; use crate::template_support::{TemplateIdentifierSpec, TemplatePlaceholderSpec}; - let bindings = vec![format_ident!("field0"), format_ident!("field1")]; let placeholder = TemplatePlaceholderSpec { identifier: TemplateIdentifierSpec::Positional(1), @@ -790,7 +778,6 @@ mod tests { use masterror_template::template::TemplateFormatter; use crate::template_support::{TemplateIdentifierSpec, TemplatePlaceholderSpec}; - let bindings = vec![format_ident!("field0")]; let placeholder = TemplatePlaceholderSpec { identifier: TemplateIdentifierSpec::Positional(5), @@ -808,7 +795,6 @@ mod tests { use masterror_template::template::TemplateFormatter; use crate::template_support::{TemplateIdentifierSpec, TemplatePlaceholderSpec}; - let bindings = vec![format_ident!("field0")]; let placeholder = TemplatePlaceholderSpec { identifier: TemplateIdentifierSpec::Implicit(0), @@ -826,7 +812,6 @@ mod tests { use masterror_template::template::TemplateFormatter; use crate::template_support::{TemplateIdentifierSpec, TemplatePlaceholderSpec}; - let bindings = vec![format_ident!("field0")]; let placeholder = TemplatePlaceholderSpec { identifier: TemplateIdentifierSpec::Named("unknown".to_string()), @@ -844,7 +829,6 @@ mod tests { use masterror_template::template::TemplateFormatter; use crate::template_support::{TemplateIdentifierSpec, TemplatePlaceholderSpec}; - let field = make_test_field("message", parse_quote!(String), 0); let fields = vec![field]; let bindings = vec![format_ident!("message")]; @@ -866,7 +850,6 @@ mod tests { use masterror_template::template::TemplateFormatter; use crate::template_support::{TemplateIdentifierSpec, TemplatePlaceholderSpec}; - let field = make_test_field("message", parse_quote!(String), 0); let fields = vec![field]; let bindings = vec![format_ident!("message")]; @@ -888,7 +871,6 @@ mod tests { use masterror_template::template::TemplateFormatter; use crate::template_support::{TemplateIdentifierSpec, TemplatePlaceholderSpec}; - let field = make_test_field("message", parse_quote!(String), 0); let fields = vec![field]; let bindings = vec![format_ident!("message")]; @@ -908,7 +890,6 @@ mod tests { use masterror_template::template::TemplateFormatter; use crate::template_support::{TemplateIdentifierSpec, TemplatePlaceholderSpec}; - let field = make_test_field("message", parse_quote!(String), 0); let fields = vec![field]; let bindings = vec![format_ident!("message")]; @@ -928,7 +909,6 @@ mod tests { use masterror_template::template::TemplateFormatter; use crate::template_support::{TemplateIdentifierSpec, TemplatePlaceholderSpec}; - let field = make_test_field("message", parse_quote!(String), 0); let fields = vec![field]; let bindings = vec![format_ident!("message")]; @@ -954,7 +934,6 @@ mod tests { ); let result = render_variant(&variant_transparent); assert!(result.is_ok()); - let variant_template = make_variant_data( "Template", Fields::Unit, @@ -964,7 +943,6 @@ mod tests { ); let result = render_variant(&variant_template); assert!(result.is_ok()); - let variant_formatter = make_variant_data( "Formatter", Fields::Unit, diff --git a/masterror-derive/src/display/format_args.rs b/masterror-derive/src/display/format_args.rs index 0a7df1a..e617ab1 100644 --- a/masterror-derive/src/display/format_args.rs +++ b/masterror-derive/src/display/format_args.rs @@ -131,19 +131,16 @@ impl<'a> FormatArgumentsEnv<'a> { positional: HashMap::new(), implicit: Vec::new() }; - for (index, arg) in spec.args.iter().enumerate() { let binding = match &arg.value { FormatArgValue::Expr(_) => Some(format_ident!("__masterror_format_arg_{}", index)), FormatArgValue::Shorthand(_) => None }; - let arg_index = env.args.len(); env.args.push(EnvFormatArg { binding: binding.clone(), arg }); - match &arg.kind { FormatBindingKind::Named(ident) => { env.named.insert(ident.to_string(), arg_index); @@ -157,7 +154,6 @@ impl<'a> FormatArgumentsEnv<'a> { } } } - env } @@ -208,7 +204,6 @@ impl<'a> FormatArgumentsEnv<'a> { placeholder: &TemplatePlaceholderSpec ) -> Result, Error> { use crate::template_support::TemplateIdentifierSpec; - let arg_index = match &placeholder.identifier { TemplateIdentifierSpec::Named(name) => self.named.get(name).copied(), TemplateIdentifierSpec::Positional(index) => self.positional.get(index).copied(), @@ -216,12 +211,10 @@ impl<'a> FormatArgumentsEnv<'a> { self.implicit.get(*index).and_then(|slot| *slot) } }; - let index = match arg_index { Some(index) => index, None => return Ok(None) }; - let resolved = self.args[index].resolved_expr(self, placeholder)?; Ok(Some(resolved)) } @@ -281,7 +274,6 @@ impl<'a> EnvFormatArg<'a> { placeholder: &TemplatePlaceholderSpec ) -> Result { use super::formatter::needs_pointer_value; - match (&self.binding, &self.arg.value) { (Some(binding), FormatArgValue::Expr(_)) => { if needs_pointer_value(&placeholder.formatter) { @@ -309,13 +301,11 @@ impl<'a> EnvFormatArg<'a> { "format argument expression binding was not generated" )) }?; - let kind = match &self.arg.kind { FormatBindingKind::Named(ident) => ResolvedFormatArgumentKind::Named(ident.clone()), FormatBindingKind::Positional(index) => ResolvedFormatArgumentKind::Positional(*index), FormatBindingKind::Implicit(index) => ResolvedFormatArgumentKind::Implicit(*index) }; - Ok(ResolvedFormatArgument { kind, expr @@ -329,15 +319,11 @@ fn resolve_struct_shorthand( placeholder: &TemplatePlaceholderSpec ) -> Result { use super::formatter::needs_pointer_value; - let FormatArgShorthand::Projection(projection) = shorthand; - let (expr, first_field, has_tail) = struct_projection_expr(fields, projection)?; - if !has_tail && let Some(field) = first_field { return Ok(struct_field_expr(field, &placeholder.formatter)); } - if needs_pointer_value(&placeholder.formatter) { Ok(ResolvedPlaceholderExpr::with(expr, false)) } else { @@ -352,16 +338,13 @@ fn resolve_variant_shorthand( placeholder: &TemplatePlaceholderSpec ) -> Result { use super::formatter::needs_pointer_value; - let FormatArgShorthand::Projection(projection) = shorthand; - let Some(first_segment) = projection.segments.first() else { return Err(Error::new( projection.span, "empty shorthand projection is not supported" )); }; - match first_segment { FormatArgProjectionSegment::Field(ident) => { let Fields::Named(named_fields) = fields else { @@ -373,34 +356,29 @@ fn resolve_variant_shorthand( ) )); }; - let position = named_fields.iter().position(|field| { field .ident .as_ref() .is_some_and(|field_ident| field_ident == ident) }); - let index = position.ok_or_else(|| { Error::new( ident.span(), format!("unknown field `{}` in format arguments", ident) ) })?; - let binding = bindings.get(index).ok_or_else(|| { Error::new( ident.span(), format!("field `{}` is not available in format arguments", ident) ) })?; - let expr = if projection.segments.len() == 1 { quote!(#binding) } else { append_projection_segments(quote!(#binding), &projection.segments[1..]) }; - if projection.segments.len() == 1 { Ok(ResolvedPlaceholderExpr::with( expr, @@ -422,20 +400,17 @@ fn resolve_variant_shorthand( "positional fields are not available for struct variants" )); }; - let binding = bindings.get(*index).ok_or_else(|| { Error::new( *span, format!("field `{}` is not available in format arguments", index) ) })?; - let expr = if projection.segments.len() == 1 { quote!(#binding) } else { append_projection_segments(quote!(#binding), &projection.segments[1..]) }; - if projection.segments.len() == 1 { Ok(ResolvedPlaceholderExpr::with( expr, @@ -469,14 +444,12 @@ fn resolve_variant_shorthand_argument( shorthand: &FormatArgShorthand ) -> Result { let FormatArgShorthand::Projection(projection) = shorthand; - let Some(first_segment) = projection.segments.first() else { return Err(Error::new( projection.span, "empty shorthand projection is not supported" )); }; - match first_segment { FormatArgProjectionSegment::Field(ident) => { let Fields::Named(named_fields) = fields else { @@ -488,28 +461,24 @@ fn resolve_variant_shorthand_argument( ) )); }; - let position = named_fields.iter().position(|field| { field .ident .as_ref() .is_some_and(|field_ident| field_ident == ident) }); - let index = position.ok_or_else(|| { Error::new( ident.span(), format!("unknown field `{}` in format arguments", ident) ) })?; - let binding = bindings.get(index).ok_or_else(|| { Error::new( ident.span(), format!("field `{}` is not available in format arguments", ident) ) })?; - if projection.segments.len() == 1 { Ok(quote!(#binding)) } else { @@ -529,14 +498,12 @@ fn resolve_variant_shorthand_argument( "positional fields are not available for struct variants" )); }; - let binding = bindings.get(*index).ok_or_else(|| { Error::new( *span, format!("field `{}` is not available in format arguments", index) ) })?; - if projection.segments.len() == 1 { Ok(quote!(#binding)) } else { @@ -558,14 +525,12 @@ fn struct_projection_expr<'a>( projection: &'a FormatArgProjection ) -> Result<(TokenStream, Option<&'a Field>, bool), Error> { use super::projection::append_method_call; - let Some(first) = projection.segments.first() else { return Err(Error::new( projection.span, "empty shorthand projection is not supported" )); }; - let mut first_field = None; let mut expr = match first { FormatArgProjectionSegment::Field(ident) => { @@ -595,11 +560,9 @@ fn struct_projection_expr<'a>( } FormatArgProjectionSegment::MethodCall(call) => append_method_call(quote!(self), call) }; - if projection.segments.len() > 1 { expr = append_projection_segments(expr, &projection.segments[1..]); } - Ok((expr, first_field, projection.segments.len() > 1)) } @@ -608,9 +571,7 @@ fn struct_field_expr( formatter: &masterror_template::template::TemplateFormatter ) -> ResolvedPlaceholderExpr { use super::{formatter::needs_pointer_value, placeholder::pointer_prefers_value}; - let member = &field.member; - if needs_pointer_value(formatter) && pointer_prefers_value(&field.ty) { ResolvedPlaceholderExpr::pointer(quote!(self.#member)) } else { diff --git a/masterror-derive/src/display/formatter.rs b/masterror-derive/src/display/formatter.rs index e599dc6..53e815d 100644 --- a/masterror-derive/src/display/formatter.rs +++ b/masterror-derive/src/display/formatter.rs @@ -69,7 +69,6 @@ pub fn format_placeholder( expr, pointer_value } = resolved; - match formatter { TemplateFormatter::Display { spec: Some(spec) diff --git a/masterror-derive/src/display/struct_impl.rs b/masterror-derive/src/display/struct_impl.rs index 18ec19d..7ae1811 100644 --- a/masterror-derive/src/display/struct_impl.rs +++ b/masterror-derive/src/display/struct_impl.rs @@ -62,10 +62,8 @@ pub fn expand_struct(input: &ErrorInput, data: &StructData) -> Result render_struct_formatter_path(&data.fields, path) }; - let ident = &input.ident; let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); - Ok(quote! { impl #impl_generics core::fmt::Display for #ident #ty_generics #where_clause { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { @@ -190,13 +188,11 @@ pub fn struct_placeholder_expr( needs_pointer_value(&placeholder.formatter) )); } - if let Some(env) = env && let Some(resolved) = env.resolve_placeholder(placeholder)? { return Ok(resolved); } - match &placeholder.identifier { TemplateIdentifierSpec::Named(name) => { if let Some(field) = fields.get_named(name) { @@ -234,7 +230,6 @@ pub fn struct_field_expr( formatter: &masterror_template::template::TemplateFormatter ) -> ResolvedPlaceholderExpr { let member = &field.member; - if needs_pointer_value(formatter) && pointer_prefers_value(&field.ty) { ResolvedPlaceholderExpr::pointer(quote!(self.#member)) } else { @@ -507,7 +502,6 @@ mod tests { use masterror_template::template::TemplateFormatter; use crate::template_support::{TemplateIdentifierSpec, TemplatePlaceholderSpec}; - let fields = Fields::Unit; let placeholder = TemplatePlaceholderSpec { identifier: TemplateIdentifierSpec::Named("self".to_string()), @@ -527,7 +521,6 @@ mod tests { use masterror_template::template::TemplateFormatter; use crate::template_support::{TemplateIdentifierSpec, TemplatePlaceholderSpec}; - let field = make_test_field("message", parse_quote!(String), 0); let fields = Fields::Named(vec![field]); let placeholder = TemplatePlaceholderSpec { @@ -549,7 +542,6 @@ mod tests { use masterror_template::template::TemplateFormatter; use crate::template_support::{TemplateIdentifierSpec, TemplatePlaceholderSpec}; - let fields = Fields::Unit; let placeholder = TemplatePlaceholderSpec { identifier: TemplateIdentifierSpec::Named("unknown".to_string()), @@ -567,7 +559,6 @@ mod tests { use masterror_template::template::TemplateFormatter; use crate::template_support::{TemplateIdentifierSpec, TemplatePlaceholderSpec}; - let field = make_test_unnamed_field(parse_quote!(i32), 0); let fields = Fields::Unnamed(vec![field]); let placeholder = TemplatePlaceholderSpec { @@ -586,7 +577,6 @@ mod tests { use masterror_template::template::TemplateFormatter; use crate::template_support::{TemplateIdentifierSpec, TemplatePlaceholderSpec}; - let fields = Fields::Unit; let placeholder = TemplatePlaceholderSpec { identifier: TemplateIdentifierSpec::Positional(5), @@ -604,7 +594,6 @@ mod tests { use masterror_template::template::TemplateFormatter; use crate::template_support::{TemplateIdentifierSpec, TemplatePlaceholderSpec}; - let field = make_test_unnamed_field(parse_quote!(String), 0); let fields = Fields::Unnamed(vec![field]); let placeholder = TemplatePlaceholderSpec { @@ -621,7 +610,6 @@ mod tests { #[test] fn test_struct_field_expr_with_display() { use masterror_template::template::TemplateFormatter; - let field = make_test_field("value", parse_quote!(String), 0); let formatter = TemplateFormatter::Display { spec: None @@ -635,7 +623,6 @@ mod tests { #[test] fn test_struct_field_expr_with_pointer() { use masterror_template::template::TemplateFormatter; - let field = make_test_field("ptr", parse_quote!(*const i32), 0); let formatter = TemplateFormatter::Pointer { alternate: false @@ -649,7 +636,6 @@ mod tests { #[test] fn test_struct_field_expr_with_reference() { use masterror_template::template::TemplateFormatter; - let field = make_test_field("ref_val", parse_quote!(&str), 0); let formatter = TemplateFormatter::Pointer { alternate: false @@ -719,7 +705,6 @@ mod tests { #[test] fn test_struct_field_expr_unnamed_field() { use masterror_template::template::TemplateFormatter; - let field = make_test_unnamed_field(parse_quote!(String), 0); let formatter = TemplateFormatter::Display { spec: None diff --git a/masterror-derive/src/display/template.rs b/masterror-derive/src/display/template.rs index b09a95e..c183dab 100644 --- a/masterror-derive/src/display/template.rs +++ b/masterror-derive/src/display/template.rs @@ -105,7 +105,6 @@ where let mut has_placeholder = false; let mut has_implicit_placeholders = false; let mut requires_format_engine = false; - for segment in &template.segments { match segment { TemplateSegmentSpec::Literal(text) => { @@ -121,7 +120,6 @@ where if placeholder_requires_format_engine(&placeholder.formatter) { requires_format_engine = true; } - let resolved = resolver(placeholder)?; format_buffer.push_str(&placeholder_format_fragment(placeholder)); segments.push(RenderedSegment::Placeholder(PlaceholderRender { @@ -133,9 +131,7 @@ where } } } - let has_additional_arguments = !preludes.is_empty() || !format_args.is_empty(); - if !has_placeholder && !has_additional_arguments { let literal = Literal::string(&literal_buffer); return Ok(quote! { @@ -143,7 +139,6 @@ where f.write_str(#literal) }); } - if has_additional_arguments || has_implicit_placeholders || requires_format_engine { let format_literal = Literal::string(&format_buffer); let args = build_template_arguments(&segments, format_args); @@ -152,7 +147,6 @@ where ::core::write!(f, #format_literal #(, #args)*) }); } - let mut pieces = preludes; for segment in segments { match segment { @@ -168,7 +162,6 @@ where } } pieces.push(quote! { Ok(()) }); - Ok(quote! { #(#pieces)* }) @@ -195,12 +188,10 @@ pub fn build_template_arguments( let mut named = Vec::new(); let mut positional = Vec::new(); let mut implicit = Vec::new(); - for segment in segments { let RenderedSegment::Placeholder(placeholder) = segment else { continue; }; - match &placeholder.identifier { TemplateIdentifierSpec::Named(name) => { if named @@ -209,7 +200,6 @@ pub fn build_template_arguments( { continue; } - named.push(NamedArgument { name: name.clone(), span: placeholder.span, @@ -223,7 +213,6 @@ pub fn build_template_arguments( { continue; } - positional.push(IndexedArgument { index: *index, expr: placeholder.resolved.expr_tokens() @@ -236,7 +225,6 @@ pub fn build_template_arguments( { continue; } - implicit.push(IndexedArgument { index: *index, expr: placeholder.resolved.expr_tokens() @@ -244,7 +232,6 @@ pub fn build_template_arguments( } } } - for argument in format_args { match argument.kind { ResolvedFormatArgumentKind::Named(ident) => { @@ -255,7 +242,6 @@ pub fn build_template_arguments( { continue; } - let span = ident.span(); named.push(NamedArgument { name, @@ -273,7 +259,6 @@ pub fn build_template_arguments( { continue; } - positional.push(IndexedArgument { index, expr: argument.expr @@ -286,7 +271,6 @@ pub fn build_template_arguments( { continue; } - implicit.push(IndexedArgument { index, expr: argument.expr @@ -294,10 +278,8 @@ pub fn build_template_arguments( } } } - positional.sort_by_key(|argument| argument.index); implicit.sort_by_key(|argument| argument.index); - let mut arguments = Vec::with_capacity(named.len() + positional.len() + implicit.len()); for IndexedArgument { expr, .. @@ -320,7 +302,6 @@ pub fn build_template_arguments( let ident = format_ident!("{}", name, span = span); arguments.push(quote_spanned!(span => #ident = #expr)); } - arguments } @@ -358,18 +339,15 @@ pub fn push_literal_fragment(buffer: &mut String, literal: &str) { /// String containing the format string fragment (e.g., `"{name:?}"` or `"{0}"`) pub fn placeholder_format_fragment(placeholder: &TemplatePlaceholderSpec) -> String { let mut fragment = String::from("{"); - match &placeholder.identifier { TemplateIdentifierSpec::Named(name) => fragment.push_str(name), TemplateIdentifierSpec::Positional(index) => fragment.push_str(&index.to_string()), TemplateIdentifierSpec::Implicit(_) => {} } - if let Some(spec) = formatter_format_fragment(&placeholder.formatter) { fragment.push(':'); fragment.push_str(spec.as_ref()); } - fragment.push('}'); fragment } diff --git a/masterror-derive/src/error_trait.rs b/masterror-derive/src/error_trait.rs index 161b9fc..b623f67 100644 --- a/masterror-derive/src/error_trait.rs +++ b/masterror-derive/src/error_trait.rs @@ -105,10 +105,8 @@ fn expand_struct(input: &ErrorInput, data: &StructData) -> Result Option<&(dyn std::error::Error + 'static)> { @@ -138,15 +136,12 @@ fn expand_enum(input: &ErrorInput, variants: &[VariantData]) -> Result Option<&(dyn std::error::Error + 'static)> { @@ -217,7 +212,6 @@ mod tests { generics: parse_quote!(), data: ErrorData::Enum(vec![variant]) }; - let result = expand(&input); assert!(result.is_ok()); let tokens = result.expect("valid tokens"); diff --git a/masterror-derive/src/error_trait/backtrace.rs b/masterror-derive/src/error_trait/backtrace.rs index 8cefa7d..868575a 100644 --- a/masterror-derive/src/error_trait/backtrace.rs +++ b/masterror-derive/src/error_trait/backtrace.rs @@ -59,7 +59,6 @@ pub(crate) fn enum_backtrace_method(variants: &[VariantData]) -> Option Option TokenStream { let variant_ident = &variant.ident; let backtrace_field = variant.fields.backtrace_field(); - match (&variant.fields, backtrace_field) { (Fields::Unit, _) => quote! { Self::#variant_ident => None }, (Fields::Named(fields), Some(backtrace)) => { @@ -202,7 +200,6 @@ mod tests { fn test_struct_backtrace_method_with_backtrace_field() { let field = make_field_with_backtrace(Some("bt"), 0); let fields = Fields::Named(vec![field]); - let result = struct_backtrace_method(&fields); assert!(result.is_some()); let output = result.expect("backtrace method").to_string(); @@ -231,7 +228,6 @@ mod tests { masterror: None, span: Span::call_site() }; - let result = enum_backtrace_method(&[variant]); assert!(result.is_some()); let output = result.expect("backtrace method").to_string(); @@ -252,7 +248,6 @@ mod tests { masterror: None, span: Span::call_site() }; - let result = enum_backtrace_method(&[variant]); assert!(result.is_none()); } @@ -270,7 +265,6 @@ mod tests { masterror: None, span: Span::call_site() }; - let result = variant_backtrace_arm(&variant); assert!(result.to_string().contains("Self :: Error => None")); } @@ -289,7 +283,6 @@ mod tests { masterror: None, span: Span::call_site() }; - let result = variant_backtrace_arm(&variant); let output = result.to_string(); assert!(output.contains("Self :: WithBacktrace")); @@ -310,7 +303,6 @@ mod tests { masterror: None, span: Span::call_site() }; - let result = variant_backtrace_arm(&variant); let output = result.to_string(); assert!(output.contains("Self :: Error")); @@ -323,7 +315,6 @@ mod tests { use syn::parse_quote; use crate::input::{Field, FieldAttrs}; - let field = Field { ident: Some(syn::Ident::new("value", Span::call_site())), member: syn::Member::Named(syn::Ident::new("value", Span::call_site())), @@ -343,7 +334,6 @@ mod tests { masterror: None, span: Span::call_site() }; - let result = variant_backtrace_arm(&variant); let output = result.to_string(); assert!(output.contains("Self :: Error")); @@ -357,7 +347,6 @@ mod tests { use syn::parse_quote; use crate::input::{Field, FieldAttrs}; - let field = Field { ident: None, member: syn::Member::Unnamed(syn::Index { @@ -380,7 +369,6 @@ mod tests { masterror: None, span: Span::call_site() }; - let result = variant_backtrace_arm(&variant); let output = result.to_string(); assert!(output.contains("Self :: Error")); diff --git a/masterror-derive/src/error_trait/provide.rs b/masterror-derive/src/error_trait/provide.rs index 4832531..1f61683 100644 --- a/masterror-derive/src/error_trait/provide.rs +++ b/masterror-derive/src/error_trait/provide.rs @@ -37,7 +37,6 @@ pub(crate) fn struct_provide_method(fields: &Fields) -> Option { }); let mut statements = Vec::new(); let mut needs_trait_import = false; - if let Some(source_field) = source_field { needs_trait_import = true; let member = &source_field.member; @@ -47,7 +46,6 @@ pub(crate) fn struct_provide_method(fields: &Fields) -> Option { &request )); } - if let Some(backtrace) = backtrace && backtrace.stores_backtrace() && source_field.is_none_or(|source| source.index != backtrace.index()) @@ -60,7 +58,6 @@ pub(crate) fn struct_provide_method(fields: &Fields) -> Option { &request )); } - for field in fields.iter() { if field.attrs.provides.is_empty() { continue; @@ -71,17 +68,14 @@ pub(crate) fn struct_provide_method(fields: &Fields) -> Option { statements.extend(provide_custom_tokens(expr.clone(), field, spec, &request)); } } - if statements.is_empty() { return None; } - let trait_import = if needs_trait_import { quote! { use masterror::provide::ThiserrorProvide as _; } } else { TokenStream::new() }; - Some(quote! { #[cfg(masterror_has_error_generic_member_access)] fn provide<'a>(&'a self, #request: &mut core::error::Request<'a>) { @@ -109,7 +103,6 @@ pub(crate) fn enum_provide_method(variants: &[VariantData]) -> Option Option(&'a self, #request: &mut core::error::Request<'a>) { @@ -158,7 +148,6 @@ pub(crate) fn variant_provide_arm_tokens( let variant_ident = &variant.ident; let backtrace = variant.fields.backtrace_field(); let source_field = variant.fields.iter().find(|field| field.attrs.has_source()); - match &variant.fields { Fields::Unit => quote! { Self::#variant_ident => {} }, Fields::Named(fields) => variant_provide_named_arm( @@ -200,13 +189,11 @@ pub(crate) fn variant_provide_named_arm( let mut backtrace_binding = None; let mut source_binding = None; let mut provide_bindings: Vec<(Ident, &Field)> = Vec::new(); - for field in fields { let ident = field.ident.clone().expect("named field"); let needs_binding = backtrace.is_some_and(|candidate| candidate.index() == field.index) || source.is_some_and(|candidate| candidate.index == field.index) || !field.attrs.provides.is_empty(); - if needs_binding { let binding = binding_ident(field); let pattern_binding = binding.clone(); @@ -215,15 +202,12 @@ pub(crate) fn variant_provide_named_arm( } else { entries.push(quote!(#ident: #pattern_binding)); } - if backtrace.is_some_and(|candidate| candidate.index() == field.index) { backtrace_binding = Some(binding.clone()); } - if source.is_some_and(|candidate| candidate.index == field.index) { source_binding = Some(binding.clone()); } - if !field.attrs.provides.is_empty() { provide_bindings.push((binding, field)); } @@ -231,9 +215,7 @@ pub(crate) fn variant_provide_named_arm( entries.push(quote!(#ident: _)); } } - let mut statements = Vec::new(); - if let Some(source_field) = source { *needs_trait_import = true; let binding = source_binding.expect("source binding"); @@ -243,7 +225,6 @@ pub(crate) fn variant_provide_named_arm( request )); } - if let Some(backtrace_field) = backtrace && backtrace_field.stores_backtrace() && !same_as_source @@ -256,7 +237,6 @@ pub(crate) fn variant_provide_named_arm( request )); } - for (binding, field) in provide_bindings { let binding_expr = quote!(#binding); for spec in &field.attrs.provides { @@ -268,9 +248,7 @@ pub(crate) fn variant_provide_named_arm( )); } } - let pattern = quote!(Self::#variant_ident { #(#entries),* }); - if statements.is_empty() { quote! { #pattern => {} } } else { @@ -298,25 +276,20 @@ pub(crate) fn variant_provide_unnamed_arm( let mut backtrace_binding = None; let mut source_binding = None; let mut provide_bindings: Vec<(Ident, &Field)> = Vec::new(); - for (index, field) in fields.iter().enumerate() { let needs_binding = backtrace.is_some_and(|candidate| candidate.index() == index) || source.is_some_and(|candidate| candidate.index == index) || !field.attrs.provides.is_empty(); - if needs_binding { let binding = binding_ident(field); let pattern_binding = binding.clone(); elements.push(quote!(#pattern_binding)); - if backtrace.is_some_and(|candidate| candidate.index() == index) { backtrace_binding = Some(binding.clone()); } - if source.is_some_and(|candidate| candidate.index == index) { source_binding = Some(binding.clone()); } - if !field.attrs.provides.is_empty() { provide_bindings.push((binding, field)); } @@ -324,9 +297,7 @@ pub(crate) fn variant_provide_unnamed_arm( elements.push(quote!(_)); } } - let mut statements = Vec::new(); - if let Some(source_field) = source { *needs_trait_import = true; let binding = source_binding.expect("source binding"); @@ -336,7 +307,6 @@ pub(crate) fn variant_provide_unnamed_arm( request )); } - if let Some(backtrace_field) = backtrace && backtrace_field.stores_backtrace() && !same_as_source @@ -349,7 +319,6 @@ pub(crate) fn variant_provide_unnamed_arm( request )); } - for (binding, field) in provide_bindings { let binding_expr = quote!(#binding); for spec in &field.attrs.provides { @@ -361,13 +330,11 @@ pub(crate) fn variant_provide_unnamed_arm( )); } } - let pattern = if elements.is_empty() { quote!(Self::#variant_ident()) } else { quote!(Self::#variant_ident(#(#elements),*)) }; - if statements.is_empty() { quote! { #pattern => {} } } else { @@ -526,7 +493,6 @@ mod tests { masterror: None, span: Span::call_site() }; - let result = enum_provide_method(&[variant]); assert!(result.is_none()); } @@ -539,7 +505,6 @@ mod tests { }; let request = quote!(req); let expr = quote!(self.source); - let result = provide_source_tokens(expr, &field, &request); let output = result.to_string(); assert!(output.contains("if let Some")); @@ -554,7 +519,6 @@ mod tests { }; let request = quote!(req); let expr = quote!(self.source); - let result = provide_source_tokens(expr, &field, &request); let output = result.to_string(); assert!(output.contains("thiserror_provide")); @@ -569,7 +533,6 @@ mod tests { }; let request = quote!(req); let expr = quote!(self.bt); - let result = provide_backtrace_tokens(expr, &field, &request); let output = result.to_string(); assert!(output.contains("if let Some")); @@ -584,7 +547,6 @@ mod tests { }; let request = quote!(req); let expr = quote!(self.bt); - let result = provide_backtrace_tokens(expr, &field, &request); let output = result.to_string(); assert!(output.contains("provide_ref")); @@ -601,7 +563,6 @@ mod tests { }; let request = quote!(req); let expr = quote!(self.trace_id); - let result = provide_custom_tokens(expr, &field, &spec, &request); assert_eq!(result.len(), 1); let output = result[0].to_string(); @@ -619,7 +580,6 @@ mod tests { }; let request = quote!(req); let expr = quote!(self.span_id); - let result = provide_custom_tokens(expr, &field, &spec, &request); assert_eq!(result.len(), 1); let output = result[0].to_string(); @@ -637,7 +597,6 @@ mod tests { }; let request = quote!(req); let expr = quote!(self.data); - let result = provide_custom_tokens(expr, &field, &spec, &request); assert_eq!(result.len(), 2); let output0 = result[0].to_string(); @@ -659,7 +618,6 @@ mod tests { }; let request = quote!(req); let expr = quote!(self.data); - let result = provide_custom_tokens(expr, &field, &spec, &request); assert_eq!(result.len(), 1); let output = result[0].to_string(); diff --git a/masterror-derive/src/error_trait/source.rs b/masterror-derive/src/error_trait/source.rs index b335062..1665fdb 100644 --- a/masterror-derive/src/error_trait/source.rs +++ b/masterror-derive/src/error_trait/source.rs @@ -116,7 +116,6 @@ fn variant_transparent_source(variant: &VariantData) -> TokenStream { fn variant_template_source(variant: &VariantData) -> TokenStream { let variant_ident = &variant.ident; let source_field = variant.fields.iter().find(|field| field.attrs.has_source()); - match (&variant.fields, source_field) { (Fields::Unit, _) => quote! { Self::#variant_ident => None }, (_, None) => match &variant.fields { @@ -224,7 +223,6 @@ mod tests { let display = DisplaySpec::Transparent { attribute: Box::new(syn::parse_quote!(#[error(transparent)])) }; - let result = struct_source_body(&fields, &display); let output = result.to_string(); assert!(output.contains("std :: error :: Error :: source")); @@ -237,7 +235,6 @@ mod tests { let display = DisplaySpec::Transparent { attribute: Box::new(syn::parse_quote!(#[error(transparent)])) }; - let result = struct_source_body(&fields, &display); assert_eq!(result.to_string(), "None"); } @@ -249,7 +246,6 @@ mod tests { let display = DisplaySpec::Template(DisplayTemplate { segments: vec![TemplateSegmentSpec::Literal("error".to_string())] }); - let result = struct_source_body(&fields, &display); let output = result.to_string(); assert!(output.contains("self . cause")); @@ -263,7 +259,6 @@ mod tests { let display = DisplaySpec::Template(DisplayTemplate { segments: vec![TemplateSegmentSpec::Literal("error".to_string())] }); - let result = struct_source_body(&fields, &display); assert_eq!(result.to_string(), "None"); } @@ -281,7 +276,6 @@ mod tests { masterror: None, span: Span::call_site() }; - let result = variant_source_arm(&variant); assert!(result.to_string().contains("Self :: Error => None")); } @@ -300,7 +294,6 @@ mod tests { masterror: None, span: Span::call_site() }; - let result = variant_source_arm(&variant); let output = result.to_string(); assert!(output.contains("Self :: Wrapped")); @@ -321,7 +314,6 @@ mod tests { masterror: None, span: Span::call_site() }; - let result = variant_source_arm(&variant); let output = result.to_string(); assert!(output.contains("Self :: Wrapped")); @@ -343,7 +335,6 @@ mod tests { masterror: None, span: Span::call_site() }; - let result = variant_source_arm(&variant); let output = result.to_string(); assert!(output.contains("Self :: Multi")); @@ -365,7 +356,6 @@ mod tests { masterror: None, span: Span::call_site() }; - let result = variant_source_arm(&variant); let output = result.to_string(); assert!(output.contains("Self :: Error")); @@ -386,7 +376,6 @@ mod tests { masterror: None, span: Span::call_site() }; - let result = variant_source_arm(&variant); let output = result.to_string(); assert!(output.contains("Self :: Error")); @@ -408,7 +397,6 @@ mod tests { masterror: None, span: Span::call_site() }; - let result = variant_source_arm(&variant); let output = result.to_string(); assert!(output.contains("Self :: Error")); @@ -429,7 +417,6 @@ mod tests { masterror: None, span: Span::call_site() }; - let result = variant_source_arm(&variant); let output = result.to_string(); assert!(output.contains("Self :: Error")); diff --git a/masterror-derive/src/from_impl.rs b/masterror-derive/src/from_impl.rs index a6250c9..56227b4 100644 --- a/masterror-derive/src/from_impl.rs +++ b/masterror-derive/src/from_impl.rs @@ -12,7 +12,6 @@ use crate::input::{ pub fn expand(input: &ErrorInput) -> Result, Error> { let mut impls = Vec::new(); - match &input.data { ErrorData::Struct(data) => { if let Some(field) = data.fields.first_from_field() { @@ -27,7 +26,6 @@ pub fn expand(input: &ErrorInput) -> Result, Error> { } } } - Ok(impls) } @@ -39,9 +37,7 @@ fn struct_from_impl( let ident = &input.ident; let ty = &field.ty; let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); - let constructor = struct_constructor(&data.fields, field)?; - Ok(quote! { impl #impl_generics core::convert::From<#ty> for #ident #ty_generics #where_clause { fn from(value: #ty) -> Self { @@ -60,9 +56,7 @@ fn enum_from_impl( let ty = &field.ty; let variant_ident = &variant.ident; let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); - let constructor = variant_constructor(variant_ident, &variant.fields, field)?; - Ok(quote! { impl #impl_generics core::convert::From<#ty> for #ident #ty_generics #where_clause { fn from(value: #ty) -> Self { @@ -130,15 +124,12 @@ fn field_value_expr(field: &Field, from_field: &Field) -> Result), make_field_attrs_with_source() ); - let result = field_value_expr(&source_field, &from_field); assert!(result.is_ok()); let tokens = result.unwrap().to_string(); @@ -682,7 +655,6 @@ mod tests { parse_quote!(String), make_field_attrs_with_source() ); - let result = field_value_expr(&source_field, &from_field); assert!(result.is_err()); let err = result.unwrap_err(); @@ -703,7 +675,6 @@ mod tests { parse_quote!(String), make_field_attrs_plain() ); - let result = field_value_expr(&plain_field, &from_field); assert!(result.is_err()); let err = result.unwrap_err(); @@ -721,7 +692,6 @@ mod tests { parse_quote!(Option), make_field_attrs_with_source() ); - let result = source_initializer(&source_field); assert!(result.is_ok()); let tokens = result.unwrap().to_string(); @@ -736,7 +706,6 @@ mod tests { parse_quote!(String), make_field_attrs_with_source() ); - let result = source_initializer(&source_field); assert!(result.is_err()); let err = result.unwrap_err(); @@ -751,7 +720,6 @@ mod tests { parse_quote!(Option), make_field_attrs_with_backtrace() ); - let tokens = backtrace_initializer(&backtrace_field); let result = tokens.to_string(); assert!(result.contains("Option :: Some")); @@ -766,7 +734,6 @@ mod tests { parse_quote!(std::backtrace::Backtrace), make_field_attrs_with_backtrace() ); - let tokens = backtrace_initializer(&backtrace_field); let result = tokens.to_string(); assert!(result.contains("Backtrace :: capture")); @@ -794,7 +761,6 @@ mod tests { make_field_attrs_with_source() ); let fields = Fields::Named(vec![from_field, backtrace_field, source_field]); - let result = struct_constructor(&fields, fields.iter().next().unwrap()); assert!(result.is_ok()); let tokens = result.unwrap().to_string(); @@ -819,7 +785,6 @@ mod tests { ); let fields = Fields::Unnamed(vec![from_field, backtrace_field]); let variant_ident = syn::Ident::new("Io", Span::call_site()); - let result = variant_constructor(&variant_ident, &fields, fields.iter().next().unwrap()); assert!(result.is_ok()); let tokens = result.unwrap().to_string(); @@ -847,7 +812,6 @@ mod tests { }; let data = ErrorData::Enum(vec![variant]); let input = make_error_input("MyError", data); - let result = expand(&input); assert!(result.is_ok()); let impls = result.unwrap(); @@ -887,7 +851,6 @@ mod tests { })) ); input.generics = parse_quote!(); - let result = struct_from_impl( &input, &struct_data, diff --git a/masterror-derive/src/input/parse.rs b/masterror-derive/src/input/parse.rs index c672892..c6c776c 100644 --- a/masterror-derive/src/input/parse.rs +++ b/masterror-derive/src/input/parse.rs @@ -23,10 +23,8 @@ use super::{ /// Main entry point for parsing error definitions from syn AST. pub fn parse_input(input: DeriveInput) -> Result { let mut errors = Vec::new(); - let ident = input.ident; let generics = input.generics; - let data = match input.data { Data::Struct(data) => parse_struct(&ident, &input.attrs, data, &mut errors), Data::Enum(data) => parse_enum(&input.attrs, data, &mut errors), @@ -38,14 +36,12 @@ pub fn parse_input(input: DeriveInput) -> Result { Err(()) } }; - let data = match data { Ok(value) => value, Err(()) => { return Err(collect_errors(errors)); } }; - if errors.is_empty() { Ok(ErrorInput { ident, @@ -68,11 +64,9 @@ fn parse_struct( let app_error = extract_app_error_spec(attrs, errors)?; let masterror = extract_masterror_spec(attrs, errors)?; let fields = Fields::from_syn(&data.fields, errors); - validate_from_usage(&fields, &display, errors); validate_backtrace_usage(&fields, errors); validate_transparent(&fields, &display, errors, None); - Ok(ErrorData::Struct(Box::new(StructData { fields, display, @@ -96,12 +90,10 @@ fn parse_enum( )); } } - let mut variants = Vec::new(); for variant in data.variants { variants.push(parse_variant(variant, errors)?); } - Ok(ErrorData::Enum(variants)) } @@ -116,16 +108,13 @@ fn parse_variant(variant: syn::Variant, errors: &mut Vec) -> Result Result, ()> { let mut spec = None; let mut had_error = false; - for attr in attrs { if !path_is(attr, "masterror") { continue; } - if spec.is_some() { errors.push(Error::new_spanned( attr, @@ -46,7 +44,6 @@ pub(crate) fn extract_masterror_spec( had_error = true; continue; } - match parse_masterror_attribute(attr) { Ok(parsed) => spec = Some(parsed), Err(err) => { @@ -55,7 +52,6 @@ pub(crate) fn extract_masterror_spec( } } } - if had_error { Err(()) } else { Ok(spec) } } @@ -66,12 +62,10 @@ pub(crate) fn extract_app_error_spec( ) -> Result, ()> { let mut spec = None; let mut had_error = false; - for attr in attrs { if !path_is(attr, "app_error") { continue; } - if spec.is_some() { errors.push(Error::new_spanned( attr, @@ -80,7 +74,6 @@ pub(crate) fn extract_app_error_spec( had_error = true; continue; } - match parse_app_error_attribute(attr) { Ok(parsed) => spec = Some(parsed), Err(err) => { @@ -89,7 +82,6 @@ pub(crate) fn extract_app_error_spec( } } } - if had_error { Err(()) } else { Ok(spec) } } @@ -101,25 +93,20 @@ pub(crate) fn extract_display_spec( ) -> Result { let mut display = None; let mut saw_error_attribute = false; - for attr in attrs { if !path_is(attr, "error") { continue; } - saw_error_attribute = true; - if display.is_some() { errors.push(Error::new_spanned(attr, "duplicate #[error] attribute")); continue; } - match parse_error_attribute(attr) { Ok(spec) => display = Some(spec), Err(err) => errors.push(err) } } - match display { Some(spec) => Ok(spec), None => { @@ -137,7 +124,6 @@ fn parse_app_error_attribute(attr: &Attribute) -> Result { let mut kind = None; let mut code = None; let mut expose_message = false; - while !input.is_empty() { let ident: Ident = input.parse()?; let name = ident.to_string(); @@ -177,7 +163,6 @@ fn parse_app_error_attribute(attr: &Attribute) -> Result { )); } } - if input.peek(Token![,]) { input.parse::()?; } else if !input.is_empty() { @@ -187,7 +172,6 @@ fn parse_app_error_attribute(attr: &Attribute) -> Result { )); } } - let kind = match kind { Some(kind) => kind, None => { @@ -197,7 +181,6 @@ fn parse_app_error_attribute(attr: &Attribute) -> Result { )); } }; - Ok(AppErrorSpec { kind, code, @@ -218,7 +201,6 @@ fn parse_masterror_attribute(attr: &Attribute) -> Result { let mut telemetry = None; let mut map_grpc = None; let mut map_problem = None; - while !input.is_empty() { let ident: Ident = input.call(Ident::parse_any)?; match ident.to_string().as_str() { @@ -298,7 +280,6 @@ fn parse_masterror_attribute(attr: &Attribute) -> Result { )); } } - if input.peek(Token![,]) { input.parse::()?; } else if !input.is_empty() { @@ -308,7 +289,6 @@ fn parse_masterror_attribute(attr: &Attribute) -> Result { )); } } - let code = match code { Some(value) => value, None => { @@ -318,7 +298,6 @@ fn parse_masterror_attribute(attr: &Attribute) -> Result { )); } }; - let category = match category { Some(value) => value, None => { @@ -328,7 +307,6 @@ fn parse_masterror_attribute(attr: &Attribute) -> Result { )); } }; - Ok(MasterrorSpec { code, category, @@ -357,13 +335,10 @@ fn parse_flag_value(input: ParseStream) -> Result { fn parse_redact_block(input: ParseStream, span: Span) -> Result { let content; syn::parenthesized!(content in input); - if content.is_empty() { return Err(Error::new(span, "redact(...) requires at least one option")); } - let mut spec = RedactSpec::default(); - while !content.is_empty() { let ident: Ident = content.call(Ident::parse_any)?; match ident.to_string().as_str() { @@ -395,7 +370,6 @@ fn parse_redact_block(input: ParseStream, span: Span) -> Result()?; } else if !content.is_empty() { @@ -405,7 +379,6 @@ fn parse_redact_block(input: ParseStream, span: Span) -> Result Result, Error> { let inner; syn::parenthesized!(inner in *content); - if inner.is_empty() { return Err(Error::new( span, "redact(fields(...)) requires at least one field" )); } - let mut fields = Vec::new(); while !inner.is_empty() { let name: LitStr = inner.parse()?; @@ -449,7 +420,6 @@ fn parse_redact_fields( name, policy }); - if inner.peek(Token![,]) { inner.parse::()?; } else if !inner.is_empty() { @@ -459,7 +429,6 @@ fn parse_redact_fields( )); } } - Ok(fields) } @@ -467,13 +436,10 @@ fn parse_redact_fields( fn parse_telemetry_block(input: ParseStream, span: Span) -> Result, Error> { let content; syn::parenthesized!(content in input); - let mut entries = Vec::new(); - while !content.is_empty() { let expr: Expr = content.parse()?; entries.push(expr); - if content.peek(Token![,]) { content.parse::()?; if content.is_empty() { @@ -489,7 +455,6 @@ fn parse_telemetry_block(input: ParseStream, span: Span) -> Result, Er )); } } - Ok(entries) } @@ -499,20 +464,17 @@ fn parse_error_attribute(attr: &Attribute) -> Result { syn::custom_keyword!(transparent); syn::custom_keyword!(fmt); } - attr.parse_args_with(|input: ParseStream| { if input.peek(LitStr) { let lit: LitStr = input.parse()?; let template = parse_display_template(lit)?; let args = parse_format_args(input)?; - if !input.is_empty() { return Err(Error::new( input.span(), "unexpected tokens after format arguments" )); } - if args.args.is_empty() { Ok(DisplaySpec::Template(template)) } else { @@ -523,14 +485,12 @@ fn parse_error_attribute(attr: &Attribute) -> Result { } } else if input.peek(kw::transparent) { let _: kw::transparent = input.parse()?; - if !input.is_empty() { return Err(Error::new( input.span(), "format arguments are not supported with #[error(transparent)]" )); } - Ok(DisplaySpec::Transparent { attribute: Box::new(attr.clone()) }) @@ -539,7 +499,6 @@ fn parse_error_attribute(attr: &Attribute) -> Result { input.parse::()?; let path: ExprPath = input.parse()?; let args = parse_format_args(input)?; - for arg in &args.args { if let FormatBindingKind::Named(ident) = &arg.kind && ident == "fmt" @@ -547,14 +506,12 @@ fn parse_error_attribute(attr: &Attribute) -> Result { return Err(Error::new(arg.span, "duplicate `fmt` handler specified")); } } - if !input.is_empty() { return Err(Error::new( input.span(), "`fmt = ...` cannot be combined with additional arguments" )); } - Ok(DisplaySpec::FormatterPath { path, args @@ -573,7 +530,6 @@ pub(crate) fn parse_provide_attribute(attr: &Attribute) -> Result Result()?; } else if !input.is_empty() { @@ -611,14 +566,12 @@ pub(crate) fn parse_provide_attribute(attr: &Attribute) -> Result Result { let mut args = FormatArgsSpec::default(); - if input.is_empty() { return Ok(args); } - let leading_comma = if input.peek(Token![,]) { let comma: Token![,] = input.parse()?; Some(comma.span) } else { None }; - if input.is_empty() { if let Some(span) = leading_comma { return Err(Error::new(span, "expected format argument after comma")); } return Ok(args); } - let parsed = syn::punctuated::Punctuated::::parse_terminated(input)?; - let mut seen_named = HashSet::new(); - let mut positional_index = 0usize; - for raw in parsed { match raw { RawFormatArg::Named { @@ -68,7 +61,6 @@ pub(crate) fn parse_format_args(input: ParseStream) -> Result Result syn::Result { let first = parse_projection_segment(input, true)?; let mut segments = vec![first]; - while input.peek(Token![.]) { input.parse::()?; segments.push(parse_projection_segment(input, false)?); } - let mut span = join_spans(dot_span, segments[0].span()); for segment in segments.iter().skip(1) { span = join_spans(span, segment.span()); } - Ok(FormatArgProjection { segments, span @@ -181,7 +169,6 @@ fn parse_projection_segment( span: literal.span() }); } - if input.peek(Ident) { let ident: Ident = input.parse()?; if let Some((turbofish, paren_token, args)) = parse_method_call_suffix(input)? { @@ -195,10 +182,8 @@ fn parse_projection_segment( } )); } - return Ok(FormatArgProjectionSegment::Field(ident)); } - let span = input.span(); if first { Err(syn::Error::new( @@ -216,17 +201,14 @@ fn parse_projection_segment( /// Parses method call suffix with optional turbofish. fn parse_method_call_suffix(input: ParseStream) -> syn::Result { let ahead = input.fork(); - let has_turbofish = ahead.peek(Token![::]); if has_turbofish { let _: Token![::] = ahead.parse()?; let _: AngleBracketedGenericArguments = ahead.parse()?; } - if !ahead.peek(Paren) { return Ok(None); } - let turbofish = if has_turbofish { let colon2_token = input.parse::()?; let generics = input.parse::()?; @@ -237,11 +219,9 @@ fn parse_method_call_suffix(input: ParseStream) -> syn::Result } else { None }; - let content; let paren_token = syn::parenthesized!(content in input); let args = Punctuated::::parse_terminated(&content)?; - Ok(Some((turbofish, paren_token, args))) } diff --git a/masterror-derive/src/input/types.rs b/masterror-derive/src/input/types.rs index 8fb7ea4..f9beda5 100644 --- a/masterror-derive/src/input/types.rs +++ b/masterror-derive/src/input/types.rs @@ -15,6 +15,7 @@ use syn::{ token::Paren }; +use super::{parse_attr::parse_provide_attribute, utils::is_backtrace_storage}; use crate::template_support::DisplayTemplate; /// Top-level parsed error type definition. @@ -226,7 +227,7 @@ impl<'a> BacktraceField<'a> { /// Checks if field type can store backtrace. pub fn stores_backtrace(&self) -> bool { - super::utils::is_backtrace_storage(&self.field.ty) + is_backtrace_storage(&self.field.ty) } /// Returns the field index. @@ -271,9 +272,7 @@ impl Field { Some(name) => syn::Member::Named(name.clone()), None => syn::Member::Unnamed(syn::Index::from(index)) }; - let attrs = FieldAttrs::from_attrs(&field.attrs, ident.as_ref(), &field.ty, errors); - Self { ident, member, @@ -312,9 +311,7 @@ impl FieldAttrs { errors: &mut Vec ) -> Self { use super::utils::{is_backtrace_type, is_option_type, option_inner_type, path_is}; - let mut result = FieldAttrs::default(); - for attr in attrs { if path_is(attr, "from") { if let Err(err) = attr.meta.require_path_only() { @@ -347,23 +344,20 @@ impl FieldAttrs { } result.backtrace = Some(attr.clone()); } else if path_is(attr, "provide") { - match super::parse_attr::parse_provide_attribute(attr) { + match parse_provide_attribute(attr) { Ok(spec) => result.provides.push(spec), Err(err) => errors.push(err) } } } - if result.source.is_none() && let Some(attr) = &result.from { result.source = Some(attr.clone()); } - if result.source.is_none() && ident.is_some_and(|ident| ident == "source") { result.inferred_source = true; } - if result.backtrace.is_none() { if is_option_type(ty) { if option_inner_type(ty).is_some_and(is_backtrace_type) { @@ -373,7 +367,6 @@ impl FieldAttrs { result.inferred_backtrace = true; } } - result } @@ -681,7 +674,6 @@ mod tests { let mut errors = Vec::new(); let parsed = Field::from_syn(&field, 0, &mut errors); let bt = BacktraceField::new(&parsed, BacktraceFieldKind::Explicit); - assert_eq!(bt.field().index, 0); assert_eq!(bt.kind(), BacktraceFieldKind::Explicit); assert!(bt.stores_backtrace()); @@ -694,7 +686,6 @@ mod tests { let mut errors = Vec::new(); let parsed = Field::from_syn(&field, 0, &mut errors); let bt = BacktraceField::new(&parsed, BacktraceFieldKind::Inferred); - assert!(bt.stores_backtrace()); } diff --git a/masterror-derive/src/input/utils.rs b/masterror-derive/src/input/utils.rs index 9ef56d2..0695a9d 100644 --- a/masterror-derive/src/input/utils.rs +++ b/masterror-derive/src/input/utils.rs @@ -25,7 +25,6 @@ pub(crate) fn validate_from_usage( let mut from_fields = fields.iter().filter(|field| field.attrs.from.is_some()); let first = from_fields.next(); let second = from_fields.next(); - if let Some(field) = first { if second.is_some() { if let Some(attr) = &field.attrs.from { @@ -36,17 +35,14 @@ pub(crate) fn validate_from_usage( } return; } - let mut has_unexpected_companions = false; for companion in fields.iter() { if companion.index == field.index { continue; } - if companion.attrs.has_backtrace() { continue; } - if companion.attrs.has_source() { if companion.attrs.from.is_none() && !is_option_type(&companion.ty) { if let Some(attr) = companion.attrs.source_attribute() { @@ -63,17 +59,14 @@ pub(crate) fn validate_from_usage( } continue; } - has_unexpected_companions = true; } - if has_unexpected_companions && let Some(attr) = &field.attrs.from { errors.push(Error::new_spanned( attr, "deriving From requires no fields other than source and backtrace" )); } - if matches!(display, DisplaySpec::Transparent { .. }) && fields.len() != 1 && let Some(attr) = &field.attrs.from @@ -94,15 +87,12 @@ pub(crate) fn validate_backtrace_usage(fields: &Fields, errors: &mut Vec) .iter() .filter(|field| field.attrs.has_backtrace()) .collect(); - for field in &backtrace_fields { validate_backtrace_field_type(field, errors); } - if backtrace_fields.len() <= 1 { return; } - for field in backtrace_fields.iter().skip(1) { if let Some(attr) = field.attrs.backtrace_attribute() { errors.push(Error::new_spanned( @@ -123,15 +113,12 @@ fn validate_backtrace_field_type(field: &Field, errors: &mut Vec) { let Some(attr) = field.attrs.backtrace_attribute() else { return; }; - if is_backtrace_storage(&field.ty) { return; } - if field.attrs.has_source() { return; } - errors.push(Error::new_spanned( attr, "fields with #[backtrace] must be std::backtrace::Backtrace or Option" @@ -148,7 +135,6 @@ pub(crate) fn validate_transparent( if fields.len() == 1 { return; } - if let DisplaySpec::Transparent { attribute } = display @@ -280,10 +266,11 @@ mod tests { use syn::parse_quote; use super::*; + use crate::template_support::parse_display_template; fn make_template() -> DisplaySpec { let lit: syn::LitStr = parse_quote! { "error message" }; - let template = crate::template_support::parse_display_template(lit).unwrap(); + let template = parse_display_template(lit).unwrap(); DisplaySpec::Template(template) } @@ -295,7 +282,6 @@ mod tests { let mut errors = Vec::new(); let parsed = Fields::from_syn(&syn::Fields::Named(fields), &mut errors); errors.clear(); - let display = make_template(); validate_from_usage(&parsed, &display, &mut errors); assert!(!errors.is_empty()); @@ -309,7 +295,6 @@ mod tests { let mut errors = Vec::new(); let parsed = Fields::from_syn(&syn::Fields::Named(fields), &mut errors); errors.clear(); - let display = make_template(); validate_from_usage(&parsed, &display, &mut errors); assert!(!errors.is_empty()); @@ -323,7 +308,6 @@ mod tests { let mut errors = Vec::new(); let parsed = Fields::from_syn(&syn::Fields::Named(fields), &mut errors); errors.clear(); - let display = make_template(); validate_from_usage(&parsed, &display, &mut errors); assert!(!errors.is_empty()); @@ -337,7 +321,6 @@ mod tests { let mut errors = Vec::new(); let parsed = Fields::from_syn(&syn::Fields::Named(fields), &mut errors); errors.clear(); - let display = make_template(); validate_from_usage(&parsed, &display, &mut errors); assert!(errors.is_empty()); @@ -351,7 +334,6 @@ mod tests { let mut errors = Vec::new(); let parsed = Fields::from_syn(&syn::Fields::Named(fields), &mut errors); errors.clear(); - let display = make_template(); validate_from_usage(&parsed, &display, &mut errors); assert!(errors.is_empty()); @@ -365,7 +347,6 @@ mod tests { let mut errors = Vec::new(); let parsed = Fields::from_syn(&syn::Fields::Named(fields), &mut errors); errors.clear(); - let attr: syn::Attribute = parse_quote! { #[error(transparent)] }; let display = DisplaySpec::Transparent { attribute: Box::new(attr) @@ -382,7 +363,6 @@ mod tests { let mut errors = Vec::new(); let parsed = Fields::from_syn(&syn::Fields::Named(fields), &mut errors); errors.clear(); - validate_backtrace_usage(&parsed, &mut errors); assert!(errors.is_empty()); } @@ -395,7 +375,6 @@ mod tests { let mut errors = Vec::new(); let parsed = Fields::from_syn(&syn::Fields::Named(fields), &mut errors); errors.clear(); - validate_backtrace_usage(&parsed, &mut errors); assert!(!errors.is_empty()); } @@ -408,7 +387,6 @@ mod tests { let mut errors = Vec::new(); let parsed = Fields::from_syn(&syn::Fields::Named(fields), &mut errors); errors.clear(); - validate_backtrace_usage(&parsed, &mut errors); assert!(!errors.is_empty()); } @@ -421,7 +399,6 @@ mod tests { let mut errors = Vec::new(); let parsed = Fields::from_syn(&syn::Fields::Named(fields), &mut errors); errors.clear(); - validate_backtrace_usage(&parsed, &mut errors); assert!(errors.is_empty()); } @@ -431,7 +408,6 @@ mod tests { let fields: syn::FieldsUnnamed = parse_quote! { (io::Error) }; let mut errors = Vec::new(); let parsed = Fields::from_syn(&syn::Fields::Unnamed(fields), &mut errors); - let attr: syn::Attribute = parse_quote! { #[error(transparent)] }; let display = DisplaySpec::Transparent { attribute: Box::new(attr) @@ -446,7 +422,6 @@ mod tests { let mut errors = Vec::new(); let parsed = Fields::from_syn(&syn::Fields::Unnamed(fields), &mut errors); errors.clear(); - let attr: syn::Attribute = parse_quote! { #[error(transparent)] }; let display = DisplaySpec::Transparent { attribute: Box::new(attr) @@ -461,7 +436,6 @@ mod tests { let mut errors = Vec::new(); let parsed = Fields::from_syn(&syn::Fields::Unnamed(fields), &mut errors); errors.clear(); - let attr: syn::Attribute = parse_quote! { #[error(transparent)] }; let display = DisplaySpec::Transparent { attribute: Box::new(attr) @@ -642,7 +616,6 @@ mod tests { let mut errors = Vec::new(); let parsed = Fields::from_syn(&syn::Fields::Named(fields), &mut errors); errors.clear(); - let display = make_template(); validate_from_usage(&parsed, &display, &mut errors); assert!(!errors.is_empty()); @@ -656,7 +629,6 @@ mod tests { let mut errors = Vec::new(); let parsed = Fields::from_syn(&syn::Fields::Named(fields), &mut errors); errors.clear(); - validate_backtrace_usage(&parsed, &mut errors); assert!(errors.is_empty()); } @@ -711,7 +683,6 @@ mod tests { let mut errors = Vec::new(); let parsed = Fields::from_syn(&syn::Fields::Named(fields), &mut errors); errors.clear(); - let attr: syn::Attribute = parse_quote! { #[error(transparent)] }; let display = DisplaySpec::Transparent { attribute: Box::new(attr) diff --git a/masterror-derive/src/lib.rs b/masterror-derive/src/lib.rs index ce2a293..48766bf 100644 --- a/masterror-derive/src/lib.rs +++ b/masterror-derive/src/lib.rs @@ -47,7 +47,6 @@ fn expand(input: DeriveInput) -> Result { let error_impl = error_trait::expand(&parsed)?; let from_impls = from_impl::expand(&parsed)?; let app_error_impls = app_error_impl::expand(&parsed)?; - Ok(quote! { #display_impl #error_impl @@ -62,7 +61,6 @@ fn expand_masterror(input: DeriveInput) -> Result Result Result Result { ensure_all_variants_have_masterror(variants)?; - let conversion = enum_conversion_impl(input, variants); let mappings = enum_mapping_impl(input, variants); - use quote::quote; Ok(quote! { #conversion diff --git a/masterror-derive/src/masterror_impl/attachment.rs b/masterror-derive/src/masterror_impl/attachment.rs index c296144..de6c09c 100644 --- a/masterror-derive/src/masterror_impl/attachment.rs +++ b/masterror-derive/src/masterror_impl/attachment.rs @@ -149,7 +149,6 @@ pub fn backtrace_attachment_tokens( else { return TokenStream::new(); }; - if is_option_type(&backtrace_field.field().ty) { quote! { if let Some(trace) = #binding { @@ -273,7 +272,6 @@ pub fn redact_tokens(spec: &RedactSpec) -> TokenStream { } else { TokenStream::new() }; - let field_updates = spec.fields.iter().map(|field_spec: &FieldRedactionSpec| { let name = &field_spec.name; let policy = field_redaction_tokens(field_spec.policy); @@ -281,7 +279,6 @@ pub fn redact_tokens(spec: &RedactSpec) -> TokenStream { __masterror_error = __masterror_error.redact_field(#name, #policy); ) }); - quote! { #message #( #field_updates )* @@ -335,7 +332,6 @@ mod tests { let entries: Vec = vec![parse_quote!(trace_id()), parse_quote!(span_id())]; let result = telemetry_initialization(&entries); let result_str = result.to_string(); - assert!(result_str.contains("__masterror_metadata_inner")); assert!(result_str.contains("trace_id")); assert!(result_str.contains("span_id")); @@ -346,7 +342,6 @@ mod tests { fn test_metadata_attach_tokens() { let result = metadata_attach_tokens(); let result_str = result.to_string(); - assert!(result_str.contains("if let Some")); assert!(result_str.contains("metadata")); assert!(result_str.contains("with_metadata")); @@ -356,13 +351,10 @@ mod tests { fn test_field_redaction_tokens_all_variants() { let none = field_redaction_tokens(FieldRedactionKind::None); assert!(none.to_string().contains("None")); - let redact = field_redaction_tokens(FieldRedactionKind::Redact); assert!(redact.to_string().contains("Redact")); - let hash = field_redaction_tokens(FieldRedactionKind::Hash); assert!(hash.to_string().contains("Hash")); - let last4 = field_redaction_tokens(FieldRedactionKind::Last4); assert!(last4.to_string().contains("Last4")); } @@ -370,15 +362,12 @@ mod tests { #[test] fn test_redact_tokens_message_only() { use crate::input::RedactSpec; - let spec = RedactSpec { message: true, fields: vec![] }; - let result = redact_tokens(&spec); let result_str = result.to_string(); - assert!(result_str.contains("redactable")); } @@ -387,7 +376,6 @@ mod tests { use syn::LitStr; use crate::input::{FieldRedactionSpec, RedactSpec}; - let spec = RedactSpec { message: false, fields: vec![FieldRedactionSpec { @@ -395,10 +383,8 @@ mod tests { policy: FieldRedactionKind::Hash }] }; - let result = redact_tokens(&spec); let result_str = result.to_string(); - assert!(result_str.contains("password")); assert!(result_str.contains("Hash")); assert!(result_str.contains("redact_field")); @@ -426,7 +412,6 @@ mod tests { use syn::parse_quote; use crate::input::{Field, FieldAttrs}; - let mut attrs = FieldAttrs::default(); attrs.source = Some(parse_quote!(#[source])); let field = Field { @@ -442,7 +427,6 @@ mod tests { field: &field, binding }]; - let result = source_attachment_tokens(&bound); let result_str = result.to_string(); assert!(result_str.contains("with_source_arc")); @@ -455,7 +439,6 @@ mod tests { use syn::parse_quote; use crate::input::{Field, FieldAttrs}; - let mut attrs = FieldAttrs::default(); attrs.source = Some(parse_quote!(#[source])); let field = Field { @@ -471,7 +454,6 @@ mod tests { field: &field, binding }]; - let result = source_attachment_tokens(&bound); let result_str = result.to_string(); assert!(result_str.contains("with_source_arc")); @@ -482,13 +464,10 @@ mod tests { fn test_field_redaction_tokens_all_kinds() { let result_none = field_redaction_tokens(FieldRedactionKind::None); assert!(result_none.to_string().contains("None")); - let result_redact = field_redaction_tokens(FieldRedactionKind::Redact); assert!(result_redact.to_string().contains("Redact")); - let result_hash = field_redaction_tokens(FieldRedactionKind::Hash); assert!(result_hash.to_string().contains("Hash")); - let result_last4 = field_redaction_tokens(FieldRedactionKind::Last4); assert!(result_last4.to_string().contains("Last4")); } diff --git a/masterror-derive/src/masterror_impl/binding.rs b/masterror-derive/src/masterror_impl/binding.rs index 2a3dcf3..74dc2e8 100644 --- a/masterror-derive/src/masterror_impl/binding.rs +++ b/masterror-derive/src/masterror_impl/binding.rs @@ -139,7 +139,6 @@ pub fn bind_variant_fields<'a>( variant: &'a VariantData ) -> (TokenStream, Vec>) { let variant_ident = &variant.ident; - match &variant.fields { Fields::Unit => (quote!(#enum_ident::#variant_ident), Vec::new()), Fields::Named(list) => { @@ -200,7 +199,6 @@ pub fn field_usage_tokens(bound_fields: &[BoundField<'_>]) -> TokenStream { if bound_fields.is_empty() { return TokenStream::new(); } - let names = bound_fields.iter().map(|field| &field.binding); quote! { let _ = (#(&#names),*); @@ -251,7 +249,6 @@ mod tests { Some(name) => syn::Member::Named(name.clone()), None => syn::Member::Unnamed(syn::Index::from(index)) }; - Field { ident, member, @@ -291,7 +288,6 @@ mod tests { field: &field, binding }]; - let result = field_usage_tokens(&bound); let result_str = result.to_string(); assert!(result_str.contains("field1")); @@ -301,7 +297,6 @@ mod tests { fn test_bind_struct_fields_unit() { let ident = format_ident!("MyError"); let fields = Fields::Unit; - let (pattern, bound) = bind_struct_fields(&ident, &fields); assert_eq!(pattern.to_string(), "let _ = value ;"); assert!(bound.is_empty()); @@ -312,10 +307,8 @@ mod tests { let ident = format_ident!("MyError"); let field = create_test_field(Some(format_ident!("message")), 0); let fields = Fields::Named(vec![field]); - let (pattern, bound) = bind_struct_fields(&ident, &fields); let pattern_str = pattern.to_string(); - assert!(pattern_str.contains("MyError")); assert!(pattern_str.contains("message")); assert_eq!(bound.len(), 1); @@ -327,10 +320,8 @@ mod tests { let ident = format_ident!("MyError"); let field = create_test_field(None, 0); let fields = Fields::Unnamed(vec![field]); - let (pattern, bound) = bind_struct_fields(&ident, &fields); let pattern_str = pattern.to_string(); - assert!(pattern_str.contains("MyError")); assert!(pattern_str.contains("__field0")); assert_eq!(bound.len(), 1); @@ -352,7 +343,6 @@ mod tests { masterror: None, span: Span::call_site() }; - let (pattern, bound) = bind_variant_fields(&enum_ident, &variant); assert_eq!(pattern.to_string(), "MyError :: NotFound"); assert!(bound.is_empty()); @@ -374,10 +364,8 @@ mod tests { masterror: None, span: Span::call_site() }; - let (pattern, bound) = bind_variant_fields(&enum_ident, &variant); let pattern_str = pattern.to_string(); - assert!(pattern_str.contains("MyError :: Auth")); assert!(pattern_str.contains("code")); assert_eq!(bound.len(), 1); @@ -400,10 +388,8 @@ mod tests { masterror: None, span: Span::call_site() }; - let (pattern, bound) = bind_variant_fields(&enum_ident, &variant); let pattern_str = pattern.to_string(); - assert!(pattern_str.contains("MyError :: Io")); assert!(pattern_str.contains("__field0")); assert_eq!(bound.len(), 1); @@ -426,7 +412,6 @@ mod tests { binding: binding2 }, ]; - let result = field_usage_tokens(&bound); let result_str = result.to_string(); assert!(result_str.contains("field1")); diff --git a/masterror-derive/src/masterror_impl/conversion.rs b/masterror-derive/src/masterror_impl/conversion.rs index 8e15861..c5e9fdb 100644 --- a/masterror-derive/src/masterror_impl/conversion.rs +++ b/masterror-derive/src/masterror_impl/conversion.rs @@ -64,7 +64,6 @@ pub fn struct_conversion_impl( let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); let code = &spec.code; let category = &spec.category; - let message_init = message_initialization(spec.expose_message, quote!(&value)); let (destructure, bound_fields) = bind_struct_fields(ident, &data.fields); let field_usage = field_usage_tokens(&bound_fields); @@ -73,7 +72,6 @@ pub fn struct_conversion_impl( let redact_tokens = redact_tokens(&spec.redact); let source_tokens = source_attachment_tokens(&bound_fields); let backtrace_tokens = backtrace_attachment_tokens(&data.fields, &bound_fields); - quote! { impl #impl_generics core::convert::From<#ident #ty_generics> for masterror::Error #where_clause { fn from(value: #ident #ty_generics) -> Self { @@ -126,10 +124,8 @@ pub fn struct_conversion_impl( pub fn enum_conversion_impl(input: &ErrorInput, variants: &[VariantData]) -> TokenStream { let ident = &input.ident; let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); - let mut arms = Vec::new(); let mut message_arms = Vec::new(); - for variant in variants { let spec = variant.masterror.as_ref().expect("presence checked"); let code = &spec.code; @@ -142,7 +138,6 @@ pub fn enum_conversion_impl(input: &ErrorInput, variants: &[VariantData]) -> Tok let source_tokens = source_attachment_tokens(&bound_fields); let backtrace_tokens = backtrace_attachment_tokens(&variant.fields, &bound_fields); message_arms.push(enum_message_arm(ident, variant, spec.expose_message)); - arms.push(quote! { #pattern => { #field_usage @@ -160,13 +155,11 @@ pub fn enum_conversion_impl(input: &ErrorInput, variants: &[VariantData]) -> Tok } }); } - let message_match = quote! { let __masterror_message: Option = match &value { #(#message_arms)* }; }; - quote! { impl #impl_generics core::convert::From<#ident #ty_generics> for masterror::Error #where_clause { fn from(value: #ident #ty_generics) -> Self { @@ -273,7 +266,6 @@ fn enum_message_arm( expose_message: bool ) -> TokenStream { use quote::format_ident; - if expose_message { let binding = format_ident!("__masterror_variant_ref"); let pattern = enum_message_pattern(enum_ident, variant, Some(&binding)); @@ -343,10 +335,10 @@ mod tests { assert_eq!(result.to_string(), expected.to_string()); } + /// Tests with empty list since creating mock VariantData structures is + /// complex. #[test] fn test_ensure_all_variants_have_masterror_valid() { - // This would require creating mock VariantData structures - // For now, testing that empty list succeeds let variants = vec![]; assert!(ensure_all_variants_have_masterror(&variants).is_ok()); } @@ -357,7 +349,6 @@ mod tests { use quote::format_ident; use crate::input::{DisplaySpec, Fields}; - let variant = VariantData { ident: format_ident!("NotFound"), fields: Fields::Unit, @@ -369,7 +360,6 @@ mod tests { masterror: None, span: Span::call_site() }; - let enum_ident = format_ident!("MyError"); let result = enum_message_pattern(&enum_ident, &variant, None); let result_str = result.to_string(); @@ -383,7 +373,6 @@ mod tests { use syn::parse_quote; use crate::input::{DisplaySpec, Field, FieldAttrs, Fields}; - let field = Field { ident: Some(format_ident!("message")), member: syn::Member::Named(format_ident!("message")), @@ -392,7 +381,6 @@ mod tests { attrs: FieldAttrs::default(), span: Span::call_site() }; - let variant = VariantData { ident: format_ident!("Custom"), fields: Fields::Named(vec![field]), @@ -404,7 +392,6 @@ mod tests { masterror: None, span: Span::call_site() }; - let enum_ident = format_ident!("MyError"); let result = enum_message_pattern(&enum_ident, &variant, None); let result_str = result.to_string(); diff --git a/masterror-derive/src/masterror_impl/mapping.rs b/masterror-derive/src/masterror_impl/mapping.rs index f7737ac..b144c49 100644 --- a/masterror-derive/src/masterror_impl/mapping.rs +++ b/masterror-derive/src/masterror_impl/mapping.rs @@ -67,16 +67,13 @@ pub fn struct_mapping_impl(input: &ErrorInput, spec: &MasterrorSpec) -> TokenStr category, MappingKind::Problem ); - quote! { impl #impl_generics #ident #ty_generics #where_clause { /// HTTP mapping for this error type. pub const HTTP_MAPPING: masterror::mapping::HttpMapping = masterror::mapping::HttpMapping::new((#code), (#category)); - /// gRPC mapping for this error type. pub const GRPC_MAPPING: Option = #grpc_mapping; - /// Problem JSON mapping for this error type. pub const PROBLEM_MAPPING: Option = #problem_mapping; } @@ -124,7 +121,6 @@ pub fn struct_mapping_impl(input: &ErrorInput, spec: &MasterrorSpec) -> TokenStr pub fn enum_mapping_impl(input: &ErrorInput, variants: &[VariantData]) -> TokenStream { let ident = &input.ident; let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); - let http_entries: Vec<_> = variants .iter() .map(|variant| { @@ -134,7 +130,6 @@ pub fn enum_mapping_impl(input: &ErrorInput, variants: &[VariantData]) -> TokenS quote!(masterror::mapping::HttpMapping::new((#code), (#category))) }) .collect(); - let grpc_entries: Vec<_> = variants .iter() .filter_map(|variant| { @@ -146,7 +141,6 @@ pub fn enum_mapping_impl(input: &ErrorInput, variants: &[VariantData]) -> TokenS ) }) .collect(); - let problem_entries: Vec<_> = variants .iter() .filter_map(|variant| { @@ -158,29 +152,23 @@ pub fn enum_mapping_impl(input: &ErrorInput, variants: &[VariantData]) -> TokenS }) }) .collect(); - let http_len = Index::from(http_entries.len()); - let grpc_slice = if grpc_entries.is_empty() { quote!(&[] as &[masterror::mapping::GrpcMapping]) } else { quote!(&[#(#grpc_entries),*]) }; - let problem_slice = if problem_entries.is_empty() { quote!(&[] as &[masterror::mapping::ProblemMapping]) } else { quote!(&[#(#problem_entries),*]) }; - quote! { impl #impl_generics #ident #ty_generics #where_clause { /// HTTP mappings for enum variants. pub const HTTP_MAPPINGS: [masterror::mapping::HttpMapping; #http_len] = [#(#http_entries),*]; - /// gRPC mappings for enum variants. pub const GRPC_MAPPINGS: &'static [masterror::mapping::GrpcMapping] = #grpc_slice; - /// Problem JSON mappings for enum variants. pub const PROBLEM_MAPPINGS: &'static [masterror::mapping::ProblemMapping] = #problem_slice; } @@ -251,10 +239,8 @@ mod tests { let expr: Expr = parse_quote!(tonic::Code::Internal); let code: Expr = parse_quote!("E001"); let category: ExprPath = parse_quote!(ErrorCategory::Internal); - let result = mapping_option_tokens(Some(&expr), &code, &category, MappingKind::Grpc); let result_str = result.to_string(); - assert!(result_str.contains("GrpcMapping")); assert!(result_str.contains("Some")); } @@ -264,10 +250,8 @@ mod tests { let expr: Expr = parse_quote!("about:blank"); let code: Expr = parse_quote!("E001"); let category: ExprPath = parse_quote!(ErrorCategory::Internal); - let result = mapping_option_tokens(Some(&expr), &code, &category, MappingKind::Problem); let result_str = result.to_string(); - assert!(result_str.contains("ProblemMapping")); assert!(result_str.contains("Some")); } @@ -276,7 +260,6 @@ mod tests { fn test_mapping_option_tokens_none() { let code: Expr = parse_quote!("E001"); let category: ExprPath = parse_quote!(ErrorCategory::Internal); - let result = mapping_option_tokens(None, &code, &category, MappingKind::Grpc); assert_eq!(result.to_string(), "None"); } @@ -285,7 +268,6 @@ mod tests { fn test_mapping_option_tokens_problem_none() { let code: Expr = parse_quote!("E002"); let category: ExprPath = parse_quote!(ErrorCategory::NotFound); - let result = mapping_option_tokens(None, &code, &category, MappingKind::Problem); assert_eq!(result.to_string(), "None"); } diff --git a/masterror-derive/src/span.rs b/masterror-derive/src/span.rs index 82920dc..71be276 100644 --- a/masterror-derive/src/span.rs +++ b/masterror-derive/src/span.rs @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: MIT -use core::ops::Range; +use core::{ops::Range, str::from_utf8}; use proc_macro2::Span; use syn::LitStr; @@ -16,15 +16,12 @@ pub fn literal_subspan(lit: &LitStr, range: Range) -> Option { if range.start > range.end { return None; } - let value = lit.value(); if range.end > value.len() { return None; } - let token = lit.token(); let repr = token.to_string(); - if repr.starts_with('r') { raw_range(&repr, range).and_then(|sub| token.subspan(sub)) } else { @@ -38,24 +35,19 @@ fn raw_range(repr: &str, range: Range) -> Option> { if bytes.get(idx)? != &b'r' { return None; } - idx += 1; while matches!(bytes.get(idx), Some(b'#')) { idx += 1; } - if bytes.get(idx)? != &b'"' { return None; } - let hash_count = idx - 1; let start_content = idx + 1; let end_content = repr.len().checked_sub(hash_count + 1)?; - if start_content > end_content || range.end > end_content - start_content { return None; } - let start = start_content + range.start; let end = start_content + range.end; Some(start..end) @@ -66,54 +58,43 @@ fn escaped_range(repr: &str, value: &str, range: Range) -> Option value.len() { return None; } - Some(mapping[range.start]..mapping[range.end]) } @@ -121,7 +102,6 @@ fn escape_sequence_len(bytes: &[u8]) -> Option { if bytes.len() < 2 || bytes[0] != b'\\' { return None; } - match bytes[1] { b'\\' | b'"' | b'\'' | b'n' | b'r' | b't' | b'0' => Some(2), b'x' => { @@ -136,16 +116,13 @@ fn escape_sequence_len(bytes: &[u8]) -> Option { if bytes.get(idx)? != &b'{' { return None; } - idx += 1; while idx < bytes.len() && bytes[idx] != b'}' { idx += 1; } - if idx >= bytes.len() { return None; } - Some(idx + 1) } _ => None diff --git a/masterror-derive/src/template_support.rs b/masterror-derive/src/template_support.rs index 7043d6d..bd98d2f 100644 --- a/masterror-derive/src/template_support.rs +++ b/masterror-derive/src/template_support.rs @@ -38,7 +38,6 @@ pub enum TemplateIdentifierSpec { pub fn parse_display_template(lit: LitStr) -> Result { let value = lit.value(); let parsed = ErrorTemplate::parse(&value).map_err(|err| template_error(&lit, err))?; - let mut segments = Vec::new(); for segment in parsed.segments() { match segment { @@ -56,7 +55,6 @@ pub fn parse_display_template(lit: LitStr) -> Result { } TemplateIdentifier::Implicit(index) => TemplateIdentifierSpec::Implicit(*index) }; - segments.push(TemplateSegmentSpec::Placeholder(TemplatePlaceholderSpec { span, identifier, @@ -65,7 +63,6 @@ pub fn parse_display_template(lit: LitStr) -> Result { } } } - Ok(DisplayTemplate { segments }) @@ -100,7 +97,6 @@ fn template_error(lit: &LitStr, error: TemplateError) -> Error { span } => literal_subspan(lit, span.clone()) }; - Error::new(span.unwrap_or_else(|| lit.span()), message) } @@ -231,7 +227,6 @@ mod tests { let result = parse_display_template(lit); assert!(result.is_ok()); let template = result.ok().unwrap(); - // "hello {{world}}" parses as: "hello ", "{", "world", "}" assert_eq!(template.segments.len(), 4); assert!(matches!( &template.segments[0], @@ -271,9 +266,9 @@ mod tests { assert!(msg.contains("not closed")); } + /// Nested placeholder `{foo{bar}` has `{` inside - should fail. #[test] fn parse_display_template_nested_placeholder() { - // A truly nested placeholder: "{foo{bar}" - has "{" inside the placeholder let lit: LitStr = parse_quote!("{foo{bar}"); let result = parse_display_template(lit); assert!(result.is_err()); @@ -469,21 +464,19 @@ mod tests { assert!(msg.contains("unsupported formatter") || msg.contains("Invalid")); } + /// Verifies that `placeholder_span` doesn't panic for valid ranges. #[test] fn placeholder_span_returns_subspan_for_valid_range() { let lit: LitStr = parse_quote!("hello {name}"); let span = placeholder_span(&lit, 6..12); - // The span should be valid (not equal to the lit span in a meaningful way) - // We can't directly compare spans, but we can verify the function doesn't panic let _ = span; } + /// Invalid range returns `lit.span()` without panicking. #[test] fn placeholder_span_returns_lit_span_for_invalid_range() { let lit: LitStr = parse_quote!("hello"); - // Invalid range beyond the string length let span = placeholder_span(&lit, 100..200); - // Should return lit.span() without panicking let _ = span; } @@ -592,7 +585,6 @@ mod tests { ("{val:#o}", "Octal"), ("{val:#p}", "Pointer"), ]; - for (template_str, formatter_name) in cases { let lit: LitStr = parse_quote!(#template_str); let result = parse_display_template(lit); @@ -606,7 +598,6 @@ mod tests { let result = parse_display_template(lit); assert!(result.is_ok()); let template = result.ok().unwrap(); - // Should have: "{", "prefix", "}", " ", placeholder, " ", "{", "suffix", "}" assert!(template.segments.len() >= 3); } diff --git a/masterror-template/src/template.rs b/masterror-template/src/template.rs index 4b98f03..b37bb9f 100644 --- a/masterror-template/src/template.rs +++ b/masterror-template/src/template.rs @@ -101,7 +101,6 @@ where } } } - Ok(()) } } @@ -677,12 +676,10 @@ mod tests { fn parses_basic_template() { let template = ErrorTemplate::parse("{code}: {message}").expect("parse"); let segments = template.segments(); - assert_eq!(segments.len(), 3); assert!(matches!(segments[0], TemplateSegment::Placeholder(_))); assert!(matches!(segments[1], TemplateSegment::Literal(": "))); assert!(matches!(segments[2], TemplateSegment::Placeholder(_))); - let placeholders: Vec<_> = template.placeholders().collect(); assert_eq!(placeholders.len(), 2); assert_eq!(placeholders[0].identifier(), &named("code")); @@ -693,7 +690,6 @@ mod tests { fn parses_implicit_identifiers() { let template = ErrorTemplate::parse("{}, {:?}, {name}, {}").expect("parse"); let mut placeholders = template.placeholders(); - let first = placeholders.next().expect("first placeholder"); assert_eq!(first.identifier(), &TemplateIdentifier::Implicit(0)); assert_eq!( @@ -702,7 +698,6 @@ mod tests { spec: None } ); - let second = placeholders.next().expect("second placeholder"); assert_eq!(second.identifier(), &TemplateIdentifier::Implicit(1)); assert_eq!( @@ -711,10 +706,8 @@ mod tests { alternate: false } ); - let third = placeholders.next().expect("third placeholder"); assert_eq!(third.identifier(), &named("name")); - let fourth = placeholders.next().expect("fourth placeholder"); assert_eq!(fourth.identifier(), &TemplateIdentifier::Implicit(2)); assert!(placeholders.next().is_none()); @@ -724,7 +717,6 @@ mod tests { fn parses_debug_formatter() { let template = ErrorTemplate::parse("{0:#?}").expect("parse"); let placeholders: Vec<_> = template.placeholders().collect(); - assert_eq!(placeholders.len(), 1); assert_eq!( placeholders[0].identifier(), @@ -827,7 +819,6 @@ mod tests { } ) ]; - for (template_str, expected) in &cases { let template = ErrorTemplate::parse(template_str).expect("parse"); let placeholder = template.placeholders().next().expect("placeholder present"); @@ -839,17 +830,14 @@ mod tests { fn preserves_hash_fill_display_specs() { let template = ErrorTemplate::parse("{value:#>4}").expect("parse"); let placeholder = template.placeholders().next().expect("placeholder present"); - assert_eq!(placeholder.formatter().display_spec(), Some("#>4")); assert_eq!( placeholder.formatter().format_fragment().as_deref(), Some("#>4") ); - let expected = TemplateFormatter::Display { spec: Some("#>4".into()) }; - assert_eq!(placeholder.formatter(), &expected); } @@ -865,17 +853,13 @@ mod tests { (TemplateFormatterKind::LowerExp, 'e'), (TemplateFormatterKind::UpperExp, 'E') ]; - for (kind, specifier) in table { assert_eq!(TemplateFormatterKind::from_specifier(specifier), Some(kind)); assert_eq!(kind.specifier(), Some(specifier)); - let with_alternate = TemplateFormatter::from_kind(kind, true); let without_alternate = TemplateFormatter::from_kind(kind, false); - assert_eq!(with_alternate.kind(), kind); assert_eq!(without_alternate.kind(), kind); - if kind.supports_alternate() { assert!(with_alternate.is_alternate()); assert!(!without_alternate.is_alternate()); @@ -884,7 +868,6 @@ mod tests { assert!(!without_alternate.is_alternate()); } } - let display = TemplateFormatter::from_kind(TemplateFormatterKind::Display, true); assert_eq!(display.kind(), TemplateFormatterKind::Display); assert!(!display.is_alternate()); @@ -896,7 +879,6 @@ mod tests { fn handles_brace_escaping() { let template = ErrorTemplate::parse("{{}} -> {value}").expect("parse"); let mut iter = template.segments().iter(); - assert!(matches!(iter.next(), Some(TemplateSegment::Literal("{")))); assert!(matches!(iter.next(), Some(TemplateSegment::Literal("}")))); assert!(matches!( @@ -949,7 +931,6 @@ mod tests { let template = ErrorTemplate::parse("{code}: {message}").expect("parse"); let code = 418; let message = "I'm a teapot"; - let rendered = format!( "{}", template.display_with(|placeholder, f| match placeholder.identifier() { @@ -958,7 +939,6 @@ mod tests { other => panic!("unexpected placeholder: {:?}", other) }) ); - assert_eq!(rendered, "418: I'm a teapot"); } } diff --git a/masterror-template/src/template/parser.rs b/masterror-template/src/template/parser.rs index d1cf3d7..0a2decf 100644 --- a/masterror-template/src/template/parser.rs +++ b/masterror-template/src/template/parser.rs @@ -14,7 +14,6 @@ pub fn parse_template<'a>(source: &'a str) -> Result>, T let mut iter = source.char_indices().peekable(); let mut literal_start = 0usize; let mut implicit_counter = 0usize; - while let Some((index, ch)) = iter.next() { match ch { '{' => { @@ -22,11 +21,9 @@ pub fn parse_template<'a>(source: &'a str) -> Result>, T if index > literal_start { segments.push(TemplateSegment::Literal(&source[literal_start..index])); } - segments.push(TemplateSegment::Literal( &source[index..index + ch.len_utf8()] )); - if let Some((_, escaped)) = iter.next() { literal_start = index + ch.len_utf8() + escaped.len_utf8(); } else { @@ -36,14 +33,11 @@ pub fn parse_template<'a>(source: &'a str) -> Result>, T } continue; } - if index > literal_start { segments.push(TemplateSegment::Literal(&source[literal_start..index])); } - let parsed = parse_placeholder(source, index, &mut implicit_counter)?; segments.push(TemplateSegment::Placeholder(parsed.placeholder)); - literal_start = parsed.after; while matches!(iter.peek(), Some(&(next_index, _)) if next_index < parsed.after) { iter.next(); @@ -54,11 +48,9 @@ pub fn parse_template<'a>(source: &'a str) -> Result>, T if index > literal_start { segments.push(TemplateSegment::Literal(&source[literal_start..index])); } - segments.push(TemplateSegment::Literal( &source[index..index + ch.len_utf8()] )); - if let Some((_, escaped)) = iter.next() { literal_start = index + ch.len_utf8() + escaped.len_utf8(); } else { @@ -68,7 +60,6 @@ pub fn parse_template<'a>(source: &'a str) -> Result>, T } continue; } - return Err(TemplateError::UnmatchedClosingBrace { index }); @@ -76,11 +67,9 @@ pub fn parse_template<'a>(source: &'a str) -> Result>, T _ => {} } } - if literal_start < source.len() { segments.push(TemplateSegment::Literal(&source[literal_start..])); } - Ok(segments) } @@ -113,7 +102,6 @@ fn parse_placeholder<'a>( _ => {} } } - Err(TemplateError::UnterminatedPlaceholder { start }) @@ -127,7 +115,6 @@ fn build_placeholder<'a>( ) -> Result, TemplateError> { let span = start..(end + 1); let body = &source[start + 1..end]; - if body.is_empty() { let identifier = next_implicit_identifier(implicit_counter, &span)?; return Ok(TemplatePlaceholder { @@ -138,17 +125,13 @@ fn build_placeholder<'a>( } }); } - let trimmed = body.trim(); - if trimmed.is_empty() { return Err(TemplateError::EmptyPlaceholder { start }); } - let (identifier, formatter) = split_placeholder(trimmed, span.clone(), implicit_counter)?; - Ok(TemplatePlaceholder { span, identifier, @@ -163,9 +146,7 @@ fn split_placeholder<'a>( ) -> Result<(TemplateIdentifier<'a>, TemplateFormatter), TemplateError> { let mut parts = body.splitn(2, ':'); let identifier_text = parts.next().unwrap_or("").trim(); - let identifier = parse_identifier(identifier_text, span.clone(), implicit_counter)?; - let formatter = match parts.next().map(str::trim) { None => TemplateFormatter::Display { spec: None @@ -177,7 +158,6 @@ fn split_placeholder<'a>( } Some(spec) => parse_formatter(spec, span.clone())? }; - Ok((identifier, formatter)) } @@ -192,28 +172,22 @@ pub(super) fn parse_formatter_spec(spec: &str) -> Option { if trimmed.is_empty() { return None; } - if let Some((last_index, ty)) = trimmed.char_indices().next_back() { if let Some(kind) = TemplateFormatterKind::from_specifier(ty) { let prefix = &trimmed[..last_index]; let alternate = detect_alternate_flag(prefix)?; - return Some(TemplateFormatter::from_kind(kind, alternate)); } - if ty.is_ascii_alphabetic() { return None; } } - if !display_allows_hash(trimmed) { return None; } - if trimmed.chars().any(|ch| matches!(ch, '%' | '{' | '}')) { return None; } - Some(TemplateFormatter::Display { spec: Some(trimmed.to_owned().into_boxed_str()) }) @@ -221,7 +195,6 @@ pub(super) fn parse_formatter_spec(spec: &str) -> Option { fn detect_alternate_flag(prefix: &str) -> Option { let mut rest = prefix; - if rest.len() >= 2 { let mut iter = rest.char_indices(); if let (Some((_, _)), Some((second_index, second))) = (iter.next(), iter.next()) @@ -231,19 +204,16 @@ fn detect_alternate_flag(prefix: &str) -> Option { rest = &rest[skip..]; } } - if let Some(first) = rest.chars().next() && matches!(first, '<' | '>' | '^' | '=') { rest = &rest[first.len_utf8()..]; } - loop { let mut chars = rest.chars(); let Some(ch) = chars.next() else { return Some(false); }; - match ch { '+' | '-' | ' ' => { rest = &rest[ch.len_utf8()..]; @@ -269,11 +239,9 @@ fn display_allows_hash(spec: &str) -> bool { let Some(align) = chars.next() else { return false; }; - if !matches!(align, '<' | '>' | '^' | '=') { return false; } - chars.all(|ch| ch != '#') } Some(_) => false @@ -288,7 +256,6 @@ fn parse_identifier<'a>( if text.is_empty() { return next_implicit_identifier(implicit_counter, &span); } - if text.chars().all(|ch| ch.is_ascii_digit()) { let value = text .parse::() @@ -297,14 +264,12 @@ fn parse_identifier<'a>( })?; return Ok(TemplateIdentifier::Positional(value)); } - if text .chars() .all(|ch| ch == '_' || ch.is_ascii_alphanumeric()) { return Ok(TemplateIdentifier::Named(text)); } - Err(TemplateError::InvalidIdentifier { span }) @@ -320,7 +285,6 @@ fn next_implicit_identifier<'a>( .ok_or_else(|| TemplateError::InvalidIdentifier { span: span.clone() })?; - Ok(TemplateIdentifier::Implicit(index)) } @@ -488,14 +452,12 @@ mod tests { } ) ]; - for (source, expected_formatter) in &cases { let segments = parse_template(source).expect("template parsed"); let placeholder = match segments.first() { Some(TemplateSegment::Placeholder(placeholder)) => placeholder, other => panic!("unexpected segments for {source:?}: {other:?}") }; - assert_eq!( placeholder.formatter(), expected_formatter, @@ -513,7 +475,6 @@ mod tests { "{value:>8q}", "{value:##x}" ]; - for source in &cases { let err = parse_template(source).expect_err("expected formatter error"); assert!( @@ -531,14 +492,12 @@ mod tests { ("{value:#>4}", "#>4"), ("{value:#>+6}", "#>+6") ]; - for (source, expected_spec) in cases { let segments = parse_template(source).expect("template parsed"); let placeholder = match segments.first() { Some(TemplateSegment::Placeholder(placeholder)) => placeholder, other => panic!("unexpected segments for {source:?}: {other:?}") }; - let formatter = placeholder.formatter(); assert!(formatter.display_spec().is_some()); assert_eq!(formatter.display_spec(), Some(expected_spec)); @@ -552,7 +511,6 @@ mod tests { Some(TemplateSegment::Placeholder(placeholder)) => placeholder, other => panic!("unexpected segments for empty braces: {other:?}") }; - assert_eq!(placeholder.identifier(), &TemplateIdentifier::Implicit(0)); assert_eq!( placeholder.formatter(), @@ -572,7 +530,6 @@ mod tests { TemplateSegment::Literal(_) => None }) .collect(); - assert_eq!(placeholders.len(), 4); assert_eq!( placeholders[0].identifier(), diff --git a/src/app_error/context.rs b/src/app_error/context.rs index e938e31..e48864d 100644 --- a/src/app_error/context.rs +++ b/src/app_error/context.rs @@ -212,7 +212,6 @@ impl Context { category, .. } = self; - if let Some(location) = caller_location { fields.push(Field::new( "caller.file", @@ -227,7 +226,6 @@ impl Context { FieldValue::U64(u64::from(location.column())) )); } - let mut error = AppError::new_raw(category, None); error.code = code; if !fields.is_empty() { @@ -404,7 +402,6 @@ mod tests { #[test] fn context_into_error_creates_error_with_kind_and_code() { use std::io::{Error as IoError, ErrorKind}; - let io_err = IoError::from(ErrorKind::Other); let ctx = Context::new(AppErrorKind::Service); let err = ctx.into_error(io_err); @@ -416,7 +413,6 @@ mod tests { #[test] fn context_into_error_applies_metadata_fields() { use std::io::{Error as IoError, ErrorKind}; - let io_err = IoError::from(ErrorKind::Other); let ctx = Context::new(AppErrorKind::Service) .with(field::str("operation", "sync")) @@ -434,7 +430,6 @@ mod tests { #[test] fn context_into_error_applies_field_redactions() { use std::io::{Error as IoError, ErrorKind}; - let io_err = IoError::from(ErrorKind::Other); let ctx = Context::new(AppErrorKind::Service) .with(field::str("secret", "password")) @@ -450,7 +445,6 @@ mod tests { #[test] fn context_into_error_applies_message_redaction() { use std::io::{Error as IoError, ErrorKind}; - let io_err = IoError::from(ErrorKind::Other); let ctx = Context::new(AppErrorKind::Service).redact(true); let err = ctx.into_error(io_err); @@ -462,7 +456,6 @@ mod tests { #[track_caller] fn context_into_error_captures_caller_location() { use std::io::{Error as IoError, ErrorKind}; - let io_err = IoError::from(ErrorKind::Other); let ctx = Context::new(AppErrorKind::Service).track_caller(); let err = ctx.into_error(io_err); @@ -476,7 +469,6 @@ mod tests { #[test] fn context_into_error_with_custom_code() { use std::io::{Error as IoError, ErrorKind}; - let io_err = IoError::from(ErrorKind::Other); let ctx = Context::new(AppErrorKind::Service).code(AppCode::Validation); let err = ctx.into_error(io_err); @@ -539,7 +531,6 @@ mod tests { .redact_field("secret", FieldRedaction::Redact) .redact(true) .track_caller(); - assert_eq!(ctx.category, AppErrorKind::Service); assert_eq!(ctx.code, AppCode::Validation); assert!(ctx.code_overridden); diff --git a/src/app_error/core.rs b/src/app_error/core.rs index baee4e6..2aef940 100644 --- a/src/app_error/core.rs +++ b/src/app_error/core.rs @@ -95,7 +95,6 @@ mod tests { #[test] fn error_with_code_overrides_code() { use crate::AppCode; - let err = Error::new(AppErrorKind::BadRequest, "test").with_code(AppCode::NotFound); assert_eq!(err.code, AppCode::NotFound); } @@ -119,7 +118,6 @@ mod tests { #[test] fn error_with_field_adds_metadata() { use crate::field; - let err = Error::new(AppErrorKind::Validation, "bad field") .with_field(field::str("field_name", "email")); assert_eq!( @@ -131,7 +129,6 @@ mod tests { #[test] fn error_with_fields_adds_multiple_metadata() { use crate::field; - let fields = vec![field::str("key1", "value1"), field::str("key2", "value2")]; let err = Error::new(AppErrorKind::BadRequest, "test").with_fields(fields); assert!(err.metadata().get("key1").is_some()); @@ -148,7 +145,6 @@ mod tests { #[test] fn error_with_source_attaches_source() { use std::io::Error as IoError; - let io_err = IoError::other("disk error"); let err = Error::new(AppErrorKind::Internal, "fail").with_source(io_err); assert!(err.source_ref().is_some()); @@ -158,7 +154,6 @@ mod tests { #[test] fn error_with_context_attaches_source() { use std::io::Error as IoError; - let io_err = IoError::other("network error"); let err = Error::new(AppErrorKind::Network, "fail").with_context(io_err); assert!(err.source_ref().is_some()); @@ -167,7 +162,6 @@ mod tests { #[test] fn error_metadata_returns_metadata() { use crate::field; - let err = Error::new(AppErrorKind::Internal, "test").with_field(field::str("test", "value")); let metadata = err.metadata(); @@ -197,7 +191,6 @@ mod tests { #[test] fn error_chain_returns_iterator() { use std::io::Error as IoError; - let io_err = IoError::other("root cause"); let err = Error::new(AppErrorKind::Internal, "wrapper").with_context(io_err); let chain: Vec<_> = err.chain().collect(); @@ -208,7 +201,6 @@ mod tests { #[test] fn error_root_cause_returns_lowest_error() { use std::io::Error as IoError; - let io_err = IoError::other("disk offline"); let err = Error::new(AppErrorKind::Internal, "db down").with_context(io_err); let root = err.root_cause(); @@ -219,7 +211,6 @@ mod tests { #[test] fn error_is_checks_source_type() { use std::io::Error as IoError; - let io_err = IoError::other("test"); let err = Error::new(AppErrorKind::Network, "fail").with_context(io_err); assert!(err.is::()); @@ -229,7 +220,6 @@ mod tests { #[test] fn error_downcast_ref_returns_concrete_type() { use std::io::Error as IoError; - let io_err = IoError::other("disk error"); let err = Error::new(AppErrorKind::Internal, "fail").with_context(io_err); assert!(err.downcast_ref::().is_some()); @@ -239,7 +229,6 @@ mod tests { #[test] fn error_downcast_mut_returns_none() { use std::io::Error as IoError; - let io_err = IoError::other("test"); let mut err = Error::new(AppErrorKind::Internal, "fail").with_context(io_err); assert!(err.downcast_mut::().is_none()); @@ -249,7 +238,6 @@ mod tests { #[test] fn error_downcast_returns_err() { use std::io::Error as IoError; - let io_err = IoError::other("test"); let err = Error::new(AppErrorKind::Internal, "fail").with_context(io_err); assert!(err.downcast::().is_err()); @@ -287,7 +275,6 @@ mod tests { #[test] fn error_with_details_json_attaches_details() { use serde_json::json; - let err = Error::new(AppErrorKind::Validation, "invalid") .with_details_json(json!({"field": "email"})); assert!(err.details.is_some()); @@ -297,12 +284,10 @@ mod tests { #[test] fn error_with_details_serializes_payload() { use serde::Serialize; - #[derive(Serialize)] struct Extra { reason: &'static str } - let err = Error::new(AppErrorKind::BadRequest, "invalid") .with_details(Extra { reason: "missing" @@ -315,7 +300,6 @@ mod tests { #[test] fn error_with_backtrace_attaches_backtrace() { use std::backtrace::Backtrace; - let bt = Backtrace::capture(); let err = Error::new(AppErrorKind::Internal, "test").with_backtrace(bt); assert!(err.backtrace.is_some()); @@ -325,11 +309,9 @@ mod tests { #[test] fn error_with_shared_backtrace_reuses_arc() { use std::{backtrace::Backtrace, sync::Arc}; - let bt = Arc::new(Backtrace::capture()); let bt_clone = Arc::clone(&bt); let err = Error::new(AppErrorKind::Internal, "test").with_shared_backtrace(bt); - assert!(err.backtrace.is_some()); assert_eq!(Arc::strong_count(&bt_clone), 2); } @@ -340,11 +322,9 @@ mod tests { use std::{io::Error as IoError, sync::Arc}; use crate::app_error::core::types::ContextAttachment; - let io_err = Arc::new(IoError::other("shared error")); let err = Error::new(AppErrorKind::Internal, "test") .with_context(ContextAttachment::Shared(io_err.clone())); - assert!(err.source_ref().is_some()); assert_eq!(Arc::strong_count(&io_err), 2); } diff --git a/src/app_error/core/backtrace.rs b/src/app_error/core/backtrace.rs index de01fe0..10bc4bf 100644 --- a/src/app_error/core/backtrace.rs +++ b/src/app_error/core/backtrace.rs @@ -73,7 +73,6 @@ fn detect_backtrace_preference() -> bool { if let Some(value) = test_backtrace_override::get() { return value; } - match env::var_os("RUST_BACKTRACE") { None => false, Some(value) => { @@ -198,28 +197,22 @@ mod tests { fn should_capture_caches_enabled_state() { reset_backtrace_preference(); set_backtrace_preference_override(Some(true)); - should_capture_backtrace(); - set_backtrace_preference_override(Some(false)); assert!( should_capture_backtrace(), "should use cached enabled state" ); - reset_backtrace_preference(); } #[test] fn detect_preference_respects_override() { reset_backtrace_preference(); - set_backtrace_preference_override(Some(true)); assert!(detect_backtrace_preference()); - set_backtrace_preference_override(Some(false)); assert!(!detect_backtrace_preference()); - reset_backtrace_preference(); } @@ -227,9 +220,7 @@ mod tests { fn detect_preference_returns_false_by_default() { reset_backtrace_preference(); set_backtrace_preference_override(None); - let result = detect_backtrace_preference(); - reset_backtrace_preference(); let _ = result; } @@ -238,9 +229,7 @@ mod tests { fn reset_clears_state_and_override() { set_backtrace_preference_override(Some(true)); BACKTRACE_STATE.store(BACKTRACE_STATE_ENABLED, AtomicOrdering::Release); - reset_backtrace_preference(); - assert_eq!( BACKTRACE_STATE.load(AtomicOrdering::Acquire), BACKTRACE_STATE_UNSET diff --git a/src/app_error/core/builder.rs b/src/app_error/core/builder.rs index d245856..2af4589 100644 --- a/src/app_error/core/builder.rs +++ b/src/app_error/core/builder.rs @@ -314,7 +314,6 @@ impl Error { { self.set_backtrace_slot(Arc::new(backtrace)); } - #[cfg(not(feature = "backtrace"))] { self.set_backtrace_slot(backtrace); diff --git a/src/app_error/core/display.rs b/src/app_error/core/display.rs index f12b92c..cc92573 100644 --- a/src/app_error/core/display.rs +++ b/src/app_error/core/display.rs @@ -10,6 +10,7 @@ use core::{ }; use super::error::Error; +use crate::{FieldRedaction, FieldValue, MessageEditPolicy}; /// Display mode for error output. /// @@ -103,7 +104,6 @@ impl DisplayMode { #[must_use] pub fn current() -> Self { static CACHED_MODE: AtomicU8 = AtomicU8::new(255); - let cached = CACHED_MODE.load(Ordering::Relaxed); if cached != 255 { return match cached { @@ -113,7 +113,6 @@ impl DisplayMode { _ => unreachable!() }; } - let mode = Self::detect(); CACHED_MODE.store(mode as u8, Ordering::Relaxed); mode @@ -125,7 +124,8 @@ impl DisplayMode { fn detect() -> Self { #[cfg(feature = "std")] { - if let Ok(env) = std::env::var("MASTERROR_ENV") { + use std::env::var; + if let Ok(env) = var("MASTERROR_ENV") { return match env.as_str() { "prod" | "production" => Self::Prod, "local" | "dev" | "development" => Self::Local, @@ -133,12 +133,10 @@ impl DisplayMode { _ => Self::detect_auto() }; } - - if std::env::var("KUBERNETES_SERVICE_HOST").is_ok() { + if var("KUBERNETES_SERVICE_HOST").is_ok() { return Self::Prod; } } - Self::detect_auto() } @@ -182,51 +180,35 @@ impl Error { fn fmt_prod_impl(&self, f: &mut Formatter<'_>) -> FmtResult { write!(f, r#"{{"kind":"{:?}","code":"{}""#, self.kind, self.code)?; - - if !matches!(self.edit_policy, super::types::MessageEditPolicy::Redact) + if !matches!(self.edit_policy, MessageEditPolicy::Redact) && let Some(msg) = &self.message { write!(f, ",\"message\":\"")?; write_json_escaped(f, msg.as_ref())?; write!(f, "\"")?; } - if !self.metadata.is_empty() { - let has_public_fields = - self.metadata - .iter_with_redaction() - .any(|(_, _, redaction)| { - !matches!( - redaction, - crate::app_error::metadata::FieldRedaction::Redact - ) - }); - + let has_public_fields = self + .metadata + .iter_with_redaction() + .any(|(_, _, redaction)| !matches!(redaction, FieldRedaction::Redact)); if has_public_fields { write!(f, r#","metadata":{{"#)?; let mut first = true; - for (name, value, redaction) in self.metadata.iter_with_redaction() { - if matches!( - redaction, - crate::app_error::metadata::FieldRedaction::Redact - ) { + if matches!(redaction, FieldRedaction::Redact) { continue; } - if !first { write!(f, ",")?; } first = false; - write!(f, r#""{}":"#, name)?; write_metadata_value(f, value)?; } - write!(f, "}}")?; } } - write!(f, "}}") } @@ -260,19 +242,15 @@ impl Error { #[cfg(feature = "colored")] { use crate::colored::style; - writeln!(f, "Error: {}", self.kind)?; writeln!(f, "Code: {}", style::error_code(self.code.to_string()))?; - if let Some(msg) = &self.message { writeln!(f, "Message: {}", style::error_message(msg))?; } - if let Some(source) = &self.source { writeln!(f)?; let mut current: &dyn CoreError = source.as_ref(); let mut depth = 0; - while depth < 10 { writeln!( f, @@ -280,7 +258,6 @@ impl Error { style::source_context("Caused by"), style::source_context(current.to_string()) )?; - if let Some(next) = current.source() { current = next; depth += 1; @@ -289,7 +266,6 @@ impl Error { } } } - if !self.metadata.is_empty() { writeln!(f)?; writeln!(f, "Context:")?; @@ -297,27 +273,21 @@ impl Error { writeln!(f, " {}: {}", style::metadata_key(key), value)?; } } - Ok(()) } - #[cfg(not(feature = "colored"))] { writeln!(f, "Error: {}", self.kind)?; writeln!(f, "Code: {}", self.code)?; - if let Some(msg) = &self.message { writeln!(f, "Message: {}", msg)?; } - if let Some(source) = &self.source { writeln!(f)?; let mut current: &dyn CoreError = source.as_ref(); let mut depth = 0; - while depth < 10 { writeln!(f, " Caused by: {}", current)?; - if let Some(next) = current.source() { current = next; depth += 1; @@ -326,7 +296,6 @@ impl Error { } } } - if !self.metadata.is_empty() { writeln!(f)?; writeln!(f, "Context:")?; @@ -334,7 +303,6 @@ impl Error { writeln!(f, " {}: {}", key, value)?; } } - Ok(()) } } @@ -367,31 +335,26 @@ impl Error { fn fmt_staging_impl(&self, f: &mut Formatter<'_>) -> FmtResult { write!(f, r#"{{"kind":"{:?}","code":"{}""#, self.kind, self.code)?; - - if !matches!(self.edit_policy, super::types::MessageEditPolicy::Redact) + if !matches!(self.edit_policy, MessageEditPolicy::Redact) && let Some(msg) = &self.message { write!(f, ",\"message\":\"")?; write_json_escaped(f, msg.as_ref())?; write!(f, "\"")?; } - if let Some(source) = &self.source { write!(f, r#","source_chain":["#)?; let mut current: &dyn CoreError = source.as_ref(); let mut depth = 0; let mut first = true; - while depth < 5 { if !first { write!(f, ",")?; } first = false; - write!(f, "\"")?; write_json_escaped(f, ¤t.to_string())?; write!(f, "\"")?; - if let Some(next) = current.source() { current = next; depth += 1; @@ -399,46 +362,30 @@ impl Error { break; } } - write!(f, "]")?; } - if !self.metadata.is_empty() { - let has_public_fields = - self.metadata - .iter_with_redaction() - .any(|(_, _, redaction)| { - !matches!( - redaction, - crate::app_error::metadata::FieldRedaction::Redact - ) - }); - + let has_public_fields = self + .metadata + .iter_with_redaction() + .any(|(_, _, redaction)| !matches!(redaction, FieldRedaction::Redact)); if has_public_fields { write!(f, r#","metadata":{{"#)?; let mut first = true; - for (name, value, redaction) in self.metadata.iter_with_redaction() { - if matches!( - redaction, - crate::app_error::metadata::FieldRedaction::Redact - ) { + if matches!(redaction, FieldRedaction::Redact) { continue; } - if !first { write!(f, ",")?; } first = false; - write!(f, r#""{}":"#, name)?; write_metadata_value(f, value)?; } - write!(f, "}}")?; } } - write!(f, "}}") } } @@ -462,12 +409,8 @@ fn write_json_escaped(f: &mut Formatter<'_>, s: &str) -> FmtResult { /// Writes a metadata field value in JSON format. #[allow(dead_code)] -fn write_metadata_value( - f: &mut Formatter<'_>, - value: &crate::app_error::metadata::FieldValue -) -> FmtResult { +fn write_metadata_value(f: &mut Formatter<'_>, value: &FieldValue) -> FmtResult { use crate::app_error::metadata::FieldValue; - match value { FieldValue::Str(s) => { write!(f, "\"")?; @@ -526,7 +469,6 @@ mod tests { fn fmt_prod_outputs_json() { let error = AppError::not_found("User not found"); let output = format!("{}", error.fmt_prod_wrapper()); - assert!(output.contains(r#""kind":"NotFound""#)); assert!(output.contains(r#""code":"NOT_FOUND""#)); assert!(output.contains(r#""message":"User not found""#)); @@ -536,7 +478,6 @@ mod tests { fn fmt_prod_excludes_redacted_message() { let error = AppError::internal("secret").redactable(); let output = format!("{}", error.fmt_prod_wrapper()); - assert!(!output.contains("secret")); } @@ -544,7 +485,6 @@ mod tests { fn fmt_prod_includes_metadata() { let error = AppError::not_found("User not found").with_field(field::u64("user_id", 12345)); let output = format!("{}", error.fmt_prod_wrapper()); - assert!(output.contains(r#""metadata""#)); assert!(output.contains(r#""user_id":12345"#)); } @@ -553,7 +493,6 @@ mod tests { fn fmt_prod_excludes_sensitive_metadata() { let error = AppError::internal("Error").with_field(field::str("password", "secret")); let output = format!("{}", error.fmt_prod_wrapper()); - assert!(!output.contains("secret")); } @@ -561,7 +500,6 @@ mod tests { fn fmt_local_outputs_human_readable() { let error = AppError::not_found("User not found"); let output = format!("{}", error.fmt_local_wrapper()); - assert!(output.contains("Error:")); assert!(output.contains("Code: NOT_FOUND")); assert!(output.contains("Message: User not found")); @@ -571,11 +509,9 @@ mod tests { #[test] fn fmt_local_includes_source_chain() { use std::io::Error as IoError; - let io_err = IoError::other("connection failed"); let error = AppError::internal("Database error").with_source(io_err); let output = format!("{}", error.fmt_local_wrapper()); - assert!(output.contains("Caused by")); assert!(output.contains("connection failed")); } @@ -584,7 +520,6 @@ mod tests { fn fmt_staging_outputs_json_with_context() { let error = AppError::service("Service unavailable"); let output = format!("{}", error.fmt_staging_wrapper()); - assert!(output.contains(r#""kind":"Service""#)); assert!(output.contains(r#""code":"SERVICE""#)); } @@ -593,11 +528,9 @@ mod tests { #[test] fn fmt_staging_includes_source_chain() { use std::io::Error as IoError; - let io_err = IoError::other("timeout"); let error = AppError::network("Network error").with_source(io_err); let output = format!("{}", error.fmt_staging_wrapper()); - assert!(output.contains(r#""source_chain""#)); assert!(output.contains("timeout")); } @@ -606,7 +539,6 @@ mod tests { fn fmt_prod_escapes_special_chars() { let error = AppError::internal("Line\nwith\"quotes\""); let output = format!("{}", error.fmt_prod_wrapper()); - assert!(output.contains(r#"\n"#)); assert!(output.contains(r#"\""#)); } @@ -615,18 +547,15 @@ mod tests { fn fmt_prod_handles_infinity_in_metadata() { let error = AppError::internal("Error").with_field(field::f64("ratio", f64::INFINITY)); let output = format!("{}", error.fmt_prod_wrapper()); - assert!(output.contains("null")); } #[test] fn fmt_prod_formats_duration_metadata() { use core::time::Duration; - let error = AppError::internal("Error") .with_field(field::duration("elapsed", Duration::from_millis(1500))); let output = format!("{}", error.fmt_prod_wrapper()); - assert!(output.contains(r#""secs":1"#)); assert!(output.contains(r#""nanos":500000000"#)); } @@ -635,7 +564,6 @@ mod tests { fn fmt_prod_formats_bool_metadata() { let error = AppError::internal("Error").with_field(field::bool("active", true)); let output = format!("{}", error.fmt_prod_wrapper()); - assert!(output.contains(r#""active":true"#)); } @@ -643,22 +571,18 @@ mod tests { #[test] fn fmt_prod_formats_ip_metadata() { use std::net::IpAddr; - let ip: IpAddr = "192.168.1.1".parse().unwrap(); let error = AppError::internal("Error").with_field(field::ip("client_ip", ip)); let output = format!("{}", error.fmt_prod_wrapper()); - assert!(output.contains(r#""client_ip":"192.168.1.1""#)); } #[test] fn fmt_prod_formats_uuid_metadata() { use uuid::Uuid; - let uuid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap(); let error = AppError::internal("Error").with_field(field::uuid("request_id", uuid)); let output = format!("{}", error.fmt_prod_wrapper()); - assert!(output.contains(r#""request_id":"550e8400-e29b-41d4-a716-446655440000""#)); } @@ -668,7 +592,6 @@ mod tests { let json = serde_json::json!({"nested": "value"}); let error = AppError::internal("Error").with_field(field::json("data", json)); let output = format!("{}", error.fmt_prod_wrapper()); - assert!(output.contains(r#""data":"#)); } @@ -676,7 +599,6 @@ mod tests { fn fmt_prod_without_message() { let error = AppError::bare(crate::AppErrorKind::Internal); let output = format!("{}", error.fmt_prod_wrapper()); - assert!(output.contains(r#""kind":"Internal""#)); assert!(!output.contains(r#""message""#)); } @@ -685,7 +607,6 @@ mod tests { fn fmt_local_without_message() { let error = AppError::bare(crate::AppErrorKind::BadRequest); let output = format!("{}", error.fmt_local_wrapper()); - assert!(output.contains("Error:")); assert!(!output.contains("Message:")); } @@ -696,7 +617,6 @@ mod tests { .with_field(field::str("key", "value")) .with_field(field::i64("count", -42)); let output = format!("{}", error.fmt_local_wrapper()); - assert!(output.contains("Context:")); assert!(output.contains("key: value")); assert!(output.contains("count: -42")); @@ -706,7 +626,6 @@ mod tests { fn fmt_staging_without_message() { let error = AppError::bare(crate::AppErrorKind::Timeout); let output = format!("{}", error.fmt_staging_wrapper()); - assert!(output.contains(r#""kind":"Timeout""#)); assert!(!output.contains(r#""message""#)); } @@ -715,7 +634,6 @@ mod tests { fn fmt_staging_with_metadata() { let error = AppError::service("Service error").with_field(field::u64("retry_count", 3)); let output = format!("{}", error.fmt_staging_wrapper()); - assert!(output.contains(r#""metadata""#)); assert!(output.contains(r#""retry_count":3"#)); } @@ -724,7 +642,6 @@ mod tests { fn fmt_staging_with_redacted_message() { let error = AppError::internal("sensitive data").redactable(); let output = format!("{}", error.fmt_staging_wrapper()); - assert!(!output.contains("sensitive data")); } @@ -732,7 +649,6 @@ mod tests { fn fmt_prod_escapes_control_chars() { let error = AppError::internal("test\x00\x1F"); let output = format!("{}", error.fmt_prod_wrapper()); - assert!(output.contains(r#"\u0000"#)); assert!(output.contains(r#"\u001f"#)); } @@ -741,7 +657,6 @@ mod tests { fn fmt_prod_escapes_tab_and_carriage_return() { let error = AppError::internal("line\ttab\rreturn"); let output = format!("{}", error.fmt_prod_wrapper()); - assert!(output.contains(r#"\t"#)); assert!(output.contains(r#"\r"#)); } @@ -797,11 +712,9 @@ mod tests { #[test] fn fmt_local_with_deep_source_chain() { use std::io::{Error as IoError, ErrorKind}; - let io1 = IoError::new(ErrorKind::NotFound, "level 1"); let io2 = IoError::other(io1); let error = AppError::internal("top").with_source(io2); - let output = format!("{}", error.fmt_local_wrapper()); assert!(output.contains("Caused by")); assert!(output.contains("level 1")); @@ -822,11 +735,9 @@ mod tests { #[test] fn fmt_staging_with_deep_source_chain() { use std::io::{Error as IoError, ErrorKind}; - let io1 = IoError::new(ErrorKind::NotFound, "inner error"); let io2 = IoError::other(io1); let error = AppError::service("outer").with_source(io2); - let output = format!("{}", error.fmt_staging_wrapper()); assert!(output.contains(r#""source_chain""#)); assert!(output.contains("inner error")); diff --git a/src/app_error/core/error.rs b/src/app_error/core/error.rs index 954fd6b..271b89e 100644 --- a/src/app_error/core/error.rs +++ b/src/app_error/core/error.rs @@ -97,18 +97,14 @@ impl Display for Error { { Display::fmt(&self.kind, f) } - #[cfg(feature = "colored")] { use crate::colored::style; - writeln!(f, "Error: {}", self.kind)?; writeln!(f, "Code: {}", style::error_code(self.code.to_string()))?; - if let Some(msg) = &self.message { writeln!(f, "Message: {}", style::error_message(msg))?; } - if let Some(source) = &self.source { writeln!(f)?; let mut current: &dyn CoreError = source.as_ref(); @@ -127,7 +123,6 @@ impl Display for Error { } } } - if !self.metadata.is_empty() { writeln!(f)?; writeln!(f, "Context:")?; @@ -135,7 +130,6 @@ impl Display for Error { writeln!(f, " {}: {}", style::metadata_key(key), value)?; } } - Ok(()) } } diff --git a/src/app_error/core/introspection.rs b/src/app_error/core/introspection.rs index 67e29c6..f27c6d8 100644 --- a/src/app_error/core/introspection.rs +++ b/src/app_error/core/introspection.rs @@ -8,6 +8,8 @@ use core::error::Error as CoreError; #[cfg(feature = "backtrace")] use {alloc::sync::Arc, std::backtrace::Backtrace}; +#[cfg(feature = "backtrace")] +use super::backtrace::capture_backtrace_snapshot; use super::{ error::Error, types::{CapturedBacktrace, ErrorChain} @@ -62,9 +64,8 @@ impl Error { if let Some(backtrace) = self.backtrace.as_ref() { return Some(Arc::clone(backtrace)); } - self.captured_backtrace - .get_or_init(super::backtrace::capture_backtrace_snapshot) + .get_or_init(capture_backtrace_snapshot) .as_ref() .map(Arc::clone) } diff --git a/src/app_error/core/telemetry.rs b/src/app_error/core/telemetry.rs index fcf5643..b17b759 100644 --- a/src/app_error/core/telemetry.rs +++ b/src/app_error/core/telemetry.rs @@ -75,7 +75,6 @@ impl Error { if let Some(backtrace) = self.backtrace.as_deref() { return Some(backtrace); } - self.captured_backtrace .get_or_init(capture_backtrace_snapshot) .as_deref() @@ -95,7 +94,6 @@ impl Error { if self.take_dirty() { #[cfg(feature = "backtrace")] let _ = self.capture_backtrace(); - #[cfg(feature = "metrics")] { let code_label = self.code.as_str().to_owned(); @@ -108,7 +106,6 @@ impl Error { .increment(1); } } - #[cfg(feature = "tracing")] self.flush_tracing(); } @@ -125,16 +122,13 @@ impl Error { if !self.take_tracing_dirty() { return; } - if !tracing::event_enabled!(target: "masterror::error", Level::ERROR) { rebuild_interest_cache(); - if !tracing::event_enabled!(target: "masterror::error", Level::ERROR) { self.mark_tracing_dirty(); return; } } - let message = self.message.as_deref(); let retry_seconds = self.retry.map(|value| value.after_seconds); let trace_id = log_mdc::get("trace_id", |value| value.map(str::to_owned)); diff --git a/src/app_error/metadata.rs b/src/app_error/metadata.rs index 2280cde..6c1f429 100644 --- a/src/app_error/metadata.rs +++ b/src/app_error/metadata.rs @@ -85,7 +85,6 @@ fn duration_parts(duration: Duration) -> (u64, Option) { if nanos == 0 { return (secs, None); } - let mut fraction = nanos; let mut width = 9u8; loop { @@ -96,7 +95,6 @@ fn duration_parts(duration: Duration) -> (u64, Option) { fraction = divided; width -= 1; } - ( secs, Some(TrimmedFraction { @@ -215,12 +213,10 @@ fn infer_default_redaction(name: &str) -> FieldRedaction { { return FieldRedaction::Redact; } - let mut card_like = false; let mut number_like = false; let has_token = contains_ascii_case_insensitive(name, "token"); let has_key = contains_ascii_case_insensitive(name, "key"); - for segment in name.split(['.', '_', '-', ':', '/']) { if segment.is_empty() { continue; @@ -235,7 +231,6 @@ fn infer_default_redaction(name: &str) -> FieldRedaction { { return FieldRedaction::Hash; } - if segment.eq_ignore_ascii_case("card") || segment.eq_ignore_ascii_case("iban") || segment.eq_ignore_ascii_case("pan") @@ -244,7 +239,6 @@ fn infer_default_redaction(name: &str) -> FieldRedaction { { card_like = true; } - if segment.eq_ignore_ascii_case("number") || segment.eq_ignore_ascii_case("no") || segment.eq_ignore_ascii_case("id") @@ -252,7 +246,6 @@ fn infer_default_redaction(name: &str) -> FieldRedaction { number_like = true; } } - if card_like && number_like { FieldRedaction::Last4 } else { @@ -274,10 +267,8 @@ fn contains_ascii_case_insensitive(haystack: &str, needle: &str) -> bool { if needle.is_empty() { return true; } - let haystack_bytes = haystack.as_bytes(); let needle_bytes = needle.as_bytes(); - haystack_bytes.len() >= needle_bytes.len() && haystack_bytes .windows(needle_bytes.len()) @@ -523,7 +514,6 @@ mod tests { let mut meta = Metadata::new(); meta.insert(field::str("request_id", Cow::Borrowed("abc"))); meta.insert(field::i64("count", 42)); - assert_eq!( meta.get("request_id"), Some(&FieldValue::Str(Cow::Borrowed("abc"))) @@ -550,7 +540,6 @@ mod tests { field::duration("elapsed", Duration::from_millis(1500)), field::ip("peer", IpAddr::from(Ipv4Addr::new(192, 168, 0, 1))) ]); - assert!(meta.get("ratio").is_some_and( |value| matches!(value, FieldValue::F64(ratio) if ratio.to_bits() == 0.25f64.to_bits()) )); @@ -586,10 +575,8 @@ mod tests { fn default_redaction_applies_to_common_keys() { let password = field::str("password", Cow::Borrowed("secret")); assert!(matches!(password.redaction(), FieldRedaction::Redact)); - let token = field::str("api_token", Cow::Borrowed("abcdef")); assert!(matches!(token.redaction(), FieldRedaction::Hash)); - let card = field::str("card_number", Cow::Borrowed("4111111111111111")); assert!(matches!(card.redaction(), FieldRedaction::Last4)); } @@ -603,7 +590,6 @@ mod tests { ("RefreshToken", FieldRedaction::Hash), ("CARD_NUMBER", FieldRedaction::Last4) ]; - for (name, expected) in cases { let field = field::str(name, Cow::Borrowed("value")); assert!( diff --git a/src/app_error/tests.rs b/src/app_error/tests.rs index 7e2787f..4151ca8 100644 --- a/src/app_error/tests.rs +++ b/src/app_error/tests.rs @@ -89,7 +89,6 @@ mod telemetry_support { if event.metadata().target() != "masterror::error" { return; } - let mut record = RecordedEvent::default(); event.record(&mut EventVisitor { record: &mut record @@ -238,7 +237,6 @@ fn constructors_match_kinds() { #[test] fn with_context_attaches_plain_source() { let err = AppError::internal("boom").with_context(IoError::from(IoErrorKind::Other)); - let source = err.source_ref().expect("stored source"); assert!(source.is::()); assert_eq!(source.to_string(), IoErrorKind::Other.to_string()); @@ -249,7 +247,6 @@ fn with_context_attaches_plain_source() { fn with_context_accepts_anyhow_error() { let upstream: AnyhowError = anyhow::anyhow!("context failed"); let err = AppError::service("downstream").with_context(AnyhowSource(upstream)); - let source = err.source_ref().expect("stored source"); let stored = source .downcast_ref::() @@ -261,10 +258,8 @@ fn with_context_accepts_anyhow_error() { fn database_accepts_optional_message() { let with_msg = AppError::database_with_message("db down"); assert_err_with_msg(with_msg, AppErrorKind::Database, "db down"); - let via_option = AppError::database(Some(Cow::Borrowed("db down"))); assert_err_with_msg(via_option, AppErrorKind::Database, "db down"); - let without = AppError::database(None); assert_err_bare(without, AppErrorKind::Database); } @@ -301,11 +296,9 @@ fn context_moves_dynamic_code_without_cloning() { let dynamic_code = AppCode::try_new(String::from("THIRD_PARTY_FAILURE")).expect("valid dynamic code"); let expected_ptr = dynamic_code.as_str().as_ptr(); - let err = Result::<(), IoError>::Err(IoError::from(IoErrorKind::Other)) .ctx(|| Context::new(AppErrorKind::Service).code(dynamic_code)) .unwrap_err(); - assert_eq!(err.code.as_str().as_ptr(), expected_ptr); } @@ -323,7 +316,6 @@ fn metadata_and_code_are_preserved() { .with_field(field::str("request_id", "abc-123")) .with_field(field::i64("attempt", 2)) .with_code(AppCode::Service); - assert_eq!(err.code, AppCode::Service); let metadata = err.metadata(); assert_eq!(metadata.len(), 2); @@ -339,7 +331,6 @@ fn custom_literal_codes_flow_into_responses() { let custom = AppCode::new("INVALID_JSON"); let err = AppError::bad_request("invalid").with_code(custom.clone()); assert_eq!(err.code, custom); - let response: ErrorResponse = err.into(); assert_eq!(response.code, custom); } @@ -349,7 +340,6 @@ fn dynamic_codes_flow_into_responses() { let custom = AppCode::try_new(String::from("THIRD_PARTY_FAILURE")).expect("valid code"); let err = AppError::service("down").with_code(custom.clone()); assert_eq!(err.code, custom); - let response: ErrorResponse = err.into(); assert_eq!(response.code, custom); } @@ -358,7 +348,6 @@ fn dynamic_codes_flow_into_responses() { #[test] fn with_details_json_attaches_payload() { use serde_json::json; - let payload = json!({"field": "email"}); let err = AppError::validation("invalid").with_details_json(payload.clone()); assert_eq!(err.details, Some(payload)); @@ -368,9 +357,7 @@ fn with_details_json_attaches_payload() { #[test] fn with_details_serialization_failure_is_bad_request() { use serde::{Serialize, Serializer}; - struct Failing; - impl Serialize for Failing { fn serialize(&self, _: S) -> Result where @@ -379,7 +366,6 @@ fn with_details_serialization_failure_is_bad_request() { Err(serde::ser::Error::custom("nope")) } } - let err = AppError::internal("boom") .with_details(Failing) .expect_err("should fail"); @@ -398,7 +384,6 @@ fn context_with_preserves_default_redaction() { let err = super::Context::new(AppErrorKind::Service) .with(field::str("request_id", "abc-123")) .into_error(DummyError); - let metadata = err.metadata(); assert_eq!(metadata.len(), 1); assert_eq!( @@ -414,7 +399,6 @@ fn context_redact_field_overrides_policy() { .with(field::str("token", "super-secret")) .redact_field("token", FieldRedaction::Redact) .into_error(DummyError); - let metadata = err.metadata(); assert_eq!( metadata.get("token"), @@ -429,7 +413,6 @@ fn context_redact_field_before_insertion_applies_policy() { .redact_field("token", FieldRedaction::Hash) .with(field::str("token", "super-secret")) .into_error(DummyError); - let metadata = err.metadata(); assert_eq!( metadata.get("token"), @@ -443,7 +426,6 @@ fn context_redact_field_mut_applies_policies() { let mut context = super::Context::new(AppErrorKind::Service); let _ = context.redact_field_mut("token", FieldRedaction::Hash); context = context.with(field::str("token", "super-secret")); - let err = context.into_error(DummyError); let metadata = err.metadata(); assert_eq!( @@ -460,7 +442,6 @@ fn context_with_uses_latest_matching_policy() { .redact_field("token", FieldRedaction::Redact) .with(field::str("token", "super-secret")) .into_error(DummyError); - let metadata = err.metadata(); assert_eq!( metadata.get("token"), @@ -474,7 +455,6 @@ fn app_error_redact_field_updates_metadata() { let err = AppError::internal("boom") .with_field(field::str("api_key", "key")) .redact_field("api_key", FieldRedaction::Hash); - assert_eq!( err.metadata().redaction("api_key"), Some(FieldRedaction::Hash) @@ -500,9 +480,7 @@ impl StdError for DummyError {} fn source_is_preserved_without_extra_allocation() { let source = Arc::new(DummyError); let err = AppError::internal("boom").with_source_arc(source.clone()); - assert_eq!(Arc::strong_count(&source), 2); - let stored = err.source_ref().expect("source"); let stored_dummy = stored .downcast_ref::() @@ -516,19 +494,16 @@ fn error_chain_is_preserved() { struct NestedError { inner: DummyError } - impl Display for NestedError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.inner.fmt(f) } } - impl StdError for NestedError { fn source(&self) -> Option<&(dyn StdError + 'static)> { Some(&self.inner) } } - let err = AppError::internal("boom").with_source(NestedError { inner: DummyError }); @@ -572,10 +547,9 @@ fn redactable_policy_is_exposed() { assert!(matches!(err.edit_policy, MessageEditPolicy::Redact)); } +/// Smoke test to ensure `log()` is callable; tracing output isn't asserted. #[test] fn log_uses_kind_and_code() { - // Smoke test to ensure the method is callable; tracing output isn't asserted - // here. let err = AppError::internal("boom"); err.log(); } @@ -584,23 +558,18 @@ fn log_uses_kind_and_code() { #[test] fn telemetry_emits_single_tracing_event_with_trace_id() { let _guard = TELEMETRY_GUARD.lock().expect("telemetry guard"); - use telemetry_support::new_recording_dispatch; use tracing::{callsite::rebuild_interest_cache, dispatcher}; - let (dispatch, events) = new_recording_dispatch(); let events = events.clone(); - dispatcher::with_default(&dispatch, || { rebuild_interest_cache(); log_mdc::insert("trace_id", "trace-123"); let err = AppError::internal("boom"); err.log(); log_mdc::remove("trace_id"); - let events = events.lock().expect("events lock"); assert_eq!(events.len(), 1, "expected exactly one tracing event"); - let event = &events[0]; assert_eq!(event.code.as_deref(), Some(AppCode::Internal.as_str())); assert_eq!(event.category.as_deref(), Some("Internal")); @@ -617,19 +586,15 @@ fn telemetry_emits_single_tracing_event_with_trace_id() { #[test] fn telemetry_flushes_after_subscriber_install() { let _guard = TELEMETRY_GUARD.lock().expect("telemetry guard"); - use telemetry_support::new_recording_dispatch; use tracing::{callsite::rebuild_interest_cache, dispatcher}; - let (dispatch, events) = new_recording_dispatch(); let events_clone = events.clone(); - dispatcher::with_default(&dispatch, || { rebuild_interest_cache(); let err = AppError::internal("boom"); err.log(); drop(err); - let events = events_clone.lock().expect("events lock"); assert_eq!( events.len(), @@ -653,13 +618,11 @@ fn metrics_counter_is_incremented_once() { use metrics::{ Counter, CounterFn, Gauge, Histogram, Key, KeyName, Metadata, Recorder, SharedString, Unit }; - #[derive(Clone, Debug, Eq, PartialEq, Hash)] struct CounterKey { name: String, labels: Vec<(String, String)> } - impl CounterKey { fn new(name: String, labels: Vec<(String, String)>) -> Self { Self { @@ -668,32 +631,26 @@ fn metrics_counter_is_incremented_once() { } } } - type CounterMap = HashMap; type SharedCounterMap = Arc>; - #[derive(Clone)] struct MetricsCounterHandle { key: CounterKey, counts: SharedCounterMap } - impl CounterFn for MetricsCounterHandle { fn increment(&self, value: u64) { let mut map = self.counts.lock().expect("counter map"); *map.entry(self.key.clone()).or_default() += value; } - fn absolute(&self, value: u64) { let mut map = self.counts.lock().expect("counter map"); map.insert(self.key.clone(), value); } } - struct CountingRecorder { counts: SharedCounterMap } - impl Recorder for CountingRecorder { fn describe_counter( &self, @@ -702,9 +659,7 @@ fn metrics_counter_is_incremented_once() { _description: SharedString ) { } - fn describe_gauge(&self, _key: KeyName, _unit: Option, _description: SharedString) {} - fn describe_histogram( &self, _key: KeyName, @@ -712,7 +667,6 @@ fn metrics_counter_is_incremented_once() { _description: SharedString ) { } - fn register_counter(&self, key: &Key, _metadata: &Metadata<'_>) -> Counter { let labels = key .labels() @@ -724,20 +678,15 @@ fn metrics_counter_is_incremented_once() { counts: self.counts.clone() })) } - fn register_gauge(&self, _key: &Key, _metadata: &Metadata<'_>) -> Gauge { Gauge::noop() } - fn register_histogram(&self, _key: &Key, _metadata: &Metadata<'_>) -> Histogram { Histogram::noop() } } - use std::sync::OnceLock; - static RECORDER_COUNTS: OnceLock = OnceLock::new(); - let counts = RECORDER_COUNTS .get_or_init(|| { let counts = Arc::new(Mutex::new(HashMap::new())); @@ -748,12 +697,9 @@ fn metrics_counter_is_incremented_once() { counts }) .clone(); - counts.lock().expect("counter map").clear(); - let err = AppError::forbidden("denied"); err.log(); - let key = CounterKey::new( "error_total".to_owned(), vec![ @@ -761,7 +707,6 @@ fn metrics_counter_is_incremented_once() { ("category".to_owned(), "Forbidden".to_owned()), ] ); - let counts = counts.lock().expect("counter map"); assert_eq!(counts.get(&key).copied(), Some(1)); } @@ -770,14 +715,13 @@ fn metrics_counter_is_incremented_once() { fn result_alias_is_generic() { let default_result: super::AppResult = Ok(1); let custom_result: super::AppResult = Ok(2); - assert!(matches!(default_result, Ok(value) if value == 1)); assert!(matches!(custom_result, Ok(value) if value == 2)); } #[test] fn app_error_fits_result_budget() { - let size = std::mem::size_of::(); + let size = size_of::(); assert!( size <= 128, "AppError grew to {size} bytes; keep the Err variant lean" @@ -789,10 +733,8 @@ fn app_error_fits_result_budget() { fn error_chain_iterates_through_sources() { let io_err = IoError::other("disk offline"); let app_err = AppError::internal("db down").with_context(io_err); - let chain: Vec<_> = app_err.chain().collect(); assert_eq!(chain.len(), 2); - let first_err = chain[0].to_string(); assert!( first_err.contains("Internal") @@ -811,7 +753,6 @@ fn error_chain_iterates_through_sources() { fn error_chain_single_error() { let err = AppError::bad_request("missing field"); let chain: Vec<_> = err.chain().collect(); - assert_eq!(chain.len(), 1); let err_str = chain[0].to_string(); assert!(err_str.contains("Bad") || err_str.contains("BAD")); @@ -828,7 +769,6 @@ fn error_chain_multiple_sources() { let root = IoError::new(IoErrorKind::NotFound, "file not found"); let wrapped = IoError::other(format!("config error: {}", root)); let app_err = AppError::internal("startup failed").with_context(wrapped); - let chain: Vec<_> = app_err.chain().collect(); assert_eq!(chain.len(), 2); } @@ -838,7 +778,6 @@ fn error_chain_multiple_sources() { fn root_cause_returns_deepest_error() { let io_err = IoError::other("disk offline"); let app_err = AppError::internal("db down").with_context(io_err); - let root = app_err.root_cause(); assert_eq!(root.to_string(), "disk offline"); } @@ -849,7 +788,6 @@ fn root_cause_returns_self_when_no_source() { let err = AppError::timeout("operation timed out"); let root = err.root_cause(); let root_str = root.to_string(); - assert!( root_str.contains("timed out") || root_str.contains("TIMEOUT") @@ -866,9 +804,7 @@ fn root_cause_returns_self_when_no_source() { fn is_checks_source_type() { let io_err = IoError::other("disk offline"); let app_err = AppError::internal("db down").with_context(io_err); - assert!(app_err.is::()); - let anyhow_err = anyhow::anyhow!("test error"); let anyhow_app_err = AppError::internal("wrapped").with_context(AnyhowSource(anyhow_err)); assert!(!anyhow_app_err.is::()); @@ -878,7 +814,6 @@ fn is_checks_source_type() { #[cfg(feature = "std")] fn is_returns_false_when_no_source() { let err = AppError::not_found("user not found"); - assert!(!err.is::()); assert!(!err.is::()); } @@ -888,7 +823,6 @@ fn is_returns_false_when_no_source() { fn downcast_ref_retrieves_source() { let io_err = IoError::other("disk offline"); let app_err = AppError::internal("db down").with_context(io_err); - let retrieved = app_err.downcast_ref::().expect("should downcast"); assert_eq!(retrieved.to_string(), "disk offline"); } @@ -898,7 +832,6 @@ fn downcast_ref_retrieves_source() { fn downcast_ref_returns_none_when_wrong_type() { let io_err = IoError::other("disk offline"); let app_err = AppError::internal("db down").with_context(io_err); - assert!(app_err.downcast_ref::().is_none()); } @@ -914,7 +847,6 @@ fn downcast_ref_returns_none_when_no_source() { fn colored_display_bare_error_without_message() { let err = AppError::bare(AppErrorKind::Internal); let output = format!("{}", err); - assert!(output.contains("Internal server error")); assert!(output.contains("Code:")); assert!(output.contains("INTERNAL")); @@ -924,25 +856,21 @@ fn colored_display_bare_error_without_message() { #[cfg(feature = "colored")] fn colored_display_deep_error_chain() { use crate::field; - #[derive(Debug)] struct CustomError { msg: String, source: Option } - impl Display for CustomError { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { write!(f, "{}", self.msg) } } - impl StdError for CustomError { fn source(&self) -> Option<&(dyn StdError + 'static)> { self.source.as_ref().map(|e| e as &(dyn StdError + 'static)) } } - let root = IoError::other("disk full"); let mid = CustomError { msg: "write failed".to_string(), @@ -951,9 +879,7 @@ fn colored_display_deep_error_chain() { let top = AppError::internal("operation failed") .with_context(mid) .with_field(field::str("operation", "backup")); - let output = format!("{}", top); - assert!(output.contains("Internal server error")); assert!(output.contains("INTERNAL")); assert!(output.contains("operation failed")); @@ -969,18 +895,14 @@ fn colored_display_deep_error_chain() { #[cfg(feature = "std")] fn with_metadata_replaces_all_metadata() { use crate::{Metadata, field}; - let err = AppError::internal("test") .with_field(field::str("key1", "value1")) .with_field(field::u64("key2", 42)); - let new_metadata = Metadata::from_fields(vec![ field::str("new_key", "new_value"), field::u64("count", 100), ]); - let err = err.with_metadata(new_metadata); - let metadata = err.metadata(); assert!(metadata.get("new_key").is_some()); assert!(metadata.get("count").is_some()); @@ -993,11 +915,9 @@ fn with_metadata_replaces_all_metadata() { fn with_context_handles_arc_source() { let io_err = IoError::other("network down"); let arc_source: Arc = Arc::new(io_err); - let err1 = AppError::internal("first") .with_context(Arc::clone(&arc_source) as Arc); let err2 = AppError::internal("second").with_context(arc_source); - assert!(err1.source_ref().is_some()); assert!(err2.source_ref().is_some()); assert_eq!(err1.source_ref().unwrap().to_string(), "network down"); @@ -1010,9 +930,7 @@ fn with_context_handles_boxed_arc_downcast() { let io_err = IoError::other("boxed arc"); let arc_source: Arc = Arc::new(io_err); let boxed_arc: Box> = Box::new(arc_source); - let err = AppError::internal("test").with_context(*boxed_arc); - assert!(err.source_ref().is_some()); assert_eq!(err.source_ref().unwrap().to_string(), "boxed arc"); } @@ -1213,7 +1131,6 @@ fn cache_constructor_creates_correct_kind() { fn constructors_accept_empty_strings() { let err = AppError::internal(""); assert_eq!(err.message.as_deref(), Some("")); - let err = AppError::validation(""); assert_eq!(err.message.as_deref(), Some("")); } @@ -1222,7 +1139,6 @@ fn constructors_accept_empty_strings() { fn constructors_accept_unicode_messages() { let err = AppError::not_found("リソースが見つかりません"); assert_eq!(err.message.as_deref(), Some("リソースが見つかりません")); - let err = AppError::validation("Неверный ввод"); assert_eq!(err.message.as_deref(), Some("Неверный ввод")); } diff --git a/src/code/app_code.rs b/src/code/app_code.rs index 9c0f518..d46b585 100644 --- a/src/code/app_code.rs +++ b/src/code/app_code.rs @@ -190,8 +190,9 @@ impl Hash for AppCode { } impl Display for AppCode { + /// Writes the stable human/machine readable form matching JSON + /// representation. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // Stable human/machine readable form matching JSON representation. f.write_str(self.as_str()) } } @@ -219,7 +220,6 @@ impl FromStr for AppCode { if let Some(code) = match_static(s) { return Ok(code); } - Self::try_new(s.to_owned()) } } @@ -231,7 +231,6 @@ impl From for AppCode { /// The mapping is 1:1 today and intentionally conservative. fn from(kind: AppErrorKind) -> Self { match kind { - // 4xx AppErrorKind::NotFound => Self::NotFound, AppErrorKind::Validation => Self::Validation, AppErrorKind::Conflict => Self::Conflict, @@ -242,8 +241,6 @@ impl From for AppCode { AppErrorKind::RateLimited => Self::RateLimited, AppErrorKind::TelegramAuth => Self::TelegramAuth, AppErrorKind::InvalidJwt => Self::InvalidJwt, - - // 5xx AppErrorKind::Internal => Self::Internal, AppErrorKind::Database => Self::Database, AppErrorKind::Service => Self::Service, @@ -276,28 +273,23 @@ impl<'de> Deserialize<'de> for AppCode { D: Deserializer<'de> { struct Visitor; - impl<'de> serde::de::Visitor<'de> for Visitor { type Value = AppCode; - fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("a SCREAMING_SNAKE_CASE code") } - fn visit_borrowed_str(self, value: &'de str) -> Result where E: serde::de::Error { AppCode::from_str(value).map_err(E::custom) } - fn visit_str(self, value: &str) -> Result where E: serde::de::Error { AppCode::from_str(value).map_err(E::custom) } - fn visit_string(self, value: String) -> Result where E: serde::de::Error @@ -305,7 +297,6 @@ impl<'de> Deserialize<'de> for AppCode { AppCode::try_new(value).map_err(E::custom) } } - deserializer.deserialize_str(Visitor) } } @@ -331,7 +322,6 @@ fn validate_code(value: &str) -> Result<(), ParseAppCodeError> { if !is_valid_literal(value) { return Err(ParseAppCodeError); } - Ok(()) } @@ -371,11 +361,9 @@ const fn is_valid_literal(value: &str) -> bool { if len == 0 { return false; } - if bytes[0] == b'_' || bytes[len - 1] == b'_' { return false; } - let mut index = 0; while index < len { let byte = bytes[index]; @@ -387,7 +375,6 @@ const fn is_valid_literal(value: &str) -> bool { } index += 1; } - true } @@ -407,9 +394,9 @@ mod tests { ); } + /// Spot checks to guard against accidental remaps. #[test] fn mapping_from_kind_is_stable() { - // Spot checks to guard against accidental remaps. assert_eq!(AppCode::from(AppErrorKind::NotFound), AppCode::NotFound); assert_eq!(AppCode::from(AppErrorKind::Validation), AppCode::Validation); assert_eq!(AppCode::from(AppErrorKind::Internal), AppCode::Internal); diff --git a/src/convert.rs b/src/convert.rs index 114a3b1..50de21f 100644 --- a/src/convert.rs +++ b/src/convert.rs @@ -197,14 +197,9 @@ mod tests { #[test] fn io_error_maps_to_internal_and_preserves_message() { use std::io::Error; - let src = Error::other("disk said nope"); let app: AppError = src.into(); - - // kind must be Internal assert!(matches!(app.kind, AppErrorKind::Internal)); - - // message should be preserved for logs/public payload assert_eq!(app.message.as_deref(), Some("disk said nope")); } @@ -213,7 +208,6 @@ mod tests { #[test] fn string_maps_to_bad_request_and_preserves_text() { let app: AppError = String::from("name must not be empty").into(); - assert!(matches!(app.kind, AppErrorKind::BadRequest)); assert_eq!(app.message.as_deref(), Some("name must not be empty")); } diff --git a/src/convert/actix.rs b/src/convert/actix.rs index 4ef8877..2d08072 100644 --- a/src/convert/actix.rs +++ b/src/convert/actix.rs @@ -123,10 +123,8 @@ mod actix_tests { let err = AppError::unauthorized("no token") .with_retry_after_secs(7) .with_www_authenticate("Bearer"); - let resp = err.error_response(); assert_eq!(resp.status().as_u16(), 401); - let headers = resp.headers().clone(); assert_eq!( headers.get(RETRY_AFTER).and_then(|v| v.to_str().ok()), @@ -136,7 +134,6 @@ mod actix_tests { headers.get(WWW_AUTHENTICATE).and_then(|v| v.to_str().ok()), Some("Bearer") ); - let bytes = to_bytes(resp.into_body()).await?; let body: serde_json::Value = serde_json::from_slice(&bytes)?; assert_eq!( diff --git a/src/convert/axum.rs b/src/convert/axum.rs index 3624136..8933e4a 100644 --- a/src/convert/axum.rs +++ b/src/convert/axum.rs @@ -58,7 +58,6 @@ impl AppError { /// `AppErrorKind::http_status()` mapping. #[inline] pub fn http_status(&self) -> StatusCode { - // `kind` is a field, not a method. self.kind.status_code() } } @@ -85,9 +84,7 @@ mod tests { #[test] fn http_status_maps_from_kind() { let e = AppError::forbidden("nope"); - // sanity: kind -> 403 assert_eq!(e.http_status(), StatusCode::FORBIDDEN); - let e = AppError::validation("bad"); assert_eq!(e.http_status(), StatusCode::UNPROCESSABLE_ENTITY); } @@ -101,40 +98,33 @@ mod tests { http::header::{CONTENT_TYPE, RETRY_AFTER, WWW_AUTHENTICATE}, response::IntoResponse }; - let app_err = AppError::unauthorized("missing token") .with_retry_after_secs(7) .with_www_authenticate("Bearer realm=\"api\""); let resp = app_err.into_response(); - assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); - let content_type = resp .headers() .get(CONTENT_TYPE) .and_then(|value| value.to_str().ok()) .expect("content-type header"); assert_eq!(content_type, "application/problem+json"); - let retry_after = resp .headers() .get(RETRY_AFTER) .and_then(|value| value.to_str().ok()) .expect("retry-after header"); assert_eq!(retry_after, "7"); - let www_authenticate = resp .headers() .get(WWW_AUTHENTICATE) .and_then(|value| value.to_str().ok()) .expect("www-authenticate header"); assert_eq!(www_authenticate, "Bearer realm=\"api\""); - let bytes = to_bytes(resp.into_body(), usize::MAX) .await .expect("read body"); let body: serde_json::Value = serde_json::from_slice(&bytes).expect("json body"); - assert_eq!( body.get("status").and_then(|value| value.as_u64()), Some(401) @@ -158,17 +148,13 @@ mod tests { #[tokio::test] async fn redacted_errors_hide_detail() { use axum::{body::to_bytes, response::IntoResponse}; - let app_err = AppError::internal("secret").redactable(); let resp = app_err.into_response(); - assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); - let bytes = to_bytes(resp.into_body(), usize::MAX) .await .expect("read body"); let body: serde_json::Value = serde_json::from_slice(&bytes).expect("json body"); - assert!(body.get("detail").is_none()); assert!(body.get("metadata").is_none()); } diff --git a/src/convert/init_data.rs b/src/convert/init_data.rs index 743bf59..a7db318 100644 --- a/src/convert/init_data.rs +++ b/src/convert/init_data.rs @@ -94,7 +94,6 @@ mod tests { InitDataError::SignatureMissing, InitDataError::SignatureInvalid("bad sig".into()), ]; - for case in cases { let app: Error = case.into(); assert!(matches!(app.kind, AppErrorKind::TelegramAuth)); diff --git a/src/convert/multipart.rs b/src/convert/multipart.rs index e19d6e6..251e668 100644 --- a/src/convert/multipart.rs +++ b/src/convert/multipart.rs @@ -90,11 +90,9 @@ impl From for Error { "http.is_client_error", status.is_client_error() )); - if let Some(reason) = status.canonical_reason() { context = context.with(field::str("http.status_reason", reason)); } - context.into_error(err) } } @@ -119,16 +117,13 @@ mod tests { ) .body(Body::from("not-a-multipart-body")) .expect("request"); - let mut multipart = Multipart::from_request(request, &()) .await .expect("extractor"); - let err = multipart.next_field().await.expect_err("error"); let status = err.status(); let body_text = err.body_text(); let app_err: Error = err.into(); - assert_eq!(app_err.kind, AppErrorKind::BadRequest); assert_eq!( app_err.metadata().get("multipart.reason"), @@ -157,14 +152,11 @@ mod tests { .header("content-type", "multipart/form-data; boundary=X") .body(Body::from("invalid-multipart-data")) .expect("request"); - let mut multipart = Multipart::from_request(request, &()) .await .expect("extractor"); - let err = multipart.next_field().await.expect_err("error"); let app_err: Error = err.into(); - assert_eq!(app_err.kind, AppErrorKind::BadRequest); assert!(app_err.metadata().get("multipart.reason").is_some()); } @@ -175,14 +167,11 @@ mod tests { .header("content-type", "multipart/form-data; boundary=BOUND") .body(Body::empty()) .expect("request"); - let mut multipart = Multipart::from_request(request, &()) .await .expect("extractor"); - let err = multipart.next_field().await.expect_err("error"); let app_err: Error = err.into(); - assert_eq!(app_err.kind, AppErrorKind::BadRequest); let metadata = app_err.metadata(); assert_eq!( @@ -197,15 +186,12 @@ mod tests { .header("content-type", "multipart/form-data; boundary=TEST") .body(Body::from("garbage")) .expect("request"); - let mut multipart = Multipart::from_request(request, &()) .await .expect("extractor"); - let err = multipart.next_field().await.expect_err("error"); let app_err: Error = err.into(); let metadata = app_err.metadata(); - assert!(metadata.get("http.status").is_some()); assert!(metadata.get("http.status_reason").is_some()); } @@ -216,15 +202,12 @@ mod tests { .header("content-type", "multipart/form-data; boundary=B") .body(Body::from("bad-data")) .expect("request"); - let mut multipart = Multipart::from_request(request, &()) .await .expect("extractor"); - let err = multipart.next_field().await.expect_err("error"); let original_message = err.body_text(); let app_err: Error = err.into(); - assert_eq!( app_err.metadata().get("multipart.reason"), Some(&FieldValue::Str(original_message.into())) @@ -237,15 +220,12 @@ mod tests { .header("content-type", "multipart/form-data; boundary=XYZ") .body(Body::from("invalid")) .expect("request"); - let mut multipart = Multipart::from_request(request, &()) .await .expect("extractor"); - let err = multipart.next_field().await.expect_err("error"); let status = err.status(); let app_err: Error = err.into(); - assert!(status.is_client_error()); assert_eq!(app_err.kind, AppErrorKind::BadRequest); } @@ -256,14 +236,11 @@ mod tests { .header("content-type", "multipart/form-data; boundary=ABC") .body(Body::from("malformed")) .expect("request"); - let mut multipart = Multipart::from_request(request, &()) .await .expect("extractor"); - let err = multipart.next_field().await.expect_err("error"); let app_err: Error = err.into(); - assert_eq!(app_err.kind, AppErrorKind::BadRequest); assert!(app_err.source.is_some()); } @@ -274,26 +251,20 @@ mod tests { .header("content-type", "multipart/form-data; boundary=A") .body(Body::from("bad1")) .expect("request"); - let mut multipart1 = Multipart::from_request(request1, &()) .await .expect("extractor"); - let err1 = multipart1.next_field().await.expect_err("error"); let app_err1: Error = err1.into(); - let request2 = Request::builder() .header("content-type", "multipart/form-data; boundary=B") .body(Body::from("bad2")) .expect("request"); - let mut multipart2 = Multipart::from_request(request2, &()) .await .expect("extractor"); - let err2 = multipart2.next_field().await.expect_err("error"); let app_err2: Error = err2.into(); - assert_eq!(app_err1.kind, AppErrorKind::BadRequest); assert_eq!(app_err2.kind, AppErrorKind::BadRequest); } diff --git a/src/convert/redis.rs b/src/convert/redis.rs index 61b41a3..bce2e3c 100644 --- a/src/convert/redis.rs +++ b/src/convert/redis.rs @@ -77,11 +77,9 @@ fn build_context(err: &RedisError) -> (Context, Option) { "redis.is_connection_dropped", err.is_connection_dropped() )); - if let Some(code) = err.code() { context = context.with(field::str("redis.code", code.to_owned())); } - if err.is_timeout() { context = context.category(AppErrorKind::Timeout); } else if err.is_connection_refusal() @@ -92,20 +90,16 @@ fn build_context(err: &RedisError) -> (Context, Option) { { context = context.category(AppErrorKind::DependencyUnavailable); } - if let Some((addr, slot)) = err.redirect_node() { context = context .with(field::str("redis.redirect_addr", addr.to_owned())) .with(field::u64("redis.redirect_slot", u64::from(slot))); } - let (retry_method_label, retry_after) = retry_method_details(err.retry_method()); context = context.with(field::str("redis.retry_method", retry_method_label)); - if let Some(secs) = retry_after { context = context.with(field::u64("redis.retry_after_hint_secs", secs)); } - (context, retry_after) } diff --git a/src/convert/reqwest.rs b/src/convert/reqwest.rs index 05f5c98..7b73c31 100644 --- a/src/convert/reqwest.rs +++ b/src/convert/reqwest.rs @@ -107,22 +107,18 @@ fn classify_reqwest_error(err: &ReqwestError) -> (Context, Option) { .with(field::bool("reqwest.is_body", err.is_body())) .with(field::bool("reqwest.is_decode", err.is_decode())) .with(field::bool("reqwest.is_redirect", err.is_redirect())); - let mut retry_after = None; - if err.is_timeout() { context = context.category(AppErrorKind::Timeout); } else if err.is_connect() || err.is_request() { context = context.category(AppErrorKind::Network); } - if let Some(status) = err.status() { let status_code = u16::from(status); context = context.with(field::u64("http.status", u64::from(status_code))); if let Some(reason) = status.canonical_reason() { context = context.with(field::str("http.status_reason", reason)); } - context = match status { StatusCode::TOO_MANY_REQUESTS => { retry_after = Some(1); @@ -133,31 +129,25 @@ fn classify_reqwest_error(err: &ReqwestError) -> (Context, Option) { _ => context }; } - if let Some(url) = err.url() { context = context .with(field::str("http.url", url.to_string())) .redact_field("http.url", FieldRedaction::Hash); - if let Some(host) = url.host_str() { context = context.with(field::str("http.host", host.to_owned())); } - if let Some(port) = url.port() { context = context.with(field::u64("http.port", u64::from(port))); } - let path = url.path(); if !path.is_empty() { context = context.with(field::str("http.path", path.to_owned())); } - let scheme = url.scheme(); if !scheme.is_empty() { context = context.with(field::str("http.scheme", scheme.to_owned())); } } - (context, retry_after) } @@ -177,43 +167,35 @@ mod tests { .await .expect("bind listener"); let addr = listener.local_addr().expect("listener addr"); - let server = tokio::spawn(async move { let (_socket, _) = listener.accept().await.expect("accept"); sleep(Duration::from_secs(5)).await; }); - let client = Client::builder() .timeout(Duration::from_millis(50)) .build() .expect("client"); - let err = client .get(format!("http://{addr}")) .send() .await .expect_err("expected timeout"); - let app_err: Error = err.into(); assert_eq!(app_err.kind, AppErrorKind::Timeout); - let metadata = app_err.metadata(); assert_eq!( metadata.get("reqwest.is_timeout"), Some(&FieldValue::Bool(true)) ); assert_eq!(metadata.redaction("http.url"), Some(FieldRedaction::Hash)); - server.abort(); } #[tokio::test] async fn status_error_maps_retry_and_rate_limit() { use tokio::io::{AsyncReadExt, AsyncWriteExt}; - let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind"); let addr = listener.local_addr().expect("addr"); - let server = tokio::spawn(async move { let (mut socket, _) = listener.accept().await.expect("accept"); let mut buf = [0_u8; 1024]; @@ -221,7 +203,6 @@ mod tests { let response = b"HTTP/1.1 429 Too Many Requests\r\ncontent-length: 0\r\n\r\n"; let _ = socket.write_all(response).await; }); - let client = Client::new(); let response = client .get(format!("http://{addr}")) @@ -229,7 +210,6 @@ mod tests { .await .expect("send"); let err = response.error_for_status().expect_err("status error"); - let app_err: Error = err.into(); assert_eq!(app_err.kind, AppErrorKind::RateLimited); assert_eq!(app_err.code, AppCode::RateLimited); @@ -240,17 +220,14 @@ mod tests { metadata.get("http.port"), Some(&FieldValue::U64(u64::from(addr.port()))) ); - server.abort(); } #[tokio::test] async fn server_error_maps_to_dependency_unavailable() { use tokio::io::{AsyncReadExt, AsyncWriteExt}; - let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind"); let addr = listener.local_addr().expect("addr"); - let server = tokio::spawn(async move { let (mut socket, _) = listener.accept().await.expect("accept"); let mut buf = [0_u8; 1024]; @@ -258,7 +235,6 @@ mod tests { let response = b"HTTP/1.1 500 Internal Server Error\r\ncontent-length: 0\r\n\r\n"; let _ = socket.write_all(response).await; }); - let client = Client::new(); let response = client .get(format!("http://{addr}")) @@ -266,22 +242,18 @@ mod tests { .await .expect("send"); let err = response.error_for_status().expect_err("status error"); - let app_err: Error = err.into(); assert_eq!(app_err.kind, AppErrorKind::DependencyUnavailable); let metadata = app_err.metadata(); assert_eq!(metadata.get("http.status"), Some(&FieldValue::U64(500))); - server.abort(); } #[tokio::test] async fn request_timeout_status_maps_to_timeout() { use tokio::io::{AsyncReadExt, AsyncWriteExt}; - let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind"); let addr = listener.local_addr().expect("addr"); - let server = tokio::spawn(async move { let (mut socket, _) = listener.accept().await.expect("accept"); let mut buf = [0_u8; 1024]; @@ -289,7 +261,6 @@ mod tests { let response = b"HTTP/1.1 408 Request Timeout\r\ncontent-length: 0\r\n\r\n"; let _ = socket.write_all(response).await; }); - let client = Client::new(); let response = client .get(format!("http://{addr}")) @@ -297,22 +268,18 @@ mod tests { .await .expect("send"); let err = response.error_for_status().expect_err("status error"); - let app_err: Error = err.into(); assert_eq!(app_err.kind, AppErrorKind::Timeout); let metadata = app_err.metadata(); assert_eq!(metadata.get("http.status"), Some(&FieldValue::U64(408))); - server.abort(); } #[tokio::test] async fn client_error_maps_to_external_api() { use tokio::io::{AsyncReadExt, AsyncWriteExt}; - let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind"); let addr = listener.local_addr().expect("addr"); - let server = tokio::spawn(async move { let (mut socket, _) = listener.accept().await.expect("accept"); let mut buf = [0_u8; 1024]; @@ -320,7 +287,6 @@ mod tests { let response = b"HTTP/1.1 400 Bad Request\r\ncontent-length: 0\r\n\r\n"; let _ = socket.write_all(response).await; }); - let client = Client::new(); let response = client .get(format!("http://{addr}")) @@ -328,12 +294,10 @@ mod tests { .await .expect("send"); let err = response.error_for_status().expect_err("status error"); - let app_err: Error = err.into(); assert_eq!(app_err.kind, AppErrorKind::ExternalApi); let metadata = app_err.metadata(); assert_eq!(metadata.get("http.status"), Some(&FieldValue::U64(400))); - server.abort(); } @@ -345,7 +309,6 @@ mod tests { .send() .await .expect_err("connection refused"); - let app_err: Error = err.into(); assert_eq!(app_err.kind, AppErrorKind::Network); let metadata = app_err.metadata(); @@ -358,10 +321,8 @@ mod tests { #[tokio::test] async fn url_metadata_is_captured() { use tokio::io::{AsyncReadExt, AsyncWriteExt}; - let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind"); let addr = listener.local_addr().expect("addr"); - let server = tokio::spawn(async move { let (mut socket, _) = listener.accept().await.expect("accept"); let mut buf = [0_u8; 1024]; @@ -369,7 +330,6 @@ mod tests { let response = b"HTTP/1.1 404 Not Found\r\ncontent-length: 0\r\n\r\n"; let _ = socket.write_all(response).await; }); - let client = Client::new(); let response = client .get(format!("http://{addr}/api/v1/test")) @@ -377,7 +337,6 @@ mod tests { .await .expect("send"); let err = response.error_for_status().expect_err("status error"); - let app_err: Error = err.into(); let metadata = app_err.metadata(); assert_eq!( @@ -397,17 +356,14 @@ mod tests { Some(&FieldValue::Str("http".into())) ); assert_eq!(metadata.redaction("http.url"), Some(FieldRedaction::Hash)); - server.abort(); } #[tokio::test] async fn status_reason_is_captured() { use tokio::io::{AsyncReadExt, AsyncWriteExt}; - let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind"); let addr = listener.local_addr().expect("addr"); - let server = tokio::spawn(async move { let (mut socket, _) = listener.accept().await.expect("accept"); let mut buf = [0_u8; 1024]; @@ -415,7 +371,6 @@ mod tests { let response = b"HTTP/1.1 403 Forbidden\r\ncontent-length: 0\r\n\r\n"; let _ = socket.write_all(response).await; }); - let client = Client::new(); let response = client .get(format!("http://{addr}")) @@ -423,14 +378,12 @@ mod tests { .await .expect("send"); let err = response.error_for_status().expect_err("status error"); - let app_err: Error = err.into(); let metadata = app_err.metadata(); assert_eq!( metadata.get("http.status_reason"), Some(&FieldValue::Str("Forbidden".into())) ); - server.abort(); } @@ -442,10 +395,8 @@ mod tests { .send() .await .expect_err("connection refused"); - let app_err: Error = err.into(); let metadata = app_err.metadata(); - assert!(metadata.get("reqwest.is_timeout").is_some()); assert!(metadata.get("reqwest.is_connect").is_some()); assert!(metadata.get("reqwest.is_request").is_some()); @@ -458,10 +409,8 @@ mod tests { #[tokio::test] async fn service_unavailable_maps_correctly() { use tokio::io::{AsyncReadExt, AsyncWriteExt}; - let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind"); let addr = listener.local_addr().expect("addr"); - let server = tokio::spawn(async move { let (mut socket, _) = listener.accept().await.expect("accept"); let mut buf = [0_u8; 1024]; @@ -469,7 +418,6 @@ mod tests { let response = b"HTTP/1.1 503 Service Unavailable\r\ncontent-length: 0\r\n\r\n"; let _ = socket.write_all(response).await; }); - let client = Client::new(); let response = client .get(format!("http://{addr}")) @@ -477,10 +425,8 @@ mod tests { .await .expect("send"); let err = response.error_for_status().expect_err("status error"); - let app_err: Error = err.into(); assert_eq!(app_err.kind, AppErrorKind::DependencyUnavailable); - server.abort(); } } diff --git a/src/convert/serde_json.rs b/src/convert/serde_json.rs index 2f00251..c7a0ed6 100644 --- a/src/convert/serde_json.rs +++ b/src/convert/serde_json.rs @@ -66,7 +66,6 @@ fn build_context(err: &SjError) -> Context { } } .with(field::str("serde_json.category", format!("{:?}", category))); - let line = err.line(); if line != 0 { let value = u64::try_from(line).unwrap_or(u64::MAX); @@ -83,7 +82,6 @@ fn build_context(err: &SjError) -> Context { format!("{line}:{column}") )); } - context } @@ -99,17 +97,14 @@ mod tests { #[test] fn io_maps_to_serialization() { struct FailWriter; - impl Write for FailWriter { fn write(&mut self, _buf: &[u8]) -> io::Result { Err(io::Error::other("fail")) } - fn flush(&mut self) -> io::Result<()> { Ok(()) } } - let err = serde_json::to_writer(FailWriter, &json!({"k": "v"})).unwrap_err(); let app: Error = err.into(); assert!(matches!(app.kind, AppErrorKind::Serialization)); diff --git a/src/convert/sqlx.rs b/src/convert/sqlx.rs index 0360830..09ddecb 100644 --- a/src/convert/sqlx.rs +++ b/src/convert/sqlx.rs @@ -198,11 +198,9 @@ fn build_sqlx_context(err: &SqlxError) -> (Context, Option) { None ) }; - if let Some(secs) = retry_after { context = context.with(field::u64("db.retry_after_hint_secs", secs)); } - (context, retry_after) } @@ -211,17 +209,14 @@ fn classify_database_error(error: &(dyn DatabaseError + 'static)) -> (Context, O let mut context = Context::new(AppErrorKind::Database) .with(field::str("db.reason", "database_error")) .with(field::str("db.message", error.message().to_owned())); - if let Some(constraint) = error.constraint() { context = context.with(field::str("db.constraint", constraint.to_owned())); } if let Some(table) = error.table() { context = context.with(field::str("db.table", table.to_owned())); } - let mut retry_after = None; let mut code_override = None; - let code = error.code().map(|code| code.into_owned()); if let Some(ref sqlstate) = code { context = context.with(field::str("db.code", sqlstate.clone())); @@ -238,7 +233,6 @@ fn classify_database_error(error: &(dyn DatabaseError + 'static)) -> (Context, O code_override = Some(app_code.clone()); } } - let category = match error.kind() { SqlxErrorKind::UniqueViolation => AppErrorKind::Conflict, SqlxErrorKind::ForeignKeyViolation => AppErrorKind::Conflict, @@ -247,12 +241,10 @@ fn classify_database_error(error: &(dyn DatabaseError + 'static)) -> (Context, O } _ => AppErrorKind::Database }; - context = context.category(category); if let Some(code) = code_override { context = context.code(code); } - (context, retry_after) } @@ -262,7 +254,6 @@ fn build_migrate_context(err: &MigrateError) -> Context { return Context::new(AppErrorKind::Database) .with(field::str("migration.phase", "invalid_mix")); } - match err { MigrateError::Execute(inner) => Context::new(AppErrorKind::Database) .with(field::str("migration.phase", "execute")) diff --git a/src/convert/teloxide.rs b/src/convert/teloxide.rs index ba25fe3..9fff0dd 100644 --- a/src/convert/teloxide.rs +++ b/src/convert/teloxide.rs @@ -61,11 +61,9 @@ fn build_teloxide_context(err: &RequestError) -> (Context, Option) { "telegram.api_error_variant", format!("{:?}", api) )); - if matches!(api, ApiError::InvalidToken) { context = context.category(AppErrorKind::Unauthorized); } - (context, None) } RequestError::MigrateToChatId(id) => ( diff --git a/src/convert/tokio.rs b/src/convert/tokio.rs index 9d5119f..03713cb 100644 --- a/src/convert/tokio.rs +++ b/src/convert/tokio.rs @@ -150,13 +150,11 @@ mod tests { .await .expect_err("expect timeout"); let app_err1: Error = err1.into(); - let fut2 = sleep(Duration::from_millis(30)); let err2 = timeout(Duration::from_millis(1), fut2) .await .expect_err("expect timeout"); let app_err2: Error = err2.into(); - assert!(matches!(app_err1.kind, AppErrorKind::Timeout)); assert!(matches!(app_err2.kind, AppErrorKind::Timeout)); assert_eq!( diff --git a/src/convert/tonic.rs b/src/convert/tonic.rs index 5993c8e..801fb46 100644 --- a/src/convert/tonic.rs +++ b/src/convert/tonic.rs @@ -67,18 +67,15 @@ impl From for Status { fn status_from_error(error: &Error) -> Status { error.emit_telemetry(); - let mapping = mapping_for_code(&error.code); let grpc_code = Code::from_i32(mapping.grpc().value); let detail = sanitize_detail(error.message.as_ref(), error.kind, error.edit_policy); let mut meta = MetadataMap::new(); - insert_ascii(&mut meta, "app-code", error.code.as_str()); let mut http_status_buffer = IntegerBuffer::new(); let http_status = http_status_buffer.format(mapping.http_status()); insert_ascii(&mut meta, "app-http-status", http_status); insert_ascii(&mut meta, "app-problem-type", mapping.problem_type()); - if let Some(advice) = error.retry { insert_retry(&mut meta, advice); } @@ -87,11 +84,9 @@ fn status_from_error(error: &Error) -> Status { { insert_ascii(&mut meta, "www-authenticate", challenge); } - if !matches!(error.edit_policy, MessageEditPolicy::Redact) { attach_metadata(&mut meta, error.metadata()); } - Status::with_metadata(grpc_code, detail, meta) } @@ -103,7 +98,6 @@ fn sanitize_detail( if matches!(policy, MessageEditPolicy::Redact) { return kind.to_string(); } - message.map_or_else(|| kind.to_string(), |msg| msg.as_ref().to_owned()) } @@ -183,7 +177,6 @@ fn metadata_value_to_ascii<'a>( if !is_ascii_metadata_value(text) { return None; } - match value { Cow::Borrowed(borrowed) => Some(MetadataAscii::Static(borrowed)), Cow::Owned(owned) => Some(MetadataAscii::Owned(owned.clone())) diff --git a/src/convert/validator.rs b/src/convert/validator.rs index a1b1763..601d57c 100644 --- a/src/convert/validator.rs +++ b/src/convert/validator.rs @@ -64,19 +64,16 @@ impl From for Error { #[cfg(feature = "validator")] fn build_context(errors: &ValidationErrors) -> Context { let mut context = Context::new(AppErrorKind::Validation); - let field_errors = errors.field_errors(); if !field_errors.is_empty() { context = context.with(field::u64( "validation.field_count", field_errors.len() as u64 )); - let total: u64 = field_errors.values().map(|errs| errs.len() as u64).sum(); if total > 0 { context = context.with(field::u64("validation.error_count", total)); } - let mut names = String::new(); for (idx, name) in field_errors.keys().take(3).enumerate() { if idx > 0 { @@ -87,7 +84,6 @@ fn build_context(errors: &ValidationErrors) -> Context { if !names.is_empty() { context = context.with(field::str("validation.fields", names)); } - let mut codes: Vec = Vec::new(); for errors in field_errors.values() { for error in *errors { @@ -105,7 +101,6 @@ fn build_context(errors: &ValidationErrors) -> Context { context = context.with(field::str("validation.codes", codes.join(","))); } } - let has_nested = errors .errors() .values() @@ -113,7 +108,6 @@ fn build_context(errors: &ValidationErrors) -> Context { if has_nested { context = context.with(field::bool("validation.has_nested", true)); } - context } diff --git a/src/frontend/browser_console_error.rs b/src/frontend/browser_console_error.rs index 7abfbdc..c3de7e1 100644 --- a/src/frontend/browser_console_error.rs +++ b/src/frontend/browser_console_error.rs @@ -205,7 +205,6 @@ mod tests { }; let err2 = err1.clone(); assert_eq!(err1, err2); - let err3 = BrowserConsoleError::ConsoleMethodNotCallable; let err4 = err3.clone(); assert_eq!(err3, err4); @@ -247,7 +246,6 @@ mod tests { let serialization3 = BrowserConsoleError::Serialization { message: "error2".to_string() }; - assert_eq!(serialization1, serialization2); assert_ne!(serialization1, serialization3); assert_ne!(serialization1, BrowserConsoleError::UnsupportedTarget); diff --git a/src/frontend/browser_console_ext.rs b/src/frontend/browser_console_ext.rs index 3786c04..a95edd8 100644 --- a/src/frontend/browser_console_ext.rs +++ b/src/frontend/browser_console_ext.rs @@ -118,7 +118,6 @@ impl BrowserConsoleExt for ErrorResponse { message: err.to_string() }) } - #[cfg(not(target_arch = "wasm32"))] { Err(BrowserConsoleError::UnsupportedTarget) @@ -133,7 +132,6 @@ impl BrowserConsoleExt for AppError { let response: ErrorResponse = self.into(); response.to_js_value() } - #[cfg(not(target_arch = "wasm32"))] { Err(BrowserConsoleError::UnsupportedTarget) @@ -149,34 +147,28 @@ fn log_js_value(value: &JsValue) -> AppResult<(), BrowserConsoleError> { message: format_js_value(&err) } })?; - if console.is_undefined() || console.is_null() { return Err(BrowserConsoleError::ConsoleUnavailable { message: "console is undefined".into() }); } - let error_fn = Reflect::get(&console, &JsValue::from_str("error")).map_err(|err| { BrowserConsoleError::ConsoleErrorUnavailable { message: format_js_value(&err) } })?; - if error_fn.is_undefined() || error_fn.is_null() { return Err(BrowserConsoleError::ConsoleErrorUnavailable { message: "console.error is undefined".into() }); } - let func = error_fn .dyn_into::() .map_err(|_| BrowserConsoleError::ConsoleMethodNotCallable)?; - func.call1(&console, value) .map_err(|err| BrowserConsoleError::ConsoleInvocation { message: format_js_value(&err) })?; - Ok(()) } diff --git a/src/frontend/tests.rs b/src/frontend/tests.rs index 23f91f0..0ec4f16 100644 --- a/src/frontend/tests.rs +++ b/src/frontend/tests.rs @@ -11,12 +11,10 @@ fn context_returns_optional_message() { message: "encode failed".to_owned() }; assert_eq!(serialization.context(), Some("encode failed")); - let invocation = BrowserConsoleError::ConsoleInvocation { message: "js error".to_owned() }; assert_eq!(invocation.context(), Some("js error")); - assert_eq!( BrowserConsoleError::ConsoleMethodNotCallable.context(), None @@ -108,7 +106,6 @@ fn partial_eq_works() { let err1 = BrowserConsoleError::UnsupportedTarget; let err2 = BrowserConsoleError::UnsupportedTarget; assert_eq!(err1, err2); - let err3 = BrowserConsoleError::ConsoleMethodNotCallable; assert_ne!(err1, err3); } @@ -167,7 +164,6 @@ fn eq_compares_messages() { let err3 = BrowserConsoleError::Serialization { message: "msg2".to_owned() }; - assert_eq!(err1, err2); assert_ne!(err1, err3); } @@ -192,7 +188,6 @@ mod native { response.to_js_value(), Err(BrowserConsoleError::UnsupportedTarget) )); - let err = AppError::conflict("already exists"); assert!(matches!( err.to_js_value(), @@ -228,7 +223,6 @@ mod native { (500, AppCode::Internal, "internal"), (401, AppCode::Unauthorized, "unauthorized"), ]; - for (status, code, message) in errors { let response = ErrorResponse::new(status, code, message).expect("status"); assert!(matches!( @@ -252,7 +246,6 @@ mod native { AppError::timeout("timeout"), AppError::network("network"), ]; - for err in errors { assert!(matches!( err.to_js_value(), diff --git a/src/kind.rs b/src/kind.rs index a7e0b30..821dfc2 100644 --- a/src/kind.rs +++ b/src/kind.rs @@ -197,14 +197,12 @@ impl Display for AppErrorKind { impl Display for AppErrorKind { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { use crate::colored::style; - let label = self.label(); let styled = if self.is_critical() { style::error_kind_critical(label) } else { style::error_kind_warning(label) }; - f.write_str(&styled) } } @@ -249,7 +247,6 @@ impl AppErrorKind { /// library errors). pub fn http_status(&self) -> u16 { match self { - // 4xx — client errors AppErrorKind::NotFound => 404, AppErrorKind::Validation => 422, AppErrorKind::Conflict => 409, @@ -260,11 +257,8 @@ impl AppErrorKind { AppErrorKind::NotImplemented => 501, AppErrorKind::BadRequest => 400, AppErrorKind::RateLimited => 429, - - // 5xx — server/infrastructure errors AppErrorKind::Timeout => 504, AppErrorKind::Network | AppErrorKind::DependencyUnavailable => 503, - AppErrorKind::Serialization | AppErrorKind::Deserialization | AppErrorKind::ExternalApi @@ -362,7 +356,6 @@ mod tests { fn display_colored_contains_label() { let output = Internal.to_string(); assert!(output.contains("Internal server error")); - let output = BadRequest.to_string(); assert!(output.contains("Bad request")); } diff --git a/src/response/actix_impl.rs b/src/response/actix_impl.rs index 2b4485a..8fc6f8d 100644 --- a/src/response/actix_impl.rs +++ b/src/response/actix_impl.rs @@ -39,13 +39,10 @@ pub(crate) fn respond_with_problem_json(mut problem: ProblemJson) -> HttpRespons .unwrap_or(actix_web::http::StatusCode::INTERNAL_SERVER_ERROR); let retry_after = problem.retry_after; let www_authenticate = problem.www_authenticate.take(); - let mut response = HttpResponse::build(status).json(problem); - response .headers_mut() .insert(CONTENT_TYPE, "application/problem+json".parse().unwrap()); - if let Some(retry) = retry_after { let mut buffer = IntegerBuffer::new(); let retry_str = buffer.format(retry); @@ -58,7 +55,6 @@ pub(crate) fn respond_with_problem_json(mut problem: ProblemJson) -> HttpRespons { response.headers_mut().insert(WWW_AUTHENTICATE, hv); } - response } @@ -93,7 +89,6 @@ mod tests { async fn respond_with_problem_json_sets_status_and_content_type() { let problem = ProblemJson::from_app_error(AppError::not_found("missing resource")); let response = respond_with_problem_json(problem); - assert_eq!(response.status(), 404); let content_type = response .headers() @@ -107,7 +102,6 @@ mod tests { let error = AppError::rate_limited("too many requests").with_retry_after_secs(60); let problem = ProblemJson::from_app_error(error); let response = respond_with_problem_json(problem); - assert_eq!(response.status(), 429); let retry = response .headers() @@ -122,7 +116,6 @@ mod tests { AppError::unauthorized("invalid token").with_www_authenticate("Bearer realm=\"api\""); let problem = ProblemJson::from_app_error(error); let response = respond_with_problem_json(problem); - assert_eq!(response.status(), 401); let auth = response .headers() @@ -138,7 +131,6 @@ mod tests { .with_www_authenticate("Bearer"); let problem = ProblemJson::from_app_error(error); let response = respond_with_problem_json(problem); - assert_eq!(response.status(), 429); let retry = response .headers() @@ -157,7 +149,6 @@ mod tests { let req = test::TestRequest::default().to_http_request(); let problem = ProblemJson::from_app_error(AppError::bad_request("invalid input")); let response = problem.respond_to(&req); - assert_eq!(response.status(), 400); let content_type = response .headers() @@ -172,7 +163,6 @@ mod tests { let error_response = ErrorResponse::new(503, AppCode::Service, "service down").expect("valid status"); let response = error_response.respond_to(&req); - assert_eq!(response.status(), 503); let content_type = response .headers() @@ -190,7 +180,6 @@ mod tests { after_seconds: 120 }); let response = error_response.respond_to(&req); - assert_eq!(response.status(), 503); let retry = response .headers() @@ -206,7 +195,6 @@ mod tests { ErrorResponse::new(401, AppCode::Unauthorized, "auth required").expect("valid status"); error_response.www_authenticate = Some("Basic".to_owned()); let response = error_response.respond_to(&req); - assert_eq!(response.status(), 401); let auth = response .headers() diff --git a/src/response/axum_impl.rs b/src/response/axum_impl.rs index 4f3f9f2..b939c22 100644 --- a/src/response/axum_impl.rs +++ b/src/response/axum_impl.rs @@ -31,12 +31,10 @@ impl IntoResponse for ProblemJson { let retry_after = body.retry_after; let www_authenticate = body.www_authenticate.take(); let mut response = (status, Json(body)).into_response(); - response.headers_mut().insert( CONTENT_TYPE, HeaderValue::from_static("application/problem+json") ); - if let Some(retry) = retry_after { let mut buffer = IntegerBuffer::new(); let retry_str = buffer.format(retry); @@ -49,7 +47,6 @@ impl IntoResponse for ProblemJson { { response.headers_mut().insert(WWW_AUTHENTICATE, hv); } - response } } @@ -76,7 +73,6 @@ mod tests { async fn problem_json_into_response_sets_status_and_content_type() { let problem = ProblemJson::from_app_error(AppError::not_found("resource not found")); let response = problem.into_response(); - assert_eq!(response.status(), StatusCode::NOT_FOUND); let content_type = response .headers() @@ -90,7 +86,6 @@ mod tests { let error = AppError::rate_limited("too many requests").with_retry_after_secs(120); let problem = ProblemJson::from_app_error(error); let response = problem.into_response(); - assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS); let retry = response .headers() @@ -105,7 +100,6 @@ mod tests { .with_www_authenticate("Basic realm=\"api\""); let problem = ProblemJson::from_app_error(error); let response = problem.into_response(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); let auth = response .headers() @@ -121,7 +115,6 @@ mod tests { .with_www_authenticate("Bearer"); let problem = ProblemJson::from_app_error(error); let response = problem.into_response(); - assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS); let retry = response .headers() @@ -140,7 +133,6 @@ mod tests { let error_response = ErrorResponse::new(500, AppCode::Internal, "internal error").expect("valid status"); let response = error_response.into_response(); - assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); let content_type = response .headers() @@ -157,7 +149,6 @@ mod tests { after_seconds: 300 }); let response = error_response.into_response(); - assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); let retry = response .headers() @@ -172,7 +163,6 @@ mod tests { ErrorResponse::new(401, AppCode::Unauthorized, "auth required").expect("valid status"); error_response.www_authenticate = Some("Digest realm=\"api\"".to_owned()); let response = error_response.into_response(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); let auth = response .headers() @@ -192,7 +182,6 @@ mod tests { StatusCode::INTERNAL_SERVER_ERROR ), ]; - for (error, expected_status) in test_cases { let problem = ProblemJson::from_app_error(error); let response = problem.into_response(); diff --git a/src/response/core.rs b/src/response/core.rs index a49f0e3..f605458 100644 --- a/src/response/core.rs +++ b/src/response/core.rs @@ -136,7 +136,6 @@ mod tests { fn new_accepts_string_types() { let owned = ErrorResponse::new(200, AppCode::Internal, String::from("owned")); assert!(owned.is_ok()); - let borrowed = ErrorResponse::new(201, AppCode::Internal, "borrowed"); assert!(borrowed.is_ok()); } @@ -235,7 +234,6 @@ mod tests { (503, StatusCode::SERVICE_UNAVAILABLE), (504, StatusCode::GATEWAY_TIMEOUT), ]; - for (num, expected) in codes { let resp = ErrorResponse::new(num, AppCode::Internal, "test").unwrap(); assert_eq!(resp.status_code(), expected); diff --git a/src/response/details.rs b/src/response/details.rs index 46d22a6..8a2647d 100644 --- a/src/response/details.rs +++ b/src/response/details.rs @@ -110,13 +110,11 @@ mod tests { #[test] fn with_details_serializes_struct() { use serde::Serialize; - #[derive(Serialize)] struct ErrorInfo { field: String, code: u32 } - let info = ErrorInfo { field: "username".to_owned(), code: 1001 @@ -125,7 +123,6 @@ mod tests { .unwrap() .with_details(info) .unwrap(); - assert!(resp.details.is_some()); let details = resp.details.unwrap(); assert_eq!(details["field"], "username"); @@ -136,13 +133,10 @@ mod tests { #[test] fn with_details_serializes_nan_as_null() { use serde::Serialize; - - // f64::NAN serializes to JSON null #[derive(Serialize)] struct DataWithNaN { value: f64 } - let data = DataWithNaN { value: f64::NAN }; @@ -150,8 +144,6 @@ mod tests { .unwrap() .with_details(data) .unwrap(); - - // NaN becomes null in JSON assert!(resp.details.is_some()); let details = resp.details.unwrap(); assert!(details["value"].is_null()); @@ -161,23 +153,19 @@ mod tests { #[test] fn with_details_preserves_other_fields() { use serde::Serialize; - #[derive(Serialize)] struct Extra { info: String } - let mut resp = ErrorResponse::new(429, AppCode::RateLimited, "too many").unwrap(); resp.retry = Some(crate::response::core::RetryAdvice { after_seconds: 60 }); - let resp = resp .with_details(Extra { info: "try later".to_owned() }) .unwrap(); - assert!(resp.details.is_some()); assert!(resp.retry.is_some()); assert_eq!(resp.status, 429); @@ -190,7 +178,6 @@ mod tests { let resp = ErrorResponse::new(404, AppCode::NotFound, "missing") .unwrap() .with_details_text("resource not found in database"); - assert_eq!(resp.status, 404); assert_eq!(resp.message, "missing"); assert!(resp.details.is_some()); diff --git a/src/response/internal.rs b/src/response/internal.rs index a67ec70..8d390a7 100644 --- a/src/response/internal.rs +++ b/src/response/internal.rs @@ -89,9 +89,7 @@ mod tests { after_seconds: 30 }); resp.www_authenticate = Some("Bearer".to_owned()); - let formatted = format!("{:?}", resp.internal()); - assert!(formatted.contains("ErrorResponse")); assert!(formatted.contains("status")); assert!(formatted.contains("404")); @@ -109,7 +107,6 @@ mod tests { fn error_response_formatter_debug_shows_none_for_optional_fields() { let resp = ErrorResponse::new(500, AppCode::Internal, "error").unwrap(); let formatted = format!("{:?}", resp.internal()); - assert!(formatted.contains("details: None")); assert!(formatted.contains("retry: None")); assert!(formatted.contains("www_authenticate: None")); @@ -119,10 +116,8 @@ mod tests { fn error_response_formatter_display_delegates_to_inner() { let resp = ErrorResponse::new(400, AppCode::BadRequest, "invalid input").unwrap(); let formatter = resp.internal(); - let display_str = format!("{}", formatter); let inner_display_str = format!("{}", resp); - assert_eq!(display_str, inner_display_str); } @@ -131,8 +126,6 @@ mod tests { let resp = ErrorResponse::new(500, AppCode::Internal, "error").unwrap(); let formatter1 = resp.internal(); let formatter2 = formatter1; - - // Both should work let _ = format!("{:?}", formatter1); let _ = format!("{:?}", formatter2); } @@ -142,10 +135,8 @@ mod tests { let error = AppError::not_found("missing resource") .with_retry_after_secs(60) .with_www_authenticate("Bearer realm=\"api\""); - let problem = ProblemJson::from_app_error(error); let formatted = format!("{:?}", problem.internal()); - assert!(formatted.contains("ProblemJson")); assert!(formatted.contains("type")); assert!(formatted.contains("title")); @@ -161,9 +152,7 @@ mod tests { fn problem_json_formatter_display_shows_status_code_detail() { let error = AppError::bad_request("validation failed"); let problem = ProblemJson::from_app_error(error); - let display_str = format!("{}", problem.internal()); - assert!(display_str.contains("400")); assert!(display_str.contains("BAD_REQUEST")); assert!(display_str.contains("validation failed")); @@ -173,10 +162,7 @@ mod tests { fn problem_json_formatter_display_format_matches_pattern() { let error = AppError::internal("server error"); let problem = ProblemJson::from_app_error(error); - let display_str = format!("{}", problem.internal()); - - // Format: "{status} {code}: {detail:?}" assert!(display_str.starts_with("500")); assert!(display_str.contains("INTERNAL")); assert!(display_str.contains(": ")); @@ -187,11 +173,8 @@ mod tests { fn problem_json_formatter_is_copy() { let error = AppError::internal("error"); let problem = ProblemJson::from_app_error(error); - let formatter1 = problem.internal(); let formatter2 = formatter1; - - // Both should work let _ = format!("{:?}", formatter1); let _ = format!("{:?}", formatter2); } @@ -208,11 +191,9 @@ mod tests { "RATE_LIMITED" ), ]; - for (error, expected_status, expected_code) in test_cases { let problem = ProblemJson::from_app_error(error); let display = format!("{}", problem.internal()); - assert!(display.contains(&expected_status.to_string())); assert!(display.contains(expected_code)); } diff --git a/src/response/legacy.rs b/src/response/legacy.rs index b82ec63..18406b7 100644 --- a/src/response/legacy.rs +++ b/src/response/legacy.rs @@ -142,7 +142,6 @@ mod tests { (502, "Bad Gateway"), (503, "Service Unavailable"), ]; - for (status, message) in test_cases { let resp = ErrorResponse::new_legacy(status, message); assert_eq!(resp.status, status); @@ -154,11 +153,8 @@ mod tests { #[test] #[allow(deprecated)] fn new_legacy_handles_edge_case_status_codes() { - // Minimum valid HTTP status let resp = ErrorResponse::new_legacy(100, "continue"); assert_eq!(resp.status, 100); - - // Maximum valid HTTP status let resp = ErrorResponse::new_legacy(599, "custom"); assert_eq!(resp.status, 599); } diff --git a/src/response/mapping.rs b/src/response/mapping.rs index fc9abbc..ca52547 100644 --- a/src/response/mapping.rs +++ b/src/response/mapping.rs @@ -36,7 +36,10 @@ //! [`MessageEditPolicy`]: crate::MessageEditPolicy use alloc::string::String; -use core::fmt::{Display, Formatter, Result as FmtResult}; +use core::{ + fmt::{Display, Formatter, Result as FmtResult}, + mem::replace +}; use super::core::ErrorResponse; use crate::{AppCode, AppError}; @@ -84,11 +87,10 @@ impl Display for ErrorResponse { impl From for ErrorResponse { fn from(mut err: AppError) -> Self { let kind = err.kind; - let code = core::mem::replace(&mut err.code, AppCode::from(kind)); + let code = replace(&mut err.code, AppCode::from(kind)); let retry = err.retry.take(); let www_authenticate = err.www_authenticate.take(); let policy = err.edit_policy; - let status = kind.http_status(); let message = match err.message.take() { Some(msg) if !matches!(policy, crate::MessageEditPolicy::Redact) => msg.into_owned(), @@ -106,7 +108,6 @@ impl From for ErrorResponse { } else { err.details.take() }; - Self { status, code, @@ -157,7 +158,6 @@ impl From<&AppError> for ErrorResponse { } else { err.details.clone() }; - Self { status, code: err.code.clone(), diff --git a/src/response/problem_json.rs b/src/response/problem_json.rs index 7cc5750..b603d72 100644 --- a/src/response/problem_json.rs +++ b/src/response/problem_json.rs @@ -7,7 +7,13 @@ use alloc::{ collections::BTreeMap, string::{String, ToString} }; -use core::{fmt::Write, net::IpAddr}; +use core::{ + fmt::Write, + iter::repeat_n, + mem::{replace, take}, + net::IpAddr, + str::from_utf8 +}; use http::StatusCode; use itoa::Buffer as IntegerBuffer; @@ -165,22 +171,19 @@ impl ProblemJson { #[must_use] pub fn from_app_error(mut error: AppError) -> Self { error.emit_telemetry(); - let kind = error.kind; - let code = core::mem::replace(&mut error.code, AppCode::from(kind)); + let code = replace(&mut error.code, AppCode::from(kind)); let message = error.message.take(); - let metadata = core::mem::take(&mut error.metadata); + let metadata = take(&mut error.metadata); let edit_policy = error.edit_policy; let details = sanitize_details_owned(error.details.take(), edit_policy); let retry = error.retry.take(); let www_authenticate = error.www_authenticate.take(); - let mapping = mapping_for_code(&code); let status = kind.http_status(); let title = Cow::Borrowed(kind.label()); let detail = sanitize_detail(message, kind, edit_policy); let metadata = sanitize_metadata_owned(metadata, edit_policy); - Self { type_uri: Some(Cow::Borrowed(mapping.problem_type())), title, @@ -218,7 +221,6 @@ impl ProblemJson { let detail = sanitize_detail_ref(error); let details = sanitize_details_ref(error); let metadata = sanitize_metadata_ref(error.metadata(), error.edit_policy); - Self { type_uri: Some(Cow::Borrowed(mapping.problem_type())), title, @@ -257,14 +259,12 @@ impl ProblemJson { retry, www_authenticate } = response; - let mapping = mapping_for_code(&code); let detail = if message.is_empty() { None } else { Some(Cow::Owned(message)) }; - Self { type_uri: Some(Cow::Borrowed(mapping.problem_type())), title: Cow::Borrowed(mapping.kind().label()), @@ -415,7 +415,6 @@ fn sanitize_detail( if matches!(policy, MessageEditPolicy::Redact) { return None; } - Some(message.unwrap_or_else(|| Cow::Borrowed(kind.label()))) } @@ -423,7 +422,6 @@ fn sanitize_detail_ref(error: &AppError) -> Option> { if matches!(error.edit_policy, MessageEditPolicy::Redact) { return None; } - match error.message.as_ref() { Some(Cow::Borrowed(msg)) => Some(Cow::Borrowed(*msg)), Some(Cow::Owned(msg)) => Some(Cow::Owned(msg.clone())), @@ -477,7 +475,6 @@ fn sanitize_metadata_owned( if matches!(policy, MessageEditPolicy::Redact) || metadata.is_empty() { return None; } - let mut public = BTreeMap::new(); for field in metadata { let (name, value, redaction) = field.into_parts(); @@ -485,7 +482,6 @@ fn sanitize_metadata_owned( public.insert(Cow::Borrowed(name), sanitized); } } - if public.is_empty() { None } else { @@ -500,14 +496,12 @@ fn sanitize_metadata_ref( if matches!(policy, MessageEditPolicy::Redact) || metadata.is_empty() { return None; } - let mut public = BTreeMap::new(); for (name, value, redaction) in metadata.iter_with_redaction() { if let Some(sanitized) = sanitize_problem_metadata_value_ref(value, redaction) { public.insert(Cow::Borrowed(name), sanitized); } } - if public.is_empty() { None } else { @@ -569,7 +563,7 @@ impl StackBuffer { } fn as_str(&self) -> Option<&str> { - core::str::from_utf8(self.as_bytes()).ok() + from_utf8(self.as_bytes()).ok() } } @@ -606,8 +600,6 @@ fn hash_field_value(value: &FieldValue) -> String { } } FieldValue::Uuid(value) => { - // `Uuid::to_string()` produces a lowercase hyphenated representation; we - // keep the same bytes to preserve the hash output that clients rely on. let mut repr = [0u8; 36]; let text = value.hyphenated().encode_lower(&mut repr); hasher.update(text.as_bytes()); @@ -682,11 +674,10 @@ fn mask_last4(value: &str) -> String { if total == 0 { return String::new(); } - let keep = if total <= 4 { 1 } else { 4 }; let mask_len = total.saturating_sub(keep); let mut masked = String::with_capacity(value.len()); - masked.extend(core::iter::repeat_n('*', mask_len)); + masked.extend(repeat_n('*', mask_len)); masked.extend(chars.skip(mask_len)); masked } @@ -1042,7 +1033,12 @@ mod tests { use uuid::Uuid; use super::*; - use crate::AppError; + #[cfg(feature = "serde_json")] + use crate::field::json; + use crate::{ + AppError, + field::{duration, f64, ip, str, u64, uuid} + }; fn sha256_hex(input: &[u8]) -> String { let mut hasher = Sha256::new(); @@ -1060,7 +1056,7 @@ mod tests { fn metadata_is_skipped_when_redacted() { let err = AppError::internal("secret") .redactable() - .with_field(crate::field::str("token", "super-secret")); + .with_field(str("token", "super-secret")); let problem = ProblemJson::from_ref(&err); assert!(problem.detail.is_none()); assert!(problem.metadata.is_none()); @@ -1068,7 +1064,7 @@ mod tests { #[test] fn metadata_is_serialized_when_allowed() { - let err = AppError::internal("oops").with_field(crate::field::u64("attempt", 2)); + let err = AppError::internal("oops").with_field(u64("attempt", 2)); let problem = ProblemJson::from_ref(&err); let metadata = problem.metadata.expect("metadata"); assert!(!metadata.is_empty()); @@ -1077,42 +1073,28 @@ mod tests { #[test] fn metadata_preserves_extended_field_types() { let mut err = AppError::internal("oops"); - err = err.with_field(crate::field::f64("ratio", 0.25)); - err = err.with_field(crate::field::duration( - "elapsed", - Duration::from_millis(1500) - )); - err = err.with_field(crate::field::ip( - "peer", - IpAddr::from(Ipv4Addr::new(10, 0, 0, 42)) - )); + err = err.with_field(f64("ratio", 0.25)); + err = err.with_field(duration("elapsed", Duration::from_millis(1500))); + err = err.with_field(ip("peer", IpAddr::from(Ipv4Addr::new(10, 0, 0, 42)))); #[cfg(feature = "serde_json")] { - err = err.with_field(crate::field::json( - "payload", - serde_json::json!({ "status": "ok" }) - )); + err = err.with_field(json("payload", serde_json::json!({ "status": "ok" }))); } - let problem = ProblemJson::from_ref(&err); let metadata = problem.metadata.expect("metadata"); - let ratio = metadata.0.get("ratio").expect("ratio metadata"); assert!(matches!( ratio, ProblemMetadataValue::F64(value) if (*value - 0.25).abs() < f64::EPSILON )); - let duration = metadata.0.get("elapsed").expect("elapsed metadata"); assert!(matches!( duration, ProblemMetadataValue::Duration { secs, nanos } if *secs == 1 && *nanos == 500_000_000 )); - let ip = metadata.0.get("peer").expect("peer metadata"); assert!(matches!(ip, ProblemMetadataValue::Ip(addr) if addr.is_ipv4())); - #[cfg(feature = "serde_json")] { let payload = metadata.0.get("payload").expect("payload metadata"); @@ -1125,7 +1107,7 @@ mod tests { #[test] fn redacted_metadata_uses_placeholder() { - let err = AppError::internal("oops").with_field(crate::field::str("password", "secret")); + let err = AppError::internal("oops").with_field(str("password", "secret")); let problem = ProblemJson::from_ref(&err); let metadata = problem.metadata.expect("metadata"); let value = metadata.0.get("password").expect("password field"); @@ -1139,7 +1121,7 @@ mod tests { #[test] fn hashed_metadata_masks_original_value() { - let err = AppError::internal("oops").with_field(crate::field::str("token", "super")); + let err = AppError::internal("oops").with_field(str("token", "super")); let problem = ProblemJson::from_ref(&err); let metadata = problem.metadata.expect("metadata"); let value = metadata.0.get("token").expect("token field"); @@ -1155,7 +1137,7 @@ mod tests { #[test] fn hashed_numeric_metadata_uses_decimal_text() { let err = AppError::internal("oops") - .with_field(crate::field::u64("attempt", 42).with_redaction(FieldRedaction::Hash)); + .with_field(u64("attempt", 42).with_redaction(FieldRedaction::Hash)); let problem = ProblemJson::from_ref(&err); let metadata = problem.metadata.expect("metadata"); let value = metadata.0.get("attempt").expect("attempt field"); @@ -1170,16 +1152,16 @@ mod tests { #[test] fn hashed_uuid_metadata_preserves_hyphenated_text() { - let uuid = Uuid::from_u128(0x1234_5678_9abc_def0_1234_5678_9abc_def0); + let trace_id = Uuid::from_u128(0x1234_5678_9abc_def0_1234_5678_9abc_def0); let err = AppError::internal("oops") - .with_field(crate::field::uuid("trace", uuid).with_redaction(FieldRedaction::Hash)); + .with_field(uuid("trace", trace_id).with_redaction(FieldRedaction::Hash)); let problem = ProblemJson::from_ref(&err); let metadata = problem.metadata.expect("metadata"); let value = metadata.0.get("trace").expect("trace field"); match value { ProblemMetadataValue::String(text) => { let mut repr = [0u8; 36]; - let expected_repr = uuid.hyphenated().encode_lower(&mut repr); + let expected_repr = trace_id.hyphenated().encode_lower(&mut repr); let expected = sha256_hex(expected_repr.as_bytes()); assert_eq!(text.as_ref(), expected); } @@ -1189,15 +1171,15 @@ mod tests { #[test] fn hashed_ip_metadata_preserves_display_text() { - let ip = IpAddr::from(Ipv4Addr::new(10, 10, 10, 10)); + let peer_addr = IpAddr::from(Ipv4Addr::new(10, 10, 10, 10)); let err = AppError::internal("oops") - .with_field(crate::field::ip("peer", ip).with_redaction(FieldRedaction::Hash)); + .with_field(ip("peer", peer_addr).with_redaction(FieldRedaction::Hash)); let problem = ProblemJson::from_ref(&err); let metadata = problem.metadata.expect("metadata"); let value = metadata.0.get("peer").expect("peer field"); match value { ProblemMetadataValue::String(text) => { - let expected = sha256_hex(ip.to_string().as_bytes()); + let expected = sha256_hex(peer_addr.to_string().as_bytes()); assert_eq!(text.as_ref(), expected); } other => panic!("unexpected metadata value: {other:?}") @@ -1206,8 +1188,7 @@ mod tests { #[test] fn last4_metadata_preserves_suffix() { - let err = AppError::internal("oops") - .with_field(crate::field::str("card_number", "4111111111111111")); + let err = AppError::internal("oops").with_field(str("card_number", "4111111111111111")); let problem = ProblemJson::from_ref(&err); let metadata = problem.metadata.expect("metadata"); let value = metadata.0.get("card_number").expect("card number"); @@ -1223,9 +1204,8 @@ mod tests { #[test] fn last4_metadata_handles_multibyte_suffix() { let multibyte = "💳💳💳💳💳💳"; - let err = AppError::internal("oops").with_field( - crate::field::str("emoji", multibyte).with_redaction(FieldRedaction::Last4) - ); + let err = AppError::internal("oops") + .with_field(str("emoji", multibyte).with_redaction(FieldRedaction::Last4)); let problem = ProblemJson::from_ref(&err); let metadata = problem.metadata.expect("metadata"); let value = metadata.0.get("emoji").expect("emoji field"); @@ -1235,7 +1215,6 @@ mod tests { let keep = if total <= 4 { 1 } else { 4 }; let expected_mask_len = total.saturating_sub(keep); let expected_suffix: String = multibyte.chars().skip(expected_mask_len).collect(); - assert!(text.ends_with(&expected_suffix)); assert!(text.chars().take(expected_mask_len).all(|c| c == '*')); assert_eq!( @@ -1251,9 +1230,8 @@ mod tests { #[test] fn last4_numeric_metadata_matches_decimal_format() { let number = 123456789u64; - let err = AppError::internal("oops").with_field( - crate::field::u64("invoice", number).with_redaction(FieldRedaction::Last4) - ); + let err = AppError::internal("oops") + .with_field(u64("invoice", number).with_redaction(FieldRedaction::Last4)); let problem = ProblemJson::from_ref(&err); let metadata = problem.metadata.expect("metadata"); let metadata_value = metadata.0.get("invoice").expect("invoice field"); @@ -1268,13 +1246,13 @@ mod tests { #[test] fn last4_uuid_metadata_matches_previous_format() { - let uuid = Uuid::from_u128(0x4321_8765_cba9_0fed_cba9_8765_4321_0fed); + let trace_id = Uuid::from_u128(0x4321_8765_cba9_0fed_cba9_8765_4321_0fed); let err = AppError::internal("oops") - .with_field(crate::field::uuid("trace", uuid).with_redaction(FieldRedaction::Last4)); + .with_field(uuid("trace", trace_id).with_redaction(FieldRedaction::Last4)); let problem = ProblemJson::from_ref(&err); let metadata = problem.metadata.expect("metadata"); let metadata_value = metadata.0.get("trace").expect("trace field"); - let expected_repr = uuid.to_string(); + let expected_repr = trace_id.to_string(); let expected_suffix = mask_last4(&expected_repr); match metadata_value { ProblemMetadataValue::String(text) => { @@ -1286,13 +1264,13 @@ mod tests { #[test] fn last4_ip_metadata_matches_previous_format() { - let ip = IpAddr::from(Ipv4Addr::new(172, 16, 10, 1)); + let peer_addr = IpAddr::from(Ipv4Addr::new(172, 16, 10, 1)); let err = AppError::internal("oops") - .with_field(crate::field::ip("peer", ip).with_redaction(FieldRedaction::Last4)); + .with_field(ip("peer", peer_addr).with_redaction(FieldRedaction::Last4)); let problem = ProblemJson::from_ref(&err); let metadata = problem.metadata.expect("metadata"); let metadata_value = metadata.0.get("peer").expect("peer field"); - let expected_suffix = mask_last4(&ip.to_string()); + let expected_suffix = mask_last4(&peer_addr.to_string()); match metadata_value { ProblemMetadataValue::String(text) => { assert_eq!(text.as_ref(), expected_suffix); @@ -1304,10 +1282,9 @@ mod tests { #[test] fn problem_json_serialization_masks_sensitive_metadata() { let secret = "super-secret"; - let err = AppError::internal("oops").with_field(crate::field::str("token", secret)); + let err = AppError::internal("oops").with_field(str("token", secret)); let problem = ProblemJson::from_ref(&err); let json = serde_json::to_value(&problem).expect("serialize problem"); - let metadata = json .get("metadata") .and_then(Value::as_object) @@ -1316,7 +1293,6 @@ mod tests { .get("token") .and_then(Value::as_str) .expect("hashed token"); - let mut hasher = Sha256::new(); hasher.update(secret.as_bytes()); let digest = hasher.finalize(); @@ -1326,10 +1302,8 @@ mod tests { let _ = write!(&mut acc, "{:02x}", byte); acc }); - assert_eq!(hashed, expected); assert!(!json.to_string().contains(secret)); - let debug_repr = format!("{:?}", problem.internal()); assert!(debug_repr.contains("metadata")); assert!(!debug_repr.contains(secret)); @@ -1340,13 +1314,11 @@ mod tests { let secret_value = "sensitive-value"; let err = AppError::internal("secret") .redactable() - .with_field(crate::field::str("token", secret_value)); + .with_field(str("token", secret_value)); let problem = ProblemJson::from_ref(&err); let json = serde_json::to_value(&problem).expect("serialize problem"); - assert!(json.get("metadata").is_none()); assert!(!json.to_string().contains(secret_value)); - let debug_repr = format!("{:?}", problem.internal()); assert!(debug_repr.contains("ProblemJson")); } diff --git a/src/response/tests.rs b/src/response/tests.rs index 5e6671e..94d00f9 100644 --- a/src/response/tests.rs +++ b/src/response/tests.rs @@ -39,7 +39,6 @@ fn with_retry_and_www_authenticate_attach_metadata() { #[test] fn with_retry_after_duration_attaches_advice() { use std::time::Duration; - let e = ErrorResponse::new(429, AppCode::RateLimited, "slow down") .expect("status") .with_retry_after_duration(Duration::from_secs(42)); @@ -65,7 +64,6 @@ fn with_retry_after_secs_large_value() { #[test] fn with_retry_after_duration_zero() { use std::time::Duration; - let e = ErrorResponse::new(503, AppCode::Internal, "unavailable") .expect("status") .with_retry_after_duration(Duration::from_secs(0)); @@ -75,7 +73,6 @@ fn with_retry_after_duration_zero() { #[test] fn with_retry_after_duration_subsecond_rounds_down() { use std::time::Duration; - let e = ErrorResponse::new(503, AppCode::Internal, "unavailable") .expect("status") .with_retry_after_duration(Duration::from_millis(999)); @@ -126,13 +123,11 @@ fn with_www_authenticate_special_characters() { #[test] fn metadata_methods_are_chainable() { use std::time::Duration; - let e = ErrorResponse::new(503, AppCode::Internal, "unavailable") .expect("status") .with_retry_after_duration(Duration::from_secs(30)) .with_www_authenticate("Bearer") .with_retry_after_secs(60); - assert_eq!(e.retry.unwrap().after_seconds, 60); assert_eq!(e.www_authenticate.as_deref(), Some("Bearer")); } @@ -158,10 +153,8 @@ fn with_www_authenticate_overwrites_previous() { #[test] fn status_code_maps_invalid_to_internal_server_error() { use http::StatusCode; - let valid = ErrorResponse::new(404, AppCode::NotFound, "missing").expect("status"); assert_eq!(valid.status_code(), StatusCode::NOT_FOUND); - let invalid = ErrorResponse { status: 1000, code: AppCode::Internal, @@ -192,10 +185,8 @@ fn details_json_are_attached() { fn custom_codes_roundtrip_via_json() { let custom = AppCode::new("INVALID_JSON"); let response = ErrorResponse::new(400, custom.clone(), "invalid body").expect("status"); - let json = serde_json::to_string(&response).expect("serialize"); let decoded: ErrorResponse = serde_json::from_str(&json).expect("decode"); - assert_eq!(decoded.code, custom); assert_eq!(decoded.code.as_str(), "INVALID_JSON"); } @@ -205,19 +196,16 @@ fn custom_codes_roundtrip_via_json() { fn with_details_serializes_custom_struct() { use serde::Serialize; use serde_json::json; - #[derive(Serialize)] struct Extra { value: i32 } - let resp = ErrorResponse::new(400, AppCode::BadRequest, "bad") .expect("status") .with_details(Extra { value: 7 }) .expect("details"); - assert_eq!(resp.details.unwrap(), json!({"value": 7})); } @@ -225,9 +213,7 @@ fn with_details_serializes_custom_struct() { #[test] fn with_details_propagates_serialization_errors() { use serde::{Serialize, Serializer}; - struct Failing; - impl Serialize for Failing { fn serialize(&self, _: S) -> Result where @@ -236,7 +222,6 @@ fn with_details_propagates_serialization_errors() { Err(serde::ser::Error::custom("nope")) } } - let err = ErrorResponse::new(400, AppCode::BadRequest, "bad") .expect("status") .with_details(Failing) @@ -258,23 +243,18 @@ fn details_text_are_attached() { #[test] fn app_error_mappings_propagate_json_details() { use serde_json::json; - let payload = json!({"hint": "enable"}); - let resp: ErrorResponse = AppError::validation("invalid") .with_details_json(payload.clone()) .into(); assert_eq!(resp.details, Some(payload.clone())); - let borrowed = AppError::validation("invalid").with_details_json(payload.clone()); let resp_ref: ErrorResponse = (&borrowed).into(); assert_eq!(resp_ref.details, Some(payload.clone())); - let problem_owned = ProblemJson::from_app_error( AppError::validation("invalid").with_details_json(payload.clone()) ); assert_eq!(problem_owned.details, Some(payload.clone())); - let problem_ref = ProblemJson::from_ref(&borrowed); assert_eq!(problem_ref.details, Some(payload)); } @@ -283,13 +263,11 @@ fn app_error_mappings_propagate_json_details() { #[test] fn redacted_app_error_strips_json_details() { use serde_json::json; - let resp: ErrorResponse = AppError::internal("boom") .with_details_json(json!({"private": true})) .redactable() .into(); assert!(resp.details.is_none()); - let borrowed = AppError::internal("boom") .with_details_json(json!({"private": true})) .redactable(); @@ -297,7 +275,6 @@ fn redacted_app_error_strips_json_details() { assert!(resp_ref.details.is_none()); let problem = ProblemJson::from_ref(&borrowed); assert!(problem.details.is_none()); - let owned_problem = ProblemJson::from_app_error( AppError::internal("boom") .with_details_json(json!({"private": true})) @@ -313,16 +290,13 @@ fn app_error_mappings_propagate_text_details() { .with_details_text("enable feature") .into(); assert_eq!(resp.details.as_deref(), Some("enable feature")); - let borrowed = AppError::validation("invalid").with_details_text("enable feature"); let resp_ref: ErrorResponse = (&borrowed).into(); assert_eq!(resp_ref.details.as_deref(), Some("enable feature")); - let problem_owned = ProblemJson::from_app_error( AppError::validation("invalid").with_details_text("enable feature") ); assert_eq!(problem_owned.details.as_deref(), Some("enable feature")); - let problem_ref = ProblemJson::from_ref(&borrowed); assert_eq!(problem_ref.details.as_deref(), Some("enable feature")); } @@ -335,7 +309,6 @@ fn redacted_app_error_strips_text_details() { .redactable() .into(); assert!(resp.details.is_none()); - let borrowed = AppError::internal("boom") .with_details_text("private") .redactable(); @@ -343,7 +316,6 @@ fn redacted_app_error_strips_text_details() { assert!(resp_ref.details.is_none()); let problem = ProblemJson::from_ref(&borrowed); assert!(problem.details.is_none()); - let owned_problem = ProblemJson::from_app_error( AppError::internal("boom") .with_details_text("private") @@ -378,9 +350,7 @@ fn from_owned_app_error_moves_message_and_metadata() { let err = AppError::unauthorized(String::from("owned message")) .with_retry_after_secs(5) .with_www_authenticate("Bearer"); - let resp: ErrorResponse = err.into(); - assert_eq!(resp.status, 401); assert_eq!(resp.code, AppCode::Unauthorized); assert_eq!(resp.message, "owned message"); @@ -391,7 +361,6 @@ fn from_owned_app_error_moves_message_and_metadata() { #[test] fn from_owned_app_error_defaults_message_when_absent() { let resp: ErrorResponse = AppError::bare(AppErrorKind::Internal).into(); - assert_eq!(resp.status, 500); assert_eq!(resp.code, AppCode::Internal); assert_eq!(resp.message, AppErrorKind::Internal.label()); @@ -401,7 +370,6 @@ fn from_owned_app_error_defaults_message_when_absent() { fn from_app_error_bare_uses_kind_display_as_message() { let app = AppError::bare(AppErrorKind::Timeout); let resp: ErrorResponse = app.into(); - assert_eq!(resp.status, 504); assert_eq!(resp.code, AppCode::Timeout); assert_eq!(resp.message, AppErrorKind::Timeout.label()); @@ -418,7 +386,6 @@ fn problem_json_fallbacks_borrow_bare_labels() { owned.detail, Some(Cow::Borrowed(label)) if label == AppErrorKind::Internal.label() )); - let borrowed_error = AppError::bare(AppErrorKind::Timeout); let borrowed_problem = ProblemJson::from_ref(&borrowed_error); assert!(matches!( @@ -435,9 +402,7 @@ fn problem_json_fallbacks_borrow_bare_labels() { fn from_app_error_redacts_message_when_policy_allows() { let app = AppError::internal("sensitive").redactable(); let resp: ErrorResponse = app.into(); - assert_eq!(resp.message, AppErrorKind::Internal.label()); - let borrowed = AppError::internal("private").redactable(); let resp_ref: ErrorResponse = (&borrowed).into(); assert_eq!(resp_ref.message, AppErrorKind::Internal.label()); @@ -448,7 +413,6 @@ fn error_response_serialization_hides_redacted_message() { let secret = "super-secret"; let resp: ErrorResponse = AppError::internal(secret).redactable().into(); let json = serde_json::to_value(&resp).expect("serialize response"); - let fallback = AppErrorKind::Internal.label(); assert_eq!( json.get("message").and_then(|value| value.as_str()), @@ -502,13 +466,11 @@ fn axum_into_response_sets_headers_and_status() { http::header::{RETRY_AFTER, WWW_AUTHENTICATE}, response::IntoResponse }; - let resp = ErrorResponse::new(401, AppCode::Unauthorized, "no token") .expect("status") .with_retry_after_secs(7) .with_www_authenticate(r#"Bearer realm="api", error="invalid_token""#) .into_response(); - assert_eq!(resp.status(), 401); let headers = resp.headers(); let retry_after = headers.get(RETRY_AFTER).expect("Retry-After"); @@ -535,20 +497,13 @@ fn actix_responder_sets_headers_and_status() { }, test::TestRequest }; - - // Build ErrorResponse with both headers let resp = ErrorResponse::new(429, AppCode::RateLimited, "slow down") .expect("status") .with_retry_after_secs(42) .with_www_authenticate("Bearer"); - - // Build a minimal HttpRequest for Responder::respond_to let req = TestRequest::default().to_http_request(); - - // `respond_to` builds HttpResponse synchronously; we can inspect it. let http = resp.respond_to(&req); assert_eq!(http.status(), StatusCode::TOO_MANY_REQUESTS); - let headers = http.headers(); let retry_after = headers.get(RETRY_AFTER).expect("Retry-After"); assert_eq!(retry_after.to_str().expect("ASCII value"), "42"); @@ -569,11 +524,9 @@ fn actix_responder_no_optional_headers_by_default() { http::header::{RETRY_AFTER, WWW_AUTHENTICATE}, test::TestRequest }; - let resp = ErrorResponse::new(500, AppCode::Internal, "boom").expect("status"); let req = TestRequest::default().to_http_request(); let http = resp.respond_to(&req); - let headers = http.headers(); assert!(headers.get(RETRY_AFTER).is_none()); assert!(headers.get(WWW_AUTHENTICATE).is_none()); @@ -588,11 +541,9 @@ fn serialized_json_contains_core_fields() { .expect("status") .with_retry_after_secs(1); let s = serde_json::to_string(&e).expect("serialize"); - // Fast contract sanity checks without tying to exact field order assert!(s.contains("\"status\":404")); assert!(s.contains("\"code\":\"NOT_FOUND\"")); assert!(s.contains("\"message\":\"nope\"")); - // Retry advice is serialized as nested object assert!(s.contains("\"retry\"")); assert!(s.contains("\"after_seconds\":1")); } @@ -602,7 +553,6 @@ fn internal_formatters_are_opt_in() { let resp = ErrorResponse::new(404, AppCode::NotFound, "missing").expect("status"); let formatted = format!("{:?}", resp.internal()); assert!(formatted.contains("ErrorResponse")); - let problem = ProblemJson::from_ref(&AppError::not_found("missing")); let formatted_problem = format!("{:?}", problem.internal()); assert!(formatted_problem.contains("ProblemJson")); @@ -612,7 +562,6 @@ fn internal_formatters_are_opt_in() { #[test] fn app_error_into_response_maps_status() { use axum::response::IntoResponse; - let app = AppError::new(AppErrorKind::Unauthorized, "no token"); let resp = app.into_response(); assert_eq!(resp.status(), 401); @@ -679,7 +628,6 @@ fn from_owned_app_error_with_custom_code() { let custom = AppCode::new("PAYMENT_FAILED"); let err = AppError::bad_request("transaction declined").with_code(custom.clone()); let resp: ErrorResponse = err.into(); - assert_eq!(resp.status, 400); assert_eq!(resp.code, custom); assert_eq!(resp.message, "transaction declined"); @@ -689,7 +637,6 @@ fn from_owned_app_error_with_custom_code() { fn from_owned_app_error_with_empty_message() { let err = AppError::internal(""); let resp: ErrorResponse = err.into(); - assert_eq!(resp.status, 500); assert_eq!(resp.message, ""); } @@ -698,7 +645,6 @@ fn from_owned_app_error_with_empty_message() { fn from_owned_app_error_with_unicode_message() { let err = AppError::not_found("Ошибка поиска"); let resp: ErrorResponse = err.into(); - assert_eq!(resp.status, 404); assert_eq!(resp.message, "Ошибка поиска"); } @@ -707,7 +653,6 @@ fn from_owned_app_error_with_unicode_message() { fn from_owned_app_error_with_special_characters() { let err = AppError::validation("Error: \"invalid\" <>&"); let resp: ErrorResponse = err.into(); - assert_eq!(resp.message, "Error: \"invalid\" <>&"); } @@ -716,7 +661,6 @@ fn from_owned_app_error_transfers_code_ownership() { let custom = AppCode::new("DUPLICATE_KEY"); let err = AppError::conflict("already exists").with_code(custom.clone()); let resp: ErrorResponse = err.into(); - assert_eq!(resp.code, custom); assert_eq!(resp.code.as_str(), "DUPLICATE_KEY"); } @@ -727,10 +671,8 @@ fn from_owned_app_error_transfers_code_ownership() { fn from_borrowed_app_error_preserves_original() { let err = AppError::forbidden("access denied"); let resp: ErrorResponse = (&err).into(); - assert_eq!(resp.status, 403); assert_eq!(resp.message, "access denied"); - assert_eq!(err.message.as_deref(), Some("access denied")); assert_eq!(err.kind, AppErrorKind::Forbidden); } @@ -740,9 +682,7 @@ fn from_borrowed_app_error_with_metadata() { let err = AppError::rate_limited("slow down") .with_retry_after_secs(120) .with_www_authenticate("Bearer realm=\"api\""); - let resp: ErrorResponse = (&err).into(); - assert_eq!(resp.status, 429); assert_eq!(resp.message, "slow down"); assert_eq!(resp.retry.unwrap().after_seconds, 120); @@ -750,7 +690,6 @@ fn from_borrowed_app_error_with_metadata() { resp.www_authenticate.as_deref(), Some("Bearer realm=\"api\"") ); - assert_eq!(err.retry.unwrap().after_seconds, 120); assert_eq!( err.www_authenticate.as_deref(), @@ -763,7 +702,6 @@ fn from_borrowed_app_error_clones_custom_code() { let custom = AppCode::new("SESSION_EXPIRED"); let err = AppError::unauthorized("login again").with_code(custom.clone()); let resp: ErrorResponse = (&err).into(); - assert_eq!(resp.code, custom); assert_eq!(err.code, custom); } @@ -772,7 +710,6 @@ fn from_borrowed_app_error_clones_custom_code() { fn from_borrowed_app_error_with_empty_message() { let err = AppError::timeout(""); let resp: ErrorResponse = (&err).into(); - assert_eq!(resp.status, 504); assert_eq!(resp.message, ""); } @@ -781,7 +718,6 @@ fn from_borrowed_app_error_with_empty_message() { fn from_borrowed_app_error_with_unicode() { let err = AppError::validation("無効な入力"); let resp: ErrorResponse = (&err).into(); - assert_eq!(resp.message, "無効な入力"); assert_eq!(err.message.as_deref(), Some("無効な入力")); } @@ -790,9 +726,7 @@ fn from_borrowed_app_error_with_unicode() { fn from_borrowed_app_error_redacts_message() { let err = AppError::internal("database password: secret123").redactable(); let resp: ErrorResponse = (&err).into(); - assert_eq!(resp.message, AppErrorKind::Internal.label()); assert!(!resp.message.contains("secret123")); - assert_eq!(err.message.as_deref(), Some("database password: secret123")); } diff --git a/src/result_ext.rs b/src/result_ext.rs index cb9560c..afe9b65 100644 --- a/src/result_ext.rs +++ b/src/result_ext.rs @@ -79,15 +79,12 @@ impl ResultExt for Result { E: CoreError + Send + Sync + 'static { let msg = msg.into(); - self.map_err(|err| { let source: Box = Box::new(err); - match source.downcast::() { Ok(app_err) => { let app_err = *app_err; let mut enriched = Error::new_raw(app_err.kind, Some(msg.clone())); - enriched.code = app_err.code.clone(); enriched.metadata = app_err.metadata.clone(); enriched.edit_policy = app_err.edit_policy; @@ -103,12 +100,10 @@ impl ResultExt for Result { } #[cfg(feature = "backtrace")] let shared_backtrace = app_err.backtrace_shared(); - #[cfg(feature = "backtrace")] if let Some(backtrace) = shared_backtrace { enriched = enriched.with_shared_backtrace(backtrace); } - enriched.with_context(app_err) } Err(source) => Error::internal(msg.clone()).with_source_arc(Arc::from(source)) @@ -188,11 +183,9 @@ mod tests { .track_caller() }) .expect_err("err"); - assert_eq!(err.kind, AppErrorKind::Service); assert_eq!(err.code, AppCode::Service); assert!(matches!(err.edit_policy, MessageEditPolicy::Redact)); - let metadata = err.metadata(); assert_eq!( metadata.get("operation"), @@ -211,7 +204,6 @@ mod tests { }) .ctx(|| Context::new(AppErrorKind::Internal)) .expect_err("err"); - let mut source = StdError::source(&err).expect("layered source"); assert!(source.is::()); source = source.source().expect("inner source"); @@ -251,10 +243,8 @@ mod tests { let err = Result::<(), SharedError>::Err(shared.clone()) .ctx(|| Context::new(AppErrorKind::Internal)) .expect_err("err"); - drop(shared); assert_eq!(Arc::strong_count(&inner), 2); - let stored = err .source_ref() .and_then(|src| src.downcast_ref::()) @@ -281,7 +271,6 @@ mod tests { .expect_err("err"); assert!(err.backtrace().is_none()); }); - with_backtrace_preference(Some(true), || { let err = Result::<(), DummyError>::Err(DummyError) .ctx(|| Context::new(AppErrorKind::Internal)) @@ -294,7 +283,6 @@ mod tests { fn context_wraps_with_simple_message() { let result: Result<(), DummyError> = Err(DummyError); let err = result.context("operation failed").expect_err("err"); - assert_eq!(err.kind, AppErrorKind::Internal); assert!(err.source_ref().is_some()); assert!(err.source_ref().unwrap().is::()); @@ -306,11 +294,9 @@ mod tests { .with_field(field::str("flag", "beta")) .with_code(AppCode::Cache) .redactable(); - let err = Result::<(), Error>::Err(base) .context("parsing configuration failed") .expect_err("err"); - assert_eq!(err.kind, AppErrorKind::BadRequest); assert_eq!(err.code, AppCode::Cache); assert_eq!(err.message.as_deref(), Some("parsing configuration failed")); @@ -319,7 +305,6 @@ mod tests { err.metadata().get("flag"), Some(&FieldValue::Str(Cow::Borrowed("beta"))) ); - let source = err .source_ref() .and_then(|src| src.downcast_ref::()) diff --git a/src/turnkey/classifier.rs b/src/turnkey/classifier.rs index 12d5bea..869bedc 100644 --- a/src/turnkey/classifier.rs +++ b/src/turnkey/classifier.rs @@ -35,7 +35,6 @@ const STACK_NEEDLE_INLINE_CAP: usize = 64; /// ``` #[must_use] pub fn classify_turnkey_error(msg: &str) -> TurnkeyErrorKind { - // Patterns grouped by kind. Keep short, ASCII, and conservative. const UNIQUE_PATTERNS: &[&str] = &[ "label must be unique", "already exists", @@ -57,7 +56,6 @@ pub fn classify_turnkey_error(msg: &str) -> TurnkeyErrorKind { const TO_PATTERNS: &[&str] = &["timeout", "timed out", "deadline exceeded"]; const AUTH_PATTERNS: &[&str] = &["401", "403", "unauthor", "forbidden"]; const NET_PATTERNS: &[&str] = &["network", "connection", "connect", "dns", "tls", "socket"]; - if contains_any_nocase(msg, UNIQUE_PATTERNS) { TurnkeyErrorKind::UniqueLabel } else if contains_any_nocase(msg, RL_PATTERNS) @@ -129,11 +127,9 @@ fn contains_nocase_with( let needle_bytes = needle.as_bytes(); let lowered = LowercasedNeedle::new(needle_bytes); let needle_lower = lowered.as_slice(); - if needle_lower.is_empty() { return true; } - haystack_bytes .windows(needle_lower.len()) .enumerate() @@ -195,9 +191,11 @@ const fn is_ascii_alphanumeric(byte: u8) -> bool { } /// Converts ASCII letters to lowercase and leaves other bytes unchanged. +/// +/// Uses direct byte comparison instead of `RangeInclusive` to stay +/// const-friendly on MSRV 1.90. #[inline] const fn ascii_lower(b: u8) -> u8 { - // ASCII-only fold without RangeInclusive to keep const-friendly on MSRV 1.90 if b >= b'A' && b <= b'Z' { b + 32 } else { b } } diff --git a/src/turnkey/domain.rs b/src/turnkey/domain.rs index 61c68c1..4530116 100644 --- a/src/turnkey/domain.rs +++ b/src/turnkey/domain.rs @@ -199,7 +199,6 @@ pub fn map_turnkey_kind(kind: TurnkeyErrorKind) -> AppErrorKind { TurnkeyErrorKind::Auth => AppErrorKind::Unauthorized, TurnkeyErrorKind::Network => AppErrorKind::Network, TurnkeyErrorKind::Service => AppErrorKind::Turnkey, - // Future-proofing: unknown variants map to Turnkey (500) by default. #[allow(unreachable_patterns)] _ => AppErrorKind::Turnkey } @@ -272,7 +271,6 @@ mod tests { let err2 = TurnkeyError::new(TurnkeyErrorKind::Auth, "invalid token"); let err3 = TurnkeyError::new(TurnkeyErrorKind::Auth, "different message"); let err4 = TurnkeyError::new(TurnkeyErrorKind::Network, "invalid token"); - assert_eq!(err1, err2); assert_ne!(err1, err3); assert_ne!(err1, err4); diff --git a/src/turnkey/tests.rs b/src/turnkey/tests.rs index 5024b31..ce210a0 100644 --- a/src/turnkey/tests.rs +++ b/src/turnkey/tests.rs @@ -221,7 +221,6 @@ fn turnkey_error_partial_eq_compares_kind_and_message() { let err2 = TurnkeyError::new(TurnkeyErrorKind::Auth, "invalid token"); let err3 = TurnkeyError::new(TurnkeyErrorKind::Auth, "different message"); let err4 = TurnkeyError::new(TurnkeyErrorKind::Service, "invalid token"); - assert_eq!(err1, err2); assert_ne!(err1, err3); assert_ne!(err1, err4); diff --git a/tests/app_code_reuse.rs b/tests/app_code_reuse.rs index 50c61a6..59a9891 100644 --- a/tests/app_code_reuse.rs +++ b/tests/app_code_reuse.rs @@ -14,9 +14,7 @@ fn error_with_dynamic_code() -> AppError { fn problem_json_reuses_app_code_allocation() { let error = error_with_dynamic_code(); let expected_ptr = error.code.as_str().as_ptr(); - let problem = ProblemJson::from_app_error(error); - assert_eq!(problem.code.as_str().as_ptr(), expected_ptr); } @@ -24,8 +22,6 @@ fn problem_json_reuses_app_code_allocation() { fn error_response_reuses_app_code_allocation() { let error = error_with_dynamic_code(); let expected_ptr = error.code.as_str().as_ptr(); - let response = ErrorResponse::from(error); - assert_eq!(response.code.as_str().as_ptr(), expected_ptr); } diff --git a/tests/build_test.rs b/tests/build_test.rs index a51ab67..ac01696 100644 --- a/tests/build_test.rs +++ b/tests/build_test.rs @@ -17,59 +17,43 @@ fn generate_readme_with_valid_input() { let temp = TempDir::new().unwrap(); let manifest_path = temp.path().join("Cargo.toml"); let template_path = temp.path().join("README.template.md"); - let manifest_content = r#" [package] name = "test-crate" version = "0.1.0" rust-version = "1.70" - [package.metadata.masterror.readme] feature_order = ["feature1", "feature2"] feature_snippet_group = 2 conversion_lines = ["std::io::Error → Internal", "String → BadRequest"] - [package.metadata.masterror.readme.features.feature1] description = "First feature" extra = ["Extra note 1"] - [package.metadata.masterror.readme.features.feature2] description = "Second feature" - [features] default = [] feature1 = [] feature2 = [] "#; - let template_content = r#"# Test Crate - Version: {{CRATE_VERSION}} MSRV: {{MSRV}} - ## Features - {{FEATURE_BULLETS}} - ## Example - ```toml [dependencies] test-crate = { version = "{{CRATE_VERSION}}", features = [ {{FEATURE_SNIPPET}} ] } ``` - ## Conversions - {{CONVERSION_BULLETS}} "#; - fs::write(&manifest_path, manifest_content).unwrap(); fs::write(&template_path, template_content).unwrap(); - let result = generate_readme(&manifest_path, &template_path).unwrap(); - assert!(result.contains("Version: 0.1.0")); assert!(result.contains("MSRV: 1.70")); assert!(result.contains("- `feature1` — First feature")); @@ -85,21 +69,16 @@ fn generate_readme_fails_with_missing_metadata() { let temp = TempDir::new().unwrap(); let manifest_path = temp.path().join("Cargo.toml"); let template_path = temp.path().join("README.template.md"); - let manifest_content = r#" [package] name = "test-crate" version = "0.1.0" - [features] feature1 = [] "#; - fs::write(&manifest_path, manifest_content).unwrap(); fs::write(&template_path, "# Template").unwrap(); - let result = generate_readme(&manifest_path, &template_path); - assert!(result.is_err()); assert!(matches!( result.unwrap_err(), @@ -112,24 +91,18 @@ fn generate_readme_fails_with_missing_feature_metadata() { let temp = TempDir::new().unwrap(); let manifest_path = temp.path().join("Cargo.toml"); let template_path = temp.path().join("README.template.md"); - let manifest_content = r#" [package] name = "test-crate" version = "0.1.0" - [package.metadata.masterror.readme] - [features] default = [] feature1 = [] "#; - fs::write(&manifest_path, manifest_content).unwrap(); fs::write(&template_path, "# Template").unwrap(); - let result = generate_readme(&manifest_path, &template_path); - assert!(result.is_err()); assert!(matches!( result.unwrap_err(), @@ -142,28 +115,21 @@ fn generate_readme_fails_with_unknown_feature_in_order() { let temp = TempDir::new().unwrap(); let manifest_path = temp.path().join("Cargo.toml"); let template_path = temp.path().join("README.template.md"); - let manifest_content = r#" [package] name = "test-crate" version = "0.1.0" - [package.metadata.masterror.readme] feature_order = ["unknown_feature"] - [package.metadata.masterror.readme.features.feature1] description = "Feature 1" - [features] default = [] feature1 = [] "#; - fs::write(&manifest_path, manifest_content).unwrap(); fs::write(&template_path, "# Template").unwrap(); - let result = generate_readme(&manifest_path, &template_path); - assert!(result.is_err()); assert!(matches!( result.unwrap_err(), @@ -176,28 +142,21 @@ fn generate_readme_fails_with_duplicate_feature_in_order() { let temp = TempDir::new().unwrap(); let manifest_path = temp.path().join("Cargo.toml"); let template_path = temp.path().join("README.template.md"); - let manifest_content = r#" [package] name = "test-crate" version = "0.1.0" - [package.metadata.masterror.readme] feature_order = ["feature1", "feature1"] - [package.metadata.masterror.readme.features.feature1] description = "Feature 1" - [features] default = [] feature1 = [] "#; - fs::write(&manifest_path, manifest_content).unwrap(); fs::write(&template_path, "# Template").unwrap(); - let result = generate_readme(&manifest_path, &template_path); - assert!(result.is_err()); assert!(matches!( result.unwrap_err(), @@ -210,25 +169,18 @@ fn generate_readme_fails_with_unresolved_placeholder() { let temp = TempDir::new().unwrap(); let manifest_path = temp.path().join("Cargo.toml"); let template_path = temp.path().join("README.template.md"); - let manifest_content = r#" [package] name = "test-crate" version = "0.1.0" - [package.metadata.masterror.readme] - [features] default = [] "#; - let template_content = "# Template {{UNKNOWN_PLACEHOLDER}}"; - fs::write(&manifest_path, manifest_content).unwrap(); fs::write(&template_path, template_content).unwrap(); - let result = generate_readme(&manifest_path, &template_path); - assert!(result.is_err()); assert!(matches!( result.unwrap_err(), @@ -241,24 +193,18 @@ fn generate_readme_fails_with_zero_snippet_group() { let temp = TempDir::new().unwrap(); let manifest_path = temp.path().join("Cargo.toml"); let template_path = temp.path().join("README.template.md"); - let manifest_content = r#" [package] name = "test-crate" version = "0.1.0" - [package.metadata.masterror.readme] feature_snippet_group = 0 - [features] default = [] "#; - fs::write(&manifest_path, manifest_content).unwrap(); fs::write(&template_path, "{{FEATURE_SNIPPET}}").unwrap(); - let result = generate_readme(&manifest_path, &template_path); - assert!(result.is_err()); assert!(matches!( result.unwrap_err(), @@ -271,25 +217,18 @@ fn generate_readme_handles_missing_rust_version() { let temp = TempDir::new().unwrap(); let manifest_path = temp.path().join("Cargo.toml"); let template_path = temp.path().join("README.template.md"); - let manifest_content = r#" [package] name = "test-crate" version = "0.1.0" - [package.metadata.masterror.readme] - [features] default = [] "#; - let template_content = "MSRV: {{MSRV}}"; - fs::write(&manifest_path, manifest_content).unwrap(); fs::write(&template_path, template_content).unwrap(); - let result = generate_readme(&manifest_path, &template_path).unwrap(); - assert!(result.contains("MSRV: unknown")); } diff --git a/tests/enforce_app_result_alias.rs b/tests/enforce_app_result_alias.rs index 59642f6..fef7eb7 100644 --- a/tests/enforce_app_result_alias.rs +++ b/tests/enforce_app_result_alias.rs @@ -26,13 +26,10 @@ fn prohibits_direct_result_app_error_usage() { let src_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src"); let mut files = Vec::new(); collect_rs_files(&src_dir, &mut files).expect("collect Rust sources"); - let mut offenders = Vec::new(); - for path in &files { let content = fs::read_to_string(path) .unwrap_or_else(|err| panic!("failed to read {}: {err}", path.display())); - for (idx, line) in content.lines().enumerate() { if line.contains("Result<") && line.contains("AppError") @@ -47,7 +44,6 @@ fn prohibits_direct_result_app_error_usage() { } } } - if !offenders.is_empty() { panic!( "Found direct `Result<_, AppError>` usage; replace with `AppResult<_>`: {}", diff --git a/tests/ensure_fail.rs b/tests/ensure_fail.rs index d9c3c2d..40e323c 100644 --- a/tests/ensure_fail.rs +++ b/tests/ensure_fail.rs @@ -14,7 +14,6 @@ fn ensure_allows_success_path() { masterror::ensure!(flag, AppError::bad_request("flag required")); Ok("ok") } - assert_eq!(run(true).unwrap(), "ok"); } @@ -24,16 +23,13 @@ fn ensure_yields_error_once() { CALLS.fetch_add(1, Ordering::SeqCst); AppError::service("bounded") } - fn run(flag: bool) -> AppResult<()> { masterror::ensure!(cond = flag, else = build_error()); Ok(()) } - CALLS.store(0, Ordering::SeqCst); assert!(run(false).is_err()); assert_eq!(CALLS.load(Ordering::SeqCst), 1); - CALLS.store(0, Ordering::SeqCst); assert!(run(true).is_ok()); assert_eq!(CALLS.load(Ordering::SeqCst), 0); @@ -45,7 +41,6 @@ fn ensure_preserves_error_kind() { masterror::ensure!(flag, AppError::unauthorized("token expired")); Ok(()) } - let err = run(false).unwrap_err(); assert!(matches!(err.kind, AppErrorKind::Unauthorized)); } @@ -55,7 +50,6 @@ fn fail_returns_error() { fn run() -> AppResult<()> { masterror::fail!(AppError::forbidden("admin only")); } - let err = run().unwrap_err(); assert!(matches!(err.kind, AppErrorKind::Forbidden)); } @@ -71,11 +65,9 @@ fn macros_work_with_custom_error_types() { masterror::ensure!(flag, CustomError("custom failure")); Ok("ok") } - fn bail() -> CustomResult<()> { masterror::fail!(CustomError("fail")); } - assert_eq!(guard(true).unwrap(), "ok"); assert_eq!(guard(false).unwrap_err(), CustomError("custom failure")); assert_eq!(bail().unwrap_err(), CustomError("fail")); diff --git a/tests/error_derive.rs b/tests/error_derive.rs index fc9a23d..8778dba 100644 --- a/tests/error_derive.rs +++ b/tests/error_derive.rs @@ -5,6 +5,7 @@ // // SPDX-License-Identifier: MIT +use core::ptr::null; #[cfg(masterror_has_error_generic_member_access)] use std::ptr; use std::{error::Error as StdError, fmt}; @@ -454,8 +455,7 @@ where { let reported = std::error::Error::backtrace(error).expect("backtrace"); assert!(ptr::eq(expected, reported)); - let provided = - std::error::request_ref::(error).expect("provided backtrace"); + let provided = request_ref::(error).expect("provided backtrace"); assert!(ptr::eq(reported, provided)); } @@ -476,13 +476,9 @@ fn struct_provides_custom_telemetry() { let err = StructuredTelemetryError { snapshot: telemetry.clone() }; - - let provided_ref = - std::error::request_ref::(&err).expect("telemetry reference"); + let provided_ref = request_ref::(&err).expect("telemetry reference"); assert!(ptr::eq(provided_ref, &err.snapshot)); - - let provided_value = - std::error::request_value::(&err).expect("telemetry value"); + let provided_value = request_value::(&err).expect("telemetry value"); assert_eq!(provided_value, telemetry); } @@ -493,27 +489,22 @@ fn option_telemetry_only_provided_when_present() { name: "task", value: 13 }; - let with_value = OptionalTelemetryError { telemetry: Some(snapshot.clone()) }; - let provided = - std::error::request_ref::(&with_value).expect("optional telemetry"); + let provided = request_ref::(&with_value).expect("optional telemetry"); let inner = with_value.telemetry.as_ref().expect("inner telemetry"); assert!(ptr::eq(provided, inner)); - let without = OptionalTelemetryError { telemetry: None }; assert!(std::error::request_ref::(&without).is_none()); - let owned_value = OptionalOwnedTelemetryError { telemetry: Some(snapshot.clone()) }; let provided_owned = - std::error::request_value::(&owned_value).expect("owned telemetry"); + request_value::(&owned_value).expect("owned telemetry"); assert_eq!(provided_owned, snapshot); - let owned_none = OptionalOwnedTelemetryError { telemetry: None }; @@ -527,33 +518,27 @@ fn enum_variants_provide_custom_telemetry() { name: "span", value: 21 }; - let named = EnumTelemetryError::Named { label: "named", snapshot: named_snapshot.clone() }; - let provided_named = - std::error::request_ref::(&named).expect("named telemetry"); + let provided_named = request_ref::(&named).expect("named telemetry"); if let EnumTelemetryError::Named { snapshot, .. } = &named { assert!(ptr::eq(provided_named, snapshot)); } - let optional = EnumTelemetryError::Optional(Some(named_snapshot.clone())); let provided_optional = - std::error::request_ref::(&optional).expect("optional telemetry"); + request_ref::(&optional).expect("optional telemetry"); if let EnumTelemetryError::Optional(Some(snapshot)) = &optional { assert!(ptr::eq(provided_optional, snapshot)); } - let optional_none = EnumTelemetryError::Optional(None); assert!(std::error::request_ref::(&optional_none).is_none()); - let owned = EnumTelemetryError::Owned(named_snapshot.clone()); - let provided_owned = - std::error::request_value::(&owned).expect("owned telemetry"); + let provided_owned = request_value::(&owned).expect("owned telemetry"); assert_eq!(provided_owned, named_snapshot); } @@ -581,14 +566,12 @@ fn enum_variants_cover_display_and_source() { let unit = EnumError::Unit; assert_eq!(unit.to_string(), "unit failure"); assert!(StdError::source(&unit).is_none()); - let code = EnumError::Code { code: 503, cause: LeafError }; assert_eq!(code.to_string(), "503"); assert_eq!(StdError::source(&code).unwrap().to_string(), "leaf failure"); - let pair = EnumError::Pair("left".into(), LeafError); assert!(pair.to_string().starts_with("left")); assert_eq!(StdError::source(&pair).unwrap().to_string(), "leaf failure"); @@ -675,14 +658,12 @@ fn enum_from_variants_generate_impls() { StdError::source(&tuple).unwrap().to_string(), "leaf failure" ); - let variant_attr = MixedFromError::from(PrimaryError); assert!(matches!(&variant_attr, MixedFromError::VariantAttr(_))); assert_eq!( StdError::source(&variant_attr).unwrap().to_string(), "primary failure" ); - let named = MixedFromError::from(SecondaryError); assert!(matches!( &named, @@ -814,7 +795,6 @@ fn enum_backtrace_field_is_returned() { if let EnumWithBacktrace::Tuple(_, trace) = &tuple { assert_backtrace_interfaces(&tuple, trace); } - let named = EnumWithBacktrace::Named { message: "named", trace: std::backtrace::Backtrace::capture() @@ -825,7 +805,6 @@ fn enum_backtrace_field_is_returned() { { assert_backtrace_interfaces(&named, trace); } - let unit = EnumWithBacktrace::Unit; #[cfg(masterror_has_error_generic_member_access)] { @@ -839,24 +818,19 @@ fn supports_display_and_debug_formatters() { label: "Alpha" }; let tuple = ("tuple", 7u8); - let expected = format!( "display={value} debug={value:?} #debug={value:#?} tuple={tuple:?} #tuple={tuple:#?}", ); - let standard_debug = format!("{value:?}"); let alternate_debug = format!("{value:#?}"); assert_ne!(standard_debug, alternate_debug); - let tuple_debug = format!("{tuple:?}"); let tuple_alternate_debug = format!("{tuple:#?}"); assert_ne!(tuple_debug, tuple_alternate_debug); - let err = FormatterDebugShowcase { value, tuple }; - assert_eq!(err.to_string(), expected); assert!(StdError::source(&err).is_none()); } @@ -870,7 +844,6 @@ fn struct_projection_shorthand_handles_nested_segments() { suggestion: Some("retry".to_string()) }; assert_eq!(err.to_string(), "range 2-5 suggestion retry"); - let none = ProjectionStructError { limits: RangeLimits { lo: -1, hi: 3 @@ -886,12 +859,10 @@ fn enum_projection_shorthand_handles_nested_segments() { data: "payload" }); assert_eq!(tuple.to_string(), "tuple data payload"); - let named = ProjectionEnumError::Named { suggestion: Some("escalate".to_string()) }; assert_eq!(named.to_string(), "named suggestion escalate"); - let fallback = ProjectionEnumError::Named { suggestion: None }; @@ -950,13 +921,11 @@ fn enum_backtrace_is_inferred_without_attribute() { assert_backtrace_interfaces(&named, trace); } assert!(StdError::source(&named).is_none()); - let tuple = AutoBacktraceEnum::Tuple(Some(std::backtrace::Backtrace::capture())); if let AutoBacktraceEnum::Tuple(Some(trace)) = &tuple { assert_backtrace_interfaces(&tuple, trace); } assert!(StdError::source(&tuple).is_none()); - #[cfg(masterror_has_error_generic_member_access)] { let none = AutoBacktraceEnum::Tuple(None); @@ -968,28 +937,23 @@ fn enum_backtrace_is_inferred_without_attribute() { fn supports_extended_formatters() { let value = 0x5A5Au32; let float = 1234.5_f64; - let ptr = core::ptr::null::(); - + let ptr = null::(); let err = FormatterShowcase { value, float, ptr }; - let expected = format!( "display={value} debug={value:?} #debug={value:#?} x={value:x} X={value:X} \ #x={value:#x} #X={value:#X} b={value:b} #b={value:#b} o={value:o} #o={value:#o} \ e={float:e} #e={float:#e} E={float:E} #E={float:#E} p={ptr:p} #p={ptr:#p}" ); - let lower_hex = format!("{value:x}"); let upper_hex = format!("{value:X}"); assert_ne!(lower_hex, upper_hex); - let lower_exp = format!("{float:e}"); let upper_exp = format!("{float:E}"); assert_ne!(lower_exp, upper_exp); - assert_eq!(err.to_string(), expected); assert!(StdError::source(&err).is_none()); } @@ -1000,7 +964,6 @@ fn formatter_variants_render_expected_output() { value: "display" }; assert_eq!(display.to_string(), "display"); - let debug = DebugFormatterError { value: PrettyDebugValue { label: "Debug" @@ -1027,7 +990,6 @@ fn formatter_variants_render_expected_output() { } ) ); - const HEX_VALUE: u32 = 0x5A5A; let lower_hex = LowerHexFormatterError { value: HEX_VALUE @@ -1035,7 +997,6 @@ fn formatter_variants_render_expected_output() { let lower_hex_expected = format!("lower={value:x} #lower={value:#x}", value = HEX_VALUE); assert_eq!(lower_hex.to_string(), lower_hex_expected); assert_ne!(format!("{HEX_VALUE:x}"), format!("{HEX_VALUE:#x}")); - let upper_hex = UpperHexFormatterError { value: HEX_VALUE }; @@ -1043,7 +1004,6 @@ fn formatter_variants_render_expected_output() { assert_eq!(upper_hex.to_string(), upper_hex_expected); assert_ne!(format!("{HEX_VALUE:X}"), format!("{HEX_VALUE:#X}")); assert_ne!(format!("{HEX_VALUE:x}"), format!("{HEX_VALUE:X}")); - const INTEGER_VALUE: u16 = 0b1010_1100; let binary = BinaryFormatterError { value: INTEGER_VALUE @@ -1051,15 +1011,13 @@ fn formatter_variants_render_expected_output() { let binary_expected = format!("binary={value:b} #binary={value:#b}", value = INTEGER_VALUE); assert_eq!(binary.to_string(), binary_expected); assert_ne!(format!("{INTEGER_VALUE:b}"), format!("{INTEGER_VALUE:#b}")); - let octal = OctalFormatterError { value: INTEGER_VALUE }; let octal_expected = format!("octal={value:o} #octal={value:#o}", value = INTEGER_VALUE); assert_eq!(octal.to_string(), octal_expected); assert_ne!(format!("{INTEGER_VALUE:o}"), format!("{INTEGER_VALUE:#o}")); - - let pointer_value = core::ptr::null::(); + let pointer_value = null::(); let pointer = PointerFormatterError { value: pointer_value }; @@ -1069,14 +1027,12 @@ fn formatter_variants_render_expected_output() { ); assert_eq!(pointer.to_string(), pointer_expected); assert_ne!(format!("{pointer_value:p}"), format!("{pointer_value:#p}")); - const FLOAT_VALUE: f64 = 1234.5; let lower_exp = LowerExpFormatterError { value: FLOAT_VALUE }; let lower_exp_expected = format!("lower={value:e} #lower={value:#e}", value = FLOAT_VALUE); assert_eq!(lower_exp.to_string(), lower_exp_expected); - let upper_exp = UpperExpFormatterError { value: FLOAT_VALUE }; @@ -1091,17 +1047,14 @@ fn display_format_specs_match_standard_formatting() { value: "x" }; assert_eq!(alignment.to_string(), format!("{:>8}", "x")); - let precision = DisplayPrecisionError { value: 123.456_f64 }; assert_eq!(precision.to_string(), format!("{:.3}", 123.456_f64)); - let fill = DisplayFillError { value: "ab" }; assert_eq!(fill.to_string(), format!("{:*<6}", "ab")); - let dynamic_width = DisplayDynamicWidthError { value: "x", width: 5 @@ -1110,7 +1063,6 @@ fn display_format_specs_match_standard_formatting() { dynamic_width.to_string(), format!("{value:>width$}", value = "x", width = 5) ); - let dynamic_precision = DisplayDynamicPrecisionError { value: 123.456_f64, precision: 4 diff --git a/tests/masterror_macro.rs b/tests/masterror_macro.rs index 0b8e6d2..9dfdc1d 100644 --- a/tests/masterror_macro.rs +++ b/tests/masterror_macro.rs @@ -68,9 +68,7 @@ fn struct_masterror_conversion_populates_metadata_and_source() { attempt: Some(3), source: Some(source) }; - let converted: MasterrorError = err.into(); - assert_eq!(converted.code, AppCode::NotFound); assert_eq!(converted.kind, AppErrorKind::NotFound); assert_eq!(converted.edit_policy, MessageEditPolicy::Redact); @@ -80,7 +78,6 @@ fn struct_masterror_conversion_populates_metadata_and_source() { .as_deref() .is_some_and(|message| message.contains("beta")) ); - let user_id = converted .metadata() .get("user_id") @@ -89,7 +86,6 @@ fn struct_masterror_conversion_populates_metadata_and_source() { _ => None }); assert_eq!(user_id, Some("alice")); - let attempt = converted .metadata() .get("attempt") @@ -98,11 +94,9 @@ fn struct_masterror_conversion_populates_metadata_and_source() { _ => None }); assert_eq!(attempt, Some(3)); - assert!(converted.source_ref().is_some()); let converted_source = StdError::source(&converted).expect("masterror source"); assert!(converted_source.is::()); - assert_eq!( MissingFlag::HTTP_MAPPING, HttpMapping::new(AppCode::NotFound, AppErrorKind::NotFound) @@ -112,11 +106,9 @@ fn struct_masterror_conversion_populates_metadata_and_source() { Some(FieldRedaction::Hash) ); assert_eq!(MissingFlag::HTTP_MAPPING.status(), 404); - let grpc = MissingFlag::GRPC_MAPPING.expect("grpc mapping"); assert_eq!(grpc.status(), 5); assert_eq!(grpc.kind(), AppErrorKind::NotFound); - let problem = MissingFlag::PROBLEM_MAPPING.expect("problem mapping"); assert_eq!(problem.type_uri(), "https://errors.example.com/not-found"); } @@ -128,7 +120,6 @@ fn enum_masterror_conversion_handles_variants() { details: "missing field", _source: io_error }; - let converted: MasterrorError = payload.into(); assert_eq!(converted.code, AppCode::BadRequest); assert_eq!(converted.kind, AppErrorKind::BadRequest); @@ -136,19 +127,16 @@ fn enum_masterror_conversion_handles_variants() { |value| matches!(value, masterror::FieldValue::Str(detail) if detail == "missing field") )); assert!(converted.source_ref().is_some()); - let offline: MasterrorError = ApiError::StorageOffline.into(); assert_eq!(offline.code, AppCode::Service); assert_eq!(offline.kind, AppErrorKind::Service); assert!(offline.metadata().is_empty()); - assert_eq!(ApiError::HTTP_MAPPINGS.len(), 2); assert!( ApiError::HTTP_MAPPINGS .iter() .any(|mapping| mapping.kind() == AppErrorKind::BadRequest) ); - assert_eq!( ApiError::GRPC_MAPPINGS, &[GrpcMapping::new( @@ -157,7 +145,6 @@ fn enum_masterror_conversion_handles_variants() { 14 )] ); - assert_eq!( ApiError::PROBLEM_MAPPINGS, &[ProblemMapping::new( @@ -175,9 +162,7 @@ fn masterror_preserves_arc_source_without_extra_clone() { source: source.clone() } .into(); - assert_eq!(Arc::strong_count(&source), 2); - let stored = converted .source_ref() .and_then(|src| src.downcast_ref::()) diff --git a/tests/readme_sync.rs b/tests/readme_sync.rs index 01c0d42..463064b 100644 --- a/tests/readme_sync.rs +++ b/tests/readme_sync.rs @@ -34,16 +34,12 @@ fn readme_is_in_sync() -> Result<(), Box> { let manifest_path = manifest_dir.join("Cargo.toml"); let template_path = manifest_dir.join("README.template.md"); let readme_path = manifest_dir.join("README.md"); - let generated = readme::generate_readme(&manifest_path, &template_path)?; let actual = fs::read_to_string(&readme_path)?; - if actual != generated { - // Use std::io::Error::other to satisfy clippy::io-other-error let msg = "README.md is out of date; run `cargo build` to regenerate"; return Err(io::Error::other(msg).into()); } - Ok(()) } @@ -53,12 +49,10 @@ fn verify_readme_succeeds_when_in_sync() -> Result<(), Box> { let manifest_path = tmp.path().join("Cargo.toml"); let template_path = tmp.path().join("README.template.md"); let readme_path = tmp.path().join("README.md"); - fs::write(&manifest_path, MINIMAL_MANIFEST)?; fs::write(&template_path, MINIMAL_TEMPLATE)?; let generated = readme::generate_readme(&manifest_path, &template_path)?; fs::write(&readme_path, generated)?; - readme::verify_readme(tmp.path()).map_err(|err| io::Error::other(err.to_string()))?; Ok(()) } @@ -69,11 +63,9 @@ fn verify_readme_detects_out_of_sync() -> Result<(), Box> { let manifest_path = tmp.path().join("Cargo.toml"); let template_path = tmp.path().join("README.template.md"); let readme_path = tmp.path().join("README.md"); - fs::write(&manifest_path, MINIMAL_MANIFEST)?; fs::write(&template_path, MINIMAL_TEMPLATE)?; fs::write(&readme_path, "outdated")?; - match readme::verify_readme(tmp.path()) { Err(readme::ReadmeError::OutOfSync { path diff --git a/tests/ui/app_error/pass/enum.rs b/tests/ui/app_error/pass/enum.rs index f616080..8268219 100644 --- a/tests/ui/app_error/pass/enum.rs +++ b/tests/ui/app_error/pass/enum.rs @@ -23,12 +23,10 @@ fn main() { let app_missing: AppError = missing.into(); assert!(matches!(app_missing.kind, AppErrorKind::NotFound)); assert_eq!(app_missing.message.as_deref(), Some("missing resource 7")); - let backend = ApiError::Backend; let app_backend: AppError = backend.into(); assert!(matches!(app_backend.kind, AppErrorKind::Service)); assert!(app_backend.message.is_none()); - let code: AppCode = ApiError::Backend.into(); assert_eq!(code, AppCode::Service); } diff --git a/tests/ui/app_error/pass/struct.rs b/tests/ui/app_error/pass/struct.rs index 396a7d7..4cb77bf 100644 --- a/tests/ui/app_error/pass/struct.rs +++ b/tests/ui/app_error/pass/struct.rs @@ -16,7 +16,6 @@ fn main() { let app: AppError = err.into(); assert!(matches!(app.kind, AppErrorKind::BadRequest)); assert_eq!(app.message.as_deref(), Some("missing flag: feature")); - let code: AppCode = MissingFlag { name: "other" }.into(); assert_eq!(code, AppCode::BadRequest); } diff --git a/tests/ui/formatter/pass/all_formatters.rs b/tests/ui/formatter/pass/all_formatters.rs index 45cc25f..e090002 100644 --- a/tests/ui/formatter/pass/all_formatters.rs +++ b/tests/ui/formatter/pass/all_formatters.rs @@ -3,6 +3,7 @@ // SPDX-License-Identifier: MIT use masterror::Error; +use core::ptr::null; #[derive(Debug, Error)] #[error( @@ -32,9 +33,8 @@ fn main() { let showcase = FormatterVariants { value: 0x5A5Au32, float: 1234.5, - ptr: core::ptr::null(), + ptr: null(), pretty: PrettyDebugValue { label: "alpha" }, }; - let _ = showcase.to_string(); } diff --git a/tests/ui/formatter/pass/display_dynamic_specs.rs b/tests/ui/formatter/pass/display_dynamic_specs.rs index 547de3d..d2e7832 100644 --- a/tests/ui/formatter/pass/display_dynamic_specs.rs +++ b/tests/ui/formatter/pass/display_dynamic_specs.rs @@ -24,7 +24,6 @@ fn main() { width: 8, } .to_string(); - let _ = DynamicPrecisionError { value: 42.4242, precision: 3, diff --git a/tests/ui/formatter/pass/fmt_path.rs b/tests/ui/formatter/pass/fmt_path.rs index 0ec7380..72d2298 100644 --- a/tests/ui/formatter/pass/fmt_path.rs +++ b/tests/ui/formatter/pass/fmt_path.rs @@ -45,7 +45,6 @@ fn main() { label: "alpha" } .to_string(); - let _ = EnumFormatter::Unit.to_string(); let _ = EnumFormatter::Tuple(10, 20).to_string(); let _ = EnumFormatter::Named { left: 5, right: 15 }.to_string(); diff --git a/tests/ui/formatter/pass/format_arguments.rs b/tests/ui/formatter/pass/format_arguments.rs index 7042ee8..80e06b9 100644 --- a/tests/ui/formatter/pass/format_arguments.rs +++ b/tests/ui/formatter/pass/format_arguments.rs @@ -32,13 +32,11 @@ fn main() { name: "right", } .to_string(); - let _ = PositionalArgumentUsage { first: "positional-0", second: "positional-1", } .to_string(); - let _ = MixedImplicitUsage { label: "tag", first: "one", diff --git a/tests/ui/formatter/pass/individual_formatters.rs b/tests/ui/formatter/pass/individual_formatters.rs index c928a29..2b5fdb6 100644 --- a/tests/ui/formatter/pass/individual_formatters.rs +++ b/tests/ui/formatter/pass/individual_formatters.rs @@ -3,6 +3,7 @@ // SPDX-License-Identifier: MIT use masterror::Error; +use core::ptr::null; #[derive(Debug, Error)] #[error("{value}")] @@ -68,7 +69,7 @@ fn main() { let _ = LowerExpPair { value: 1234.5 }.to_string(); let _ = UpperExpPair { value: 1234.5 }.to_string(); let _ = PointerPair { - value: core::ptr::null::() + value: null::() } .to_string(); } From 8753321ce7bd554194556cef15138a7d014f1f46 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Dec 2025 09:49:55 +0700 Subject: [PATCH 2/5] chore(deps): bump the minor-and-patch group with 5 updates (#352) * chore: update CHANGELOG.md [skip ci] * chore(deps): bump the minor-and-patch group with 5 updates Bumps the minor-and-patch group with 5 updates: | Package | From | To | | --- | --- | --- | | [serde_json](https://github.com/serde-rs/json) | `1.0.147` | `1.0.148` | | [itoa](https://github.com/dtolnay/itoa) | `1.0.16` | `1.0.17` | | [ryu](https://github.com/dtolnay/ryu) | `1.0.21` | `1.0.22` | | [proc-macro2](https://github.com/dtolnay/proc-macro2) | `1.0.103` | `1.0.104` | | [axum-test](https://github.com/JosephLenton/axum-test) | `18.4.1` | `18.5.0` | Updates `serde_json` from 1.0.147 to 1.0.148 - [Release notes](https://github.com/serde-rs/json/releases) - [Commits](https://github.com/serde-rs/json/compare/v1.0.147...v1.0.148) Updates `itoa` from 1.0.16 to 1.0.17 - [Release notes](https://github.com/dtolnay/itoa/releases) - [Commits](https://github.com/dtolnay/itoa/compare/1.0.16...1.0.17) Updates `ryu` from 1.0.21 to 1.0.22 - [Release notes](https://github.com/dtolnay/ryu/releases) - [Commits](https://github.com/dtolnay/ryu/compare/1.0.21...1.0.22) Updates `proc-macro2` from 1.0.103 to 1.0.104 - [Release notes](https://github.com/dtolnay/proc-macro2/releases) - [Commits](https://github.com/dtolnay/proc-macro2/compare/1.0.103...1.0.104) Updates `axum-test` from 18.4.1 to 18.5.0 - [Commits](https://github.com/JosephLenton/axum-test/commits) --- updated-dependencies: - dependency-name: serde_json dependency-version: 1.0.148 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-and-patch - dependency-name: itoa dependency-version: 1.0.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-and-patch - dependency-name: ryu dependency-version: 1.0.22 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-and-patch - dependency-name: proc-macro2 dependency-version: 1.0.104 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-and-patch - dependency-name: axum-test dependency-version: 18.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-and-patch ... Signed-off-by: dependabot[bot] --------- Signed-off-by: dependabot[bot] Co-authored-by: github-actions[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: RA <70325462+RAprogramm@users.noreply.github.com> --- CHANGELOG.md | 4 ++++ Cargo.lock | 32 +++++++++++++++---------------- examples/axum-rest-api/Cargo.toml | 2 +- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcfc0f5..5641d1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [unreleased](https://github.com/RAprogramm/masterror/compare/v0.27.0...HEAD) + +### Miscellaneous + +- Configure dependabot to group minor/patch updates by [@RAprogramm](https://github.com/RAprogramm) ([653ccd2](https://github.com/RAprogramm/masterror/commit/653ccd299cb2833f11cdff716bdc24fa06af1d03)) ## [0.27.0](https://github.com/RAprogramm/masterror/releases/tag/v0.27.0) - 2025-12-26 ### Dependencies diff --git a/Cargo.lock b/Cargo.lock index ee5cd52..db7b813 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -361,9 +361,9 @@ dependencies = [ [[package]] name = "axum-test" -version = "18.4.1" +version = "18.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3290e73c56c5cc4701cdd7d46b9ced1b4bd61c7e9f9c769a9e9e87ff617d75d2" +checksum = "cf48df8b4be768081e11b7bb6d50e7dd96a3616b0b728f9e8d49bfbd8116f3c6" dependencies = [ "anyhow", "axum", @@ -1140,9 +1140,9 @@ dependencies = [ [[package]] name = "expect-json" -version = "1.7.1" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "422e7906e79941e5ac58c64dfd2da03e6ae3de62227f87606fbbe125d91080f9" +checksum = "aaf3355a7ef83e52c9383ab0c7719acd1da54be5fed7c6572d87ddc4d8589753" dependencies = [ "chrono", "email_address", @@ -1158,9 +1158,9 @@ dependencies = [ [[package]] name = "expect-json-macros" -version = "1.7.1" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6b515b7f10f1e61bfd938522e9884509b82060af2016153f5b3d6f44d6da89c" +checksum = "24ff9262e5b5f9760f60c57ada4fffd25201ae9fefd426f29f097dcc573d86e6" dependencies = [ "proc-macro2", "quote", @@ -1849,9 +1849,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" @@ -2584,9 +2584,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" dependencies = [ "unicode-ident", ] @@ -2936,9 +2936,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62049b2877bf12821e8f9ad256ee38fdc31db7387ec2d3b3f403024de2034aea" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] name = "same-file" @@ -3072,9 +3072,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.147" +version = "1.0.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6af14725505314343e673e9ecb7cd7e8a36aa9791eb936235a3567cc31447ae4" +checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" dependencies = [ "itoa", "memchr", @@ -4775,6 +4775,6 @@ dependencies = [ [[package]] name = "zmij" -version = "0.1.9" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0095ecd462946aa3927d9297b63ef82fb9a5316d7a37d134eeb36e58228615a" +checksum = "e9747e91771f56fd7893e1164abd78febd14a670ceec257caad15e051de35f06" diff --git a/examples/axum-rest-api/Cargo.toml b/examples/axum-rest-api/Cargo.toml index 35b8520..35f3eb8 100644 --- a/examples/axum-rest-api/Cargo.toml +++ b/examples/axum-rest-api/Cargo.toml @@ -22,4 +22,4 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } uuid = { version = "1", features = ["v4", "serde"] } [dev-dependencies] -axum-test = "18.1" +axum-test = "18.5" From 402dd7b80122ca2a93051853e98029b7c169a93e Mon Sep 17 00:00:00 2001 From: RAprogramm Date: Tue, 30 Dec 2025 09:52:09 +0700 Subject: [PATCH 3/5] up ver --- Cargo.toml | 2 +- README.md | 6 +++--- masterror-derive/Cargo.toml | 2 +- masterror-template/Cargo.toml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 60d0c9d..6bcbc9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "masterror" -version = "0.27.0" +version = "0.27.1" rust-version = "1.92" edition = "2024" license = "MIT" diff --git a/README.md b/README.md index df749cc..246baf3 100644 --- a/README.md +++ b/README.md @@ -159,9 +159,9 @@ The build script keeps the full feature snippet below in sync with ~~~toml [dependencies] -masterror = { version = "0.27.0", default-features = false } +masterror = { version = "0.27.1", default-features = false } # or with features: -# masterror = { version = "0.27.0", features = [ +# masterror = { version = "0.27.1", features = [ # "std", "axum", "actix", "openapi", # "serde_json", "tracing", "metrics", "backtrace", # "colored", "sqlx", "sqlx-migrate", "reqwest", @@ -640,7 +640,7 @@ Enable the `colored` feature for enhanced terminal output in local mode: ~~~toml [dependencies] -masterror = { version = "0.27.0", features = ["colored"] } +masterror = { version = "0.27.1", features = ["colored"] } ~~~ With `colored` enabled, errors display with syntax highlighting: diff --git a/masterror-derive/Cargo.toml b/masterror-derive/Cargo.toml index 677f091..ad4d16f 100644 --- a/masterror-derive/Cargo.toml +++ b/masterror-derive/Cargo.toml @@ -5,7 +5,7 @@ [package] name = "masterror-derive" rust-version = "1.92" -version = "0.11.0" +version = "0.11.1" edition = "2024" license = "MIT" repository = "https://github.com/RAprogramm/masterror" diff --git a/masterror-template/Cargo.toml b/masterror-template/Cargo.toml index a32c680..0381206 100644 --- a/masterror-template/Cargo.toml +++ b/masterror-template/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "masterror-template" -version = "0.4.0" +version = "0.4.1" rust-version = "1.92" edition = "2024" repository = "https://github.com/RAprogramm/masterror" From 1f4664ee5938ec49c287baea97f771ac048a3c06 Mon Sep 17 00:00:00 2001 From: RAprogramm Date: Tue, 30 Dec 2025 09:52:47 +0700 Subject: [PATCH 4/5] up ver --- Cargo.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index db7b813..e26ed9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1964,7 +1964,7 @@ checksum = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7" [[package]] name = "masterror" -version = "0.27.0" +version = "0.27.1" dependencies = [ "actix-web", "anyhow", @@ -1978,7 +1978,7 @@ dependencies = [ "log", "log-mdc", "masterror-derive 0.10.0", - "masterror-template 0.4.0", + "masterror-template 0.4.1", "metrics", "owo-colors", "redis", @@ -2019,9 +2019,9 @@ dependencies = [ [[package]] name = "masterror-derive" -version = "0.11.0" +version = "0.11.1" dependencies = [ - "masterror-template 0.4.0", + "masterror-template 0.4.1", "proc-macro2", "quote", "syn", @@ -2035,7 +2035,7 @@ checksum = "76cbb19c37caa0e505f0fb43a68184a4d22dc6e81daea40dd00fc24a356ffa9a" [[package]] name = "masterror-template" -version = "0.4.0" +version = "0.4.1" [[package]] name = "matchers" From 83cb01cba4a3ea92b106c699a1196d6652b26d63 Mon Sep 17 00:00:00 2001 From: RAprogramm Date: Tue, 30 Dec 2025 09:55:18 +0700 Subject: [PATCH 5/5] up deps --- Cargo.lock | 60 +++++++++++++++++++----------------------------------- Cargo.toml | 2 +- 2 files changed, 22 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e26ed9a..7e49c23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -326,9 +326,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", @@ -788,9 +788,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.2.0-rc.6" +version = "0.2.0-rc.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7fa010a85c7440677a0f4c59cf7ebabef52d7d8b4f79051e5fa60d3f0dd87d0" +checksum = "e6165b8029cdc3e765b74d3548f85999ee799d5124877ce45c2c85ca78e4d4aa" dependencies = [ "hybrid-array", ] @@ -986,13 +986,13 @@ dependencies = [ [[package]] name = "digest" -version = "0.11.0-rc.4" +version = "0.11.0-rc.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea390c940e465846d64775e55e3115d5dc934acb953de6f6e6360bc232fe2bf7" +checksum = "ebf9423bafb058e4142194330c52273c343f8a5beb7176d052f0e73b17dd35b9" dependencies = [ "block-buffer 0.11.0", "const-oid 0.10.1", - "crypto-common 0.2.0-rc.6", + "crypto-common 0.2.0-rc.8", "subtle", ] @@ -1481,7 +1481,7 @@ version = "0.13.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1c597ac7d6cc8143e30e83ef70915e7f883b18d8bec2e2b2bce47f5bbb06d57" dependencies = [ - "digest 0.11.0-rc.4", + "digest 0.11.0-rc.5", ] [[package]] @@ -1813,9 +1813,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ "memchr", "serde", @@ -1903,13 +1903,13 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags", "libc", - "redox_syscall 0.6.0", + "redox_syscall 0.7.0", ] [[package]] @@ -1977,8 +1977,8 @@ dependencies = [ "js-sys", "log", "log-mdc", - "masterror-derive 0.10.0", - "masterror-template 0.4.1", + "masterror-derive", + "masterror-template", "metrics", "owo-colors", "redis", @@ -2005,34 +2005,16 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "masterror-derive" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c67cb4a9c7ac7a6473b96e7ed3b72f755809334e1c5c44cf80211358d382d68" -dependencies = [ - "masterror-template 0.3.8", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "masterror-derive" version = "0.11.1" dependencies = [ - "masterror-template 0.4.1", + "masterror-template", "proc-macro2", "quote", "syn", ] -[[package]] -name = "masterror-template" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76cbb19c37caa0e505f0fb43a68184a4d22dc6e81daea40dd00fc24a356ffa9a" - [[package]] name = "masterror-template" version = "0.4.1" @@ -2522,9 +2504,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f59e70c4aef1e55797c2e8fd94a4f2a973fc972cfde0e0b05f683667b0cd39dd" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" [[package]] name = "potential_utf" @@ -2731,9 +2713,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec96166dafa0886eb81fe1c0a388bece180fbef2135f97c1e2cf8302e74b43b5" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" dependencies = [ "bitflags", ] @@ -3176,7 +3158,7 @@ checksum = "19d43dc0354d88b791216bb5c1bfbb60c0814460cc653ae0ebd71f286d0bd927" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.11.0-rc.4", + "digest 0.11.0-rc.5", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6bcbc9d..2ea21d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,7 +93,7 @@ masterror-derive = { version = "0.11" } masterror-template = { version = "0.4" } [dependencies] -masterror-derive = { version = "0.10" } +masterror-derive = { version = "0.11" } masterror-template = { workspace = true } tracing = { version = "0.1", optional = true, default-features = false, features = [ "attributes",