rust_rfc7807/lib.rs
1//! # rust-rfc7807
2//!
3//! [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807) Problem Details for HTTP APIs.
4//!
5//! This crate provides a lightweight, framework-agnostic [`Problem`] type that serializes
6//! to `application/problem+json` with safe defaults, an ergonomic builder API, and
7//! first-class support for validation errors, error codes, and trace correlation.
8//!
9//! Core dependencies: `serde` and `serde_json` only.
10//!
11//! # Creating a Problem
12//!
13//! ```
14//! use rust_rfc7807::Problem;
15//!
16//! let problem = Problem::not_found()
17//! .type_("https://api.example.com/problems/user-not-found")
18//! .title("User not found")
19//! .detail("No user with ID 42 exists")
20//! .instance("/users/42")
21//! .code("USER_NOT_FOUND")
22//! .trace_id("abc-123");
23//!
24//! let json = serde_json::to_value(&problem).unwrap();
25//! assert_eq!(json["status"], 404);
26//! assert_eq!(json["type"], "https://api.example.com/problems/user-not-found");
27//! assert_eq!(json["code"], "USER_NOT_FOUND");
28//! assert_eq!(json["trace_id"], "abc-123");
29//! ```
30//!
31//! # Validation Errors
32//!
33//! [`Problem::validation()`] returns a 422 with `type` set to `"validation_error"`.
34//! Use [`Problem::push_error`] and [`Problem::push_error_code`] to add field-level errors:
35//!
36//! ```
37//! use rust_rfc7807::Problem;
38//!
39//! let problem = Problem::validation()
40//! .push_error_code("email", "must be a valid email address", "INVALID_EMAIL")
41//! .push_error("name", "is required")
42//! .code("VALIDATION_ERROR");
43//!
44//! let json = serde_json::to_value(&problem).unwrap();
45//! assert_eq!(json["status"], 422);
46//! assert_eq!(json["type"], "validation_error");
47//!
48//! let errors = json["errors"].as_array().unwrap();
49//! assert_eq!(errors[0]["field"], "email");
50//! assert_eq!(errors[0]["code"], "INVALID_EMAIL");
51//! assert_eq!(errors[1]["field"], "name");
52//! ```
53//!
54//! # Security: Internal Causes Never Serialize
55//!
56//! For 5xx errors, [`Problem`] defaults to a generic public message. Use
57//! [`Problem::with_cause`] to attach a diagnostic error for server-side logging
58//! that is **never** included in JSON output:
59//!
60//! ```
61//! use rust_rfc7807::Problem;
62//!
63//! let problem = Problem::internal_server_error()
64//! .with_cause(std::io::Error::other("connection to db:5432 refused"));
65//!
66//! // Safe for clients — no internal details
67//! let json = serde_json::to_string(&problem).unwrap();
68//! assert!(!json.contains("db:5432"));
69//! assert!(json.contains("An unexpected error occurred."));
70//!
71//! // Available for server-side logging
72//! let cause = problem.internal_cause().unwrap();
73//! assert!(cause.to_string().contains("db:5432"));
74//! ```
75//!
76//! # Mapping Domain Errors
77//!
78//! Implement [`IntoProblem`] on your application's error types:
79//!
80//! ```
81//! use rust_rfc7807::{IntoProblem, Problem};
82//!
83//! enum AppError {
84//! UserNotFound(u64),
85//! }
86//!
87//! impl IntoProblem for AppError {
88//! fn into_problem(self) -> Problem {
89//! match self {
90//! AppError::UserNotFound(id) => Problem::not_found()
91//! .detail(format!("No user with ID {id}"))
92//! .code("USER_NOT_FOUND"),
93//! }
94//! }
95//! }
96//!
97//! let problem = AppError::UserNotFound(42).into_problem();
98//! assert_eq!(problem.status, Some(404));
99//! assert_eq!(problem.get_code(), Some("USER_NOT_FOUND"));
100//! ```
101//!
102//! # Extension Fields
103//!
104//! Arbitrary extension fields are flattened into the top-level JSON object:
105//!
106//! ```
107//! use rust_rfc7807::Problem;
108//!
109//! let problem = Problem::new(429)
110//! .code("RATE_LIMITED")
111//! .extension("retry_after", 30);
112//!
113//! let json = serde_json::to_value(&problem).unwrap();
114//! assert_eq!(json["retry_after"], 30);
115//! assert_eq!(json["code"], "RATE_LIMITED");
116//! ```
117//!
118//! # Axum Integration
119//!
120//! Enable the `axum` feature for [`IntoResponse`](axum_core::response::IntoResponse)
121//! on [`Problem`], or use the companion crate
122//! [`rust-rfc7807-axum`](https://docs.rs/rust-rfc7807-axum) for the full integration
123//! including [`ApiError`](https://docs.rs/rust-rfc7807-axum/latest/rust_rfc7807_axum/enum.ApiError.html).
124
125mod problem;
126mod traits;
127mod validation;
128
129#[cfg(feature = "axum")]
130mod axum_impl;
131
132pub use problem::Problem;
133pub use traits::IntoProblem;
134pub use validation::ValidationItem;
135
136/// The `Content-Type` header value for RFC 7807 problem responses.
137pub const APPLICATION_PROBLEM_JSON: &str = "application/problem+json";
138
139#[cfg(test)]
140mod tests;