redactable_derive/lib.rs
1//! Derive macros for `redactable`.
2//!
3//! This crate generates traversal code behind `#[derive(Sensitive)]`,
4//! `#[derive(SensitiveDisplay)]`, `#[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 quote::quote;
70use syn::{DeriveInput, parse_macro_input};
71
72mod container;
73mod crate_paths;
74mod debug_impl;
75mod derive_enum;
76mod derive_struct;
77mod fresh_ident;
78mod generics;
79mod not_sensitive;
80mod redacted_display;
81mod sensitive;
82mod strategy;
83mod transform;
84
85pub(crate) use crate_paths::{crate_path, crate_root};
86use not_sensitive::{expand_not_sensitive, expand_not_sensitive_display};
87pub(crate) use sensitive::DeriveOutput;
88use sensitive::{DeriveKind, expand, expand_with_mode};
89
90/// Derives `redactable::RedactableWithMapper` (and related impls) for structs and enums.
91///
92/// # Container Attributes
93///
94/// These attributes are placed on the struct/enum itself:
95///
96/// `Sensitive` and `SensitiveDisplay` are standalone derives. Use `SensitiveDual` when a type
97/// needs both structural and display redaction.
98///
99/// Use `#[redactable(recursive)]` on a field whose crate-qualified, aliased, or
100/// mutually recursive type would otherwise create a self-referential inferred
101/// bound. Unannotated fields retain their exact complete-type bounds.
102/// `#[redactable(legacy_formatting)]` and `#[redactable(generated_formatting)]`
103/// are display-only formatting options; standalone `Sensitive` rejects **both**.
104/// Apply them on `SensitiveDisplay` or `SensitiveDual` (which is what to use when
105/// a type needs structural and display redaction together); those derives
106/// document what each option selects.
107///
108/// # Field Attributes
109///
110/// - **No annotation**: The field is traversed by default. Scalars pass through unchanged; nested
111/// structs/enums are walked using `RedactableWithMapper` (so external types must implement it).
112///
113/// - `#[sensitive(Secret)]`: For scalar types (i32, bool, char, etc.), redacts to default values
114/// (0, false, '*'). For string-like types, applies full redaction to `"[REDACTED]"`.
115///
116/// - `#[sensitive(Policy)]`: Applies the policy's redaction rules to string-like
117/// values. Works for `String`, `Option<String>`, `Vec<String>`, `Box<String>`. Scalars can only
118/// use `#[sensitive(Secret)]`.
119///
120/// - `#[not_sensitive]`: Explicit passthrough - the field is not transformed at all. Use this
121/// for foreign types that don't implement `RedactableWithMapper`. This is equivalent to wrapping
122/// the field type in `NotSensitiveValue<T>`, but without changing the type signature.
123///
124/// Unions are rejected at compile time.
125///
126/// # Generated Impls
127///
128/// - `RedactableWithMapper`: always generated.
129/// - `Redactable`: always generated. Provides `.redact()` and certifies the type for the
130/// redacted-output extension traits (`RedactedOutputExt`, `RedactedJsonExt`, `SlogRedactedExt`).
131/// - `Debug`: redacted by default; actual values in the consumer's `cfg(test)` builds or when
132/// `redactable`'s `testing` feature is enabled.
133/// - `slog::Value` + `SlogRedacted` (requires `slog` feature): borrowed generated output is a
134/// fixed fail-closed placeholder and never clones or serializes the raw reference. Owned values
135/// can use `SlogRedactedExt::slog_redacted_json` for redact-then-serialize structured output.
136/// - `TracingRedacted` (requires `tracing` feature): marker trait.
137#[proc_macro_derive(Sensitive, attributes(sensitive, not_sensitive, redactable))]
138pub fn derive_sensitive_container(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
139 let input = parse_macro_input!(input as DeriveInput);
140 match expand(input, DeriveKind::Sensitive) {
141 Ok(tokens) => tokens.into(),
142 Err(err) => err.into_compile_error().into(),
143 }
144}
145
146/// Derives structural and display redaction as one authenticated expansion.
147///
148/// Use this instead of combining `Sensitive` and `SensitiveDisplay` with the
149/// legacy `#[sensitive(dual)]` coordination attribute.
150#[proc_macro_derive(SensitiveDual, attributes(sensitive, not_sensitive, redactable, error))]
151pub fn derive_sensitive_dual(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
152 let input = parse_macro_input!(input as DeriveInput);
153 let sensitive = expand_with_mode(input.clone(), DeriveKind::Sensitive, true);
154 let display = expand_with_mode(input, DeriveKind::SensitiveDisplay, true);
155 match (sensitive, display) {
156 (Ok(sensitive), Ok(display)) => quote!(#sensitive #display).into(),
157 (Err(mut first), Err(second)) => {
158 first.combine(second);
159 first.into_compile_error().into()
160 }
161 (Err(err), _) | (_, Err(err)) => err.into_compile_error().into(),
162 }
163}
164
165/// Derives a no-op `redactable::RedactableWithMapper` implementation, along with
166/// `slog::Value` / `SlogRedacted` and `TracingRedacted`.
167///
168/// This is useful for types that are known to be non-sensitive but still need to
169/// satisfy `RedactableWithMapper` / `Redactable` bounds. Because the type has no
170/// sensitive data, logging integration works without wrappers.
171///
172/// # Generated Impls
173///
174/// - `RedactableWithMapper`: no-op passthrough (the type has no sensitive data)
175/// - `Redactable`: deriving `NotSensitive` is an explicit declaration, so the type is
176/// certified for consuming and borrowed adapters. Generated slog serialization borrows rather
177/// than clones; serde's `RefCell` implementation
178/// reports an active mutable borrow as an error, which is converted to `"[REDACTED]"`.
179/// - `slog::Value` and `SlogRedacted` (behind `cfg(feature = "slog")`): serializes the explicitly
180/// non-sensitive value directly as structured JSON. Requires `Serialize` on the type.
181/// - `TracingRedacted` (behind `cfg(feature = "tracing")`): marker trait
182///
183/// `NotSensitive` does **not** generate a `Debug` impl - there's nothing to redact.
184/// Use `#[derive(Debug)]` when needed.
185///
186/// # Rejected Attributes
187///
188/// `#[sensitive]` and `#[not_sensitive]` attributes are rejected on both the container
189/// and its fields - the former is wrong (the type is explicitly non-sensitive), the
190/// latter is redundant (the entire type is already non-sensitive).
191///
192/// Unions are rejected at compile time.
193#[proc_macro_derive(NotSensitive, attributes(sensitive, not_sensitive))]
194pub fn derive_not_sensitive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
195 let input = parse_macro_input!(input as DeriveInput);
196 match expand_not_sensitive(input) {
197 Ok(tokens) => tokens.into(),
198 Err(err) => err.into_compile_error().into(),
199 }
200}
201
202/// Derives `redactable::RedactableWithFormatter` for types with no sensitive data.
203///
204/// This is the display counterpart to `NotSensitive`. Use it when you have a type
205/// with no sensitive data that needs logging integration (e.g., for use with slog).
206///
207/// Unlike `SensitiveDisplay`, this derive does **not** require a display template.
208/// Instead, it delegates directly to the type's existing `Display` implementation.
209///
210/// # Required Bounds
211///
212/// The type must implement `Display`. This is required because `RedactableWithFormatter` delegates
213/// to `Display::fmt`.
214///
215/// # Generated Impls
216///
217/// - `RedactableWithMapper`: no-op passthrough (allows use inside `Sensitive` containers)
218/// - `Redactable`: deriving `NotSensitiveDisplay` is an explicit declaration, so the type is
219/// certified for consuming and borrowed adapters.
220/// - `RedactableWithFormatter`: delegates to `Display::fmt`
221/// - `ToRedactedOutput`: emits the `Display` text; certifies the type for
222/// `slog_redacted_display()` and `tracing_redacted()`
223/// - `slog::Value` and `SlogRedacted` (behind `cfg(feature = "slog")`): uses `RedactableWithFormatter` output
224/// - `TracingRedacted` (behind `cfg(feature = "tracing")`): marker trait
225///
226/// # Debug
227///
228/// `NotSensitiveDisplay` does **not** generate a `Debug` impl - there's nothing to redact.
229/// Use `#[derive(Debug)]` alongside `NotSensitiveDisplay` when needed.
230///
231/// # Rejected Attributes
232///
233/// `#[sensitive]` and `#[not_sensitive]` attributes are rejected on both the container
234/// and its fields - the former is wrong (the type is explicitly non-sensitive), the
235/// latter is redundant (the entire type is already non-sensitive).
236///
237/// # Example
238///
239/// ```ignore
240/// use redactable::NotSensitiveDisplay;
241/// use std::fmt;
242///
243/// #[derive(NotSensitiveDisplay)]
244/// enum RetryDecision {
245/// Retry,
246/// Abort,
247/// }
248///
249/// impl fmt::Display for RetryDecision {
250/// fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
251/// formatter.write_str(match self {
252/// Self::Retry => "Retry",
253/// Self::Abort => "Abort",
254/// })
255/// }
256/// }
257///
258/// assert_eq!(RetryDecision::Retry.to_string(), "Retry");
259/// ```
260#[proc_macro_derive(NotSensitiveDisplay, attributes(sensitive, not_sensitive))]
261pub fn derive_not_sensitive_display(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
262 let input = parse_macro_input!(input as DeriveInput);
263 match expand_not_sensitive_display(input) {
264 Ok(tokens) => tokens.into(),
265 Err(err) => err.into_compile_error().into(),
266 }
267}
268
269/// Derives `redactable::RedactableWithFormatter` using a display template.
270///
271/// This generates a redacted string representation without requiring `Clone`.
272/// Unannotated fields use `RedactableWithFormatter` by default (passthrough for scalars,
273/// redacted display for nested `SensitiveDisplay` types).
274///
275/// # Field Annotations
276///
277/// - *(none)*: Uses `RedactableWithFormatter` (requires the field type to implement it)
278/// - `#[sensitive(Policy)]`: Apply the policy's redaction rules
279/// - `#[not_sensitive]`: Render raw via `Display` (use for types without `RedactableWithFormatter`)
280///
281/// The display template is taken from `#[error("...")]` (thiserror-style) or from
282/// doc comments (displaydoc-style). If neither is present, the derive fails.
283///
284/// Fields are redacted by reference, so field types do not need `Clone`.
285/// A custom `PolicyApplicableRef` leaf nested inside a container can explicitly
286/// select its ordinary borrowed projection with
287/// `#[redactable(legacy_formatting)]`. The explicit route does not require the
288/// direct-leaf formatting marker; it requires `PolicyApplicableRef` on the whole
289/// field and the selected format capability on its output. It inherits the
290/// projection's `Clone` requirements and borrow behavior; library-owned fields
291/// should stay on the default conflict-safe route. It composes with
292/// `#[redactable(recursive)]`, retaining the projection/output bounds while
293/// suppressing the cyclic inferred field bound.
294///
295/// `#[redactable(generated_formatting)]` instead selects the library-owned
296/// recursive formatter for an alias-hidden or otherwise ambiguous container
297/// field. `legacy_formatting` and `generated_formatting` are mutually exclusive,
298/// and standalone `Sensitive` rejects both (they only affect display output).
299///
300/// Use `SensitiveDual` instead when the same type also needs structural redaction.
301///
302/// # Generated Impls
303///
304/// - `RedactableWithFormatter`: always generated.
305/// - `ToRedactedOutput`: always generated; emits the redacted display text and certifies the
306/// type for `slog_redacted_display()` and `tracing_redacted()`.
307/// - `Debug`: redacted by default; actual values in the consumer's `cfg(test)` builds or when
308/// `redactable`'s `testing` feature is enabled.
309/// - `slog::Value` + `SlogRedacted`: emits the redacted display string (requires `slog` feature).
310/// - `TracingRedacted`: marker trait (requires `tracing` feature).
311#[proc_macro_derive(
312 SensitiveDisplay,
313 attributes(sensitive, not_sensitive, redactable, error)
314)]
315pub fn derive_sensitive_display(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
316 let input = parse_macro_input!(input as DeriveInput);
317 match expand(input, DeriveKind::SensitiveDisplay) {
318 Ok(tokens) => tokens.into(),
319 Err(err) => err.into_compile_error().into(),
320 }
321}
322
323#[cfg(all(test, feature = "slog"))]
324mod generated_dependency_tests;
325
326#[cfg(all(test, feature = "slog"))]
327#[test]
328fn structural_generated_dependency_roots() {
329 generated_dependency_tests::run_structural_generated_dependency_roots();
330}