use quote::ToTokens;
use syn::{
Field,
Ident,
LitStr,
Meta,
Token,
};
use crate::{
field_mode::FieldMode,
sensitivity::Sensitivity,
};
#[must_use]
pub(crate) struct FieldAttributes {
mode: FieldMode,
}
impl FieldAttributes {
pub(crate) fn parse(
field: &Field,
type_name: &Ident,
field_name: &str,
require_explicit: bool,
) -> syn::Result<Self> {
let mut selected = None;
for attribute in &field.attrs {
if !attribute.path().is_ident("redact") {
continue;
}
let Meta::List(list) = &attribute.meta else {
return Err(field_error(
attribute,
type_name,
field_name,
"expected `#[redact(plain)]`, `#[redact(level = \"...\")]`, `#[redact(skip)]`, \
`#[redact(nested)]`, `#[redact(map)]`, or `#[redact(json)]`",
));
};
if list.tokens.is_empty() {
return Err(field_error(
attribute,
type_name,
field_name,
"empty `#[redact()]` is not allowed; choose `plain`, `level = \"...\"`, `skip`, \
`nested`, `map`, or `json`",
));
}
attribute.parse_nested_meta(|meta| {
let mode = parse_mode(&meta, type_name, field_name)?;
select_mode(&meta, type_name, field_name, &mut selected, mode)
})?;
}
let mode = match selected {
Some(mode) => mode,
None if require_explicit => {
return Err(field_error(
field,
type_name,
field_name,
"requires an explicit mode; use `plain`, `level = \"...\"`, `skip`, \
`nested`, `map`, or `json`",
));
}
None => FieldMode::Plain,
};
Ok(Self { mode })
}
#[inline(always)]
pub(crate) const fn mode(&self) -> &FieldMode {
&self.mode
}
}
fn parse_mode(
meta: &syn::meta::ParseNestedMeta<'_>,
type_name: &Ident,
field_name: &str,
) -> syn::Result<FieldMode> {
if meta.path.is_ident("plain") {
require_bare(
meta,
type_name,
field_name,
"bare `plain` without arguments",
)?;
Ok(FieldMode::Plain)
} else if meta.path.is_ident("level") {
if !meta.input.peek(Token![=]) {
return Err(meta.error(format!(
"Redact derive for `{type_name}` field `{field_name}` requires \
`level = \"low|medium|high|secret\"`",
)));
}
let literal: LitStr = meta.value()?.parse()?;
Ok(FieldMode::Level(Sensitivity::parse(
&literal, type_name, field_name,
)?))
} else if meta.path.is_ident("skip") {
require_bare(
meta,
type_name,
field_name,
"bare `skip` without arguments",
)?;
Ok(FieldMode::Skip)
} else if meta.path.is_ident("nested") {
require_bare(
meta,
type_name,
field_name,
"bare `nested` without arguments",
)?;
Ok(FieldMode::Nested)
} else if meta.path.is_ident("map") {
require_bare(
meta,
type_name,
field_name,
"bare `map` without arguments; map values are classified by runtime key \
and the complete policy",
)?;
Ok(FieldMode::Map)
} else if meta.path.is_ident("json") {
require_bare(
meta,
type_name,
field_name,
"bare `json` without arguments; JSON text is parsed and redacted by field key",
)?;
Ok(FieldMode::Json)
} else {
let key = meta.path.to_token_stream().to_string();
Err(meta.error(format!(
"Redact derive for `{type_name}` field `{field_name}` has unknown \
attribute `{key}`; use `plain`, `level = \"...\"`, `skip`, `nested`, `map`, or `json`",
)))
}
}
#[inline]
fn require_bare(
meta: &syn::meta::ParseNestedMeta<'_>,
type_name: &Ident,
field_name: &str,
requirement: &str,
) -> syn::Result<()> {
if meta.input.peek(Token![=]) || meta.input.peek(syn::token::Paren) {
Err(meta.error(format!(
"Redact derive for `{type_name}` field `{field_name}` requires {requirement}",
)))
} else {
Ok(())
}
}
#[inline]
fn select_mode(
meta: &syn::meta::ParseNestedMeta<'_>,
type_name: &Ident,
field_name: &str,
selected: &mut Option<FieldMode>,
mode: FieldMode,
) -> syn::Result<()> {
if selected.is_some() {
return Err(meta.error(format!(
"Redact derive for `{type_name}` field `{field_name}` has conflicting or \
repeated modes; choose exactly one of `plain`, `level = \"...\"`, `skip`, \
`nested`, `map`, or `json`; map values are classified by runtime key and the \
complete policy",
)));
}
*selected = Some(mode);
Ok(())
}
#[inline]
fn field_error(
tokens: impl ToTokens,
type_name: &Ident,
field_name: &str,
message: &str,
) -> syn::Error {
syn::Error::new_spanned(
tokens,
format!(
"Redact derive for `{type_name}` field `{field_name}`: {message}"
),
)
}