Skip to main content

redactable_derive/
lib.rs

1//! Derive macros for `redactable`.
2//!
3//! This crate generates traversal code behind `#[derive(Sensitive)]`,
4//! `#[derive(SensitiveDisplay)]`, `#[derive(SensitiveDual)]`, `#[derive(NotSensitive)]`, and
5//! `#[derive(NotSensitiveDisplay)]`. It:
6//! - reads `#[sensitive(...)]` and `#[not_sensitive]` attributes
7//! - emits trait implementations for redaction and logging integration
8//!
9//! It does **not** define policy markers or text policies. Those live in the main
10//! `redactable` crate and are applied at runtime.
11
12// <https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html>
13#![warn(
14    anonymous_parameters,
15    bare_trait_objects,
16    elided_lifetimes_in_paths,
17    missing_copy_implementations,
18    rust_2018_idioms,
19    trivial_casts,
20    trivial_numeric_casts,
21    unreachable_pub,
22    unsafe_code,
23    unused_extern_crates,
24    unused_import_braces
25)]
26// <https://rust-lang.github.io/rust-clippy/stable>
27#![warn(
28    clippy::all,
29    clippy::cargo,
30    clippy::dbg_macro,
31    clippy::float_cmp_const,
32    clippy::get_unwrap,
33    clippy::mem_forget,
34    clippy::nursery,
35    clippy::pedantic,
36    clippy::todo,
37    clippy::unwrap_used,
38    clippy::uninlined_format_args
39)]
40// Allow some clippy lints
41#![allow(
42    clippy::default_trait_access,
43    clippy::doc_markdown,
44    clippy::if_not_else,
45    clippy::module_name_repetitions,
46    clippy::multiple_crate_versions,
47    clippy::must_use_candidate,
48    clippy::needless_pass_by_value,
49    clippy::needless_ifs,
50    clippy::use_self,
51    clippy::cargo_common_metadata,
52    clippy::missing_errors_doc,
53    clippy::enum_glob_use,
54    clippy::struct_excessive_bools,
55    clippy::missing_const_for_fn,
56    clippy::redundant_pub_crate,
57    clippy::result_large_err,
58    clippy::future_not_send,
59    clippy::option_if_let_else,
60    clippy::from_over_into,
61    clippy::manual_inspect
62)]
63// Allow some lints while testing
64#![cfg_attr(test, allow(clippy::non_ascii_literal, clippy::unwrap_used))]
65
66#[allow(unused_extern_crates)]
67extern crate proc_macro;
68
69use proc_macro::TokenStream;
70use syn::{DeriveInput, parse_macro_input};
71
72mod container;
73mod crate_paths;
74mod declaration;
75mod derive_enum;
76mod derive_struct;
77mod fresh_ident;
78mod generics;
79mod not_sensitive;
80mod output;
81mod redacted_display;
82mod sensitive;
83mod strategy;
84mod transform;
85
86pub(crate) use crate_paths::{crate_path, crate_root};
87use not_sensitive::{expand_not_sensitive, expand_not_sensitive_display};
88pub(crate) use sensitive::DeriveOutput;
89use sensitive::{Expansion, expand};
90
91/// Derives `redactable::RedactableWithMapper` (and related impls) for structs and enums.
92///
93/// `Sensitive` and `SensitiveDisplay` are standalone derives. Use `SensitiveDual` when a type
94/// needs both structural and display redaction.
95///
96/// # Recursive Fields
97///
98/// Use `#[redactable(recursive)]` on a field whose crate-qualified, aliased, or
99/// mutually recursive type would otherwise create a self-referential inferred
100/// bound. Unannotated fields retain their exact complete-type bounds.
101///
102/// # Field Attributes
103///
104/// - **No annotation**: The field requires declared `Redactable` behavior and is traversed
105///   with the supplied mapper. Raw leaves need a policy or an explicit public declaration.
106///
107/// - `#[sensitive(Secret)]`: For scalar types (i32, bool, char, etc.), redacts to default values
108///   (0, false, '*'). For string-like types, applies full redaction to `"[REDACTED]"`.
109///
110/// - `#[sensitive(Policy)]`: Applies the policy's redaction rules to string-like
111///   values. Works for `String`, `Option<String>`, `Vec<String>`, `Box<String>`. Scalars can only
112///   use `#[sensitive(Secret)]`.
113///
114/// - `#[not_sensitive]`: Explicit passthrough - the field is not transformed at all. Use this
115///   for foreign types that don't implement `RedactableWithMapper`. This is the right
116///   declaration for a foreign field in a struct you own; `BypassRedaction<T>` is only
117///   for satisfying a `Redactable` bound on a value you cannot annotate.
118///
119/// Field operations are checked against the original declaration bounds, even
120/// when no generated method is called. For an unannotated generic field `T`,
121/// declare `T: Redactable`; complete container bounds may be declared instead.
122/// Policy fields require the corresponding complete-type `PolicyField<P>` bound.
123/// The recursion override omits inferred recursive predicates, but still checks
124/// the actual field operations.
125///
126/// Unions are rejected at compile time.
127///
128/// # Generated Impls
129///
130/// - `ToRedacted`: always generated. It clones, redacts and serializes, producing a
131///   `RedactedValue` that carries redacted JSON. This is why `Sensitive` requires
132///   `Clone` and `serde::Serialize` on the type; a missing bound is reported on the
133///   generated impl.
134/// - `RedactableWithMapper`: always generated.
135/// - `Redactable`: always generated. Provides `.redact()` and allows the type
136///   inside `Sensitive` containers.
137/// - `Debug`: uses the production redacted representation in every build mode.
138/// - `slog::Value` + `SlogRedacted` (requires `slog` feature): borrowed generated output is a
139///   fixed fail-closed placeholder and never clones or serializes the raw reference. Owned values
140///   can use `SlogRedactedExt::slog_redacted_json` for redact-then-serialize structured output.
141/// - `TracingRedacted` (requires `tracing` feature): marker trait.
142#[proc_macro_derive(Sensitive, attributes(sensitive, not_sensitive, redactable))]
143pub fn derive_sensitive_container(input: TokenStream) -> TokenStream {
144    let input = parse_macro_input!(input as DeriveInput);
145    match expand(input, Expansion::Sensitive) {
146        Ok(tokens) => tokens.into(),
147        Err(err) => err.into_compile_error().into(),
148    }
149}
150
151/// Derives structural redaction and redacted template text for the same type.
152///
153/// Use this instead of combining `Sensitive` and `SensitiveDisplay` with the
154/// legacy `#[sensitive(dual)]` coordination attribute. Every structural field is checked,
155/// including fields omitted from the template. Its single `ToRedacted` impl carries
156/// both representations: the template text and the redacted JSON. It therefore
157/// requires `Clone` and `serde::Serialize` as well as a template.
158#[proc_macro_derive(SensitiveDual, attributes(sensitive, not_sensitive, redactable, error))]
159pub fn derive_sensitive_dual(input: TokenStream) -> TokenStream {
160    let input = parse_macro_input!(input as DeriveInput);
161    match expand(input, Expansion::Dual) {
162        Ok(tokens) => tokens.into(),
163        Err(err) => err.into_compile_error().into(),
164    }
165}
166
167/// Derives a no-op `redactable::RedactableWithMapper` implementation, along with
168/// `slog::Value` / `SlogRedacted` and `TracingRedacted`.
169///
170/// This is useful for types that are known to be non-sensitive but still need to
171/// satisfy `RedactableWithMapper` / `Redactable` bounds. Because the type has no
172/// sensitive data, logging integration works without wrappers.
173///
174/// # Generated Impls
175///
176/// - `RedactableWithMapper`: no-op passthrough (the type has no sensitive data)
177/// - `Redactable`: declares the type public and allows it inside `Sensitive` containers.
178/// - `ToRedacted`: always generated; emits the raw `Serialize` output the author
179///   declared public. This is why `NotSensitive` requires `serde::Serialize`; `Clone`
180///   is not required, because nothing is redacted.
181/// - `slog::Value` and `SlogRedacted` (behind `cfg(feature = "slog")`): serializes the explicitly
182///   non-sensitive value directly as structured JSON. Requires `Serialize` on the type.
183///   Serialization borrows the value without cloning it. Serde reports an active
184///   mutable `RefCell` borrow as an error, which becomes `"[REDACTED]"`.
185/// - `TracingRedacted` (behind `cfg(feature = "tracing")`): marker trait
186///
187/// `NotSensitive` does **not** generate a `Debug` impl - there's nothing to redact.
188/// Use `#[derive(Debug)]` when needed.
189///
190/// # Rejected Attributes
191///
192/// `#[sensitive]` and `#[not_sensitive]` attributes are rejected on both the container
193/// and its fields - the former is wrong (the type is explicitly non-sensitive), the
194/// latter is redundant (the entire type is already non-sensitive).
195///
196/// Unions are rejected at compile time.
197#[proc_macro_derive(NotSensitive, attributes(sensitive, not_sensitive, redactable))]
198pub fn derive_not_sensitive(input: TokenStream) -> TokenStream {
199    let input = parse_macro_input!(input as DeriveInput);
200    match expand_not_sensitive(input) {
201        Ok(tokens) => tokens.into(),
202        Err(err) => err.into_compile_error().into(),
203    }
204}
205
206/// Derives `redactable::RedactableWithFormatter` for types with no sensitive data.
207///
208/// This is the display counterpart to `NotSensitive`. Use it when you have a type
209/// with no sensitive data that needs logging integration (e.g., for use with slog).
210///
211/// Unlike `SensitiveDisplay`, this derive does **not** require a display template.
212/// Instead, it delegates directly to the type's existing `Display` implementation.
213///
214/// # Required Bounds
215///
216/// The type must implement `Display`. This is required because `RedactableWithFormatter` delegates
217/// to `Display::fmt`.
218///
219/// # Generated Impls
220///
221/// - `RedactableWithMapper`: no-op passthrough (allows use inside `Sensitive` containers)
222/// - `Redactable`: declares the type public and allows it inside `Sensitive` containers.
223/// - `RedactableWithFormatter`: delegates to `Display::fmt`
224/// - `ToRedacted`: emits the `Display` text for `slog_redacted()` and `tracing_redacted()`
225/// - `slog::Value` and `SlogRedacted` (behind `cfg(feature = "slog")`): uses `RedactableWithFormatter` output
226/// - `TracingRedacted` (behind `cfg(feature = "tracing")`): marker trait
227///
228/// # Debug
229///
230/// `NotSensitiveDisplay` does **not** generate a `Debug` impl - there's nothing to redact.
231/// Use `#[derive(Debug)]` alongside `NotSensitiveDisplay` when needed.
232///
233/// # Rejected Attributes
234///
235/// `#[sensitive]` and `#[not_sensitive]` attributes are rejected on both the container
236/// and its fields - the former is wrong (the type is explicitly non-sensitive), the
237/// latter is redundant (the entire type is already non-sensitive).
238///
239/// # Example
240///
241/// ```ignore
242/// use redactable::NotSensitiveDisplay;
243/// use std::fmt::{Display, Formatter, Result as FmtResult};
244///
245/// #[derive(NotSensitiveDisplay)]
246/// enum RetryDecision {
247///     Retry,
248///     Abort,
249/// }
250///
251/// impl Display for RetryDecision {
252///     fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
253///         formatter.write_str(match self {
254///             Self::Retry => "Retry",
255///             Self::Abort => "Abort",
256///         })
257///     }
258/// }
259///
260/// assert_eq!(RetryDecision::Retry.to_string(), "Retry");
261/// ```
262#[proc_macro_derive(NotSensitiveDisplay, attributes(sensitive, not_sensitive, redactable))]
263pub fn derive_not_sensitive_display(input: TokenStream) -> TokenStream {
264    let input = parse_macro_input!(input as DeriveInput);
265    match expand_not_sensitive_display(input) {
266        Ok(tokens) => tokens.into(),
267        Err(err) => err.into_compile_error().into(),
268    }
269}
270
271/// Derives `redactable::RedactableWithFormatter` using a display template.
272///
273/// This generates a redacted string representation by borrowing the source.
274/// The text/secret route needs no `Clone`; IP-map formatting clones keys and the HashMap hasher.
275/// Referenced unannotated fields require declared formatting, such as a nested
276/// `SensitiveDisplay` type. Unreferenced fields and constant templates remain supported.
277///
278/// # Field Annotations
279///
280/// - *(none)*: Uses `RedactableWithFormatter` and the public
281///   `redactable::DeclaredFormatting` declaration
282/// - `#[sensitive(Policy)]`: Apply the policy's redaction rules
283/// - `#[not_sensitive]`: Render raw via `Display` (use for types without `RedactableWithFormatter`)
284///
285/// The display template is taken from `#[error("...")]` (thiserror-style) or from
286/// doc comments (displaydoc-style). If neither is present, the derive fails.
287///
288/// # Policy Formatting
289///
290/// A custom leaf supports policy formatting by implementing
291/// `redactable::PolicyFormat`. Supported containers forward
292/// to their contents. A nested `RefCell` borrow conflict renders as `<borrowed>`.
293///
294/// Generic policy fields declare `PolicyDisplay<P>` for `{value}`,
295/// `PolicyDebug<P>` for `{value:?}`, or both when both modes are used.
296/// The same requirements apply to concrete policy fields.
297/// These policy-specific bounds permit supported scalars and typed IP addresses
298/// without requiring structural `Redactable` or `Clone`. Missing capabilities
299/// reject the declaration even when no formatting method is called.
300///
301/// Use `SensitiveDual` instead when the same type also needs structural redaction.
302/// Its declarations must satisfy both structural and template capabilities.
303///
304/// # Generated Impls
305///
306/// - `RedactableWithFormatter`: always generated.
307/// - `ToRedacted`: always generated; emits the redacted display text for
308///   `slog_redacted()` and `tracing_redacted()`.
309/// - `Debug`: uses the production redacted representation in every build mode.
310/// - `slog::Value` + `SlogRedacted`: emits the redacted display string (requires `slog` feature).
311/// - `TracingRedacted`: marker trait (requires `tracing` feature).
312#[proc_macro_derive(
313    SensitiveDisplay,
314    attributes(sensitive, not_sensitive, redactable, error)
315)]
316pub fn derive_sensitive_display(input: TokenStream) -> TokenStream {
317    let input = parse_macro_input!(input as DeriveInput);
318    match expand(input, Expansion::SensitiveDisplay) {
319        Ok(tokens) => tokens.into(),
320        Err(err) => err.into_compile_error().into(),
321    }
322}
323
324#[cfg(all(test, feature = "slog"))]
325mod generated_dependency_tests;
326
327#[cfg(all(test, feature = "slog"))]
328#[test]
329fn structural_generated_dependency_roots() {
330    generated_dependency_tests::run_structural_generated_dependency_roots();
331}