use quote::ToTokens;
use syn::Error;
use syn::Field;
use syn::Ident;
use syn::LitStr;
use syn::Meta;
use syn::Result;
use syn::Token;
use syn::meta::ParseNestedMeta;
use syn::token::Paren;
use crate::model::FieldMode;
use crate::model::Sensitivity;
#[must_use]
pub(crate) struct FieldAttributes {
mode: FieldMode,
}
impl FieldAttributes {
pub(crate) fn parse(field: &Field, type_name: &Ident, field_name: &str) -> Result<Self> {
let mut selected = None;
let mut display = false;
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(level = \"...\")]`, `#[redact(skip)]`, \
`#[redact(nested)]`, `#[redact(map)]`, \
`#[redact(keyed_by = key)]`, or `#[redact(json)]`",
));
};
if list.tokens.is_empty() {
return Err(field_error(
attribute,
type_name,
field_name,
"empty `#[redact()]` is not allowed; choose `level = \"...\"`, `skip`, \
`nested`, `map`, `keyed_by = key`, or `json`",
));
}
attribute.parse_nested_meta(|meta| {
if meta.path.is_ident("display") {
require_bare(&meta, type_name, field_name, "bare `display` without arguments")?;
if display {
return Err(meta.error("duplicate `display` field adapter"));
}
display = true;
return Ok(());
}
let mode = parse_mode(&meta, type_name, field_name)?;
select_mode(&meta, type_name, field_name, &mut selected, mode)
})?;
}
if matches!(selected, Some(FieldMode::MapLevels { key: None, .. })) {
return Err(field_error(
field,
type_name,
field_name,
"`map_value_level` requires `map_key_level` in the same attribute",
));
}
let mode = selected.unwrap_or(FieldMode::Unmarked);
let mode = if display {
match mode {
FieldMode::Level(level) => FieldMode::DisplayLevel(level),
_ => {
return Err(field_error(
field,
type_name,
field_name,
"`display` requires an explicit `level` and cannot be combined with another mode",
));
}
}
} else {
mode
};
Ok(Self { mode })
}
#[inline(always)]
pub(crate) const fn mode(&self) -> &FieldMode {
&self.mode
}
}
fn parse_mode(meta: &ParseNestedMeta<'_>, type_name: &Ident, field_name: &str) -> Result<FieldMode> {
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("map_key_level") || meta.path.is_ident("map_value_level") {
if !meta.input.peek(Token![=]) {
return Err(meta.error(format!(
"Redact derive for `{type_name}` field `{field_name}` requires a sensitivity string",
)));
}
let literal: LitStr = meta.value()?.parse()?;
let level = Sensitivity::parse(&literal, type_name, field_name)?;
Ok(if meta.path.is_ident("map_key_level") {
FieldMode::MapLevels {
key: Some(level),
value: None,
}
} else {
FieldMode::MapLevels {
key: None,
value: Some(level),
}
})
} else if meta.path.is_ident("keyed_by") {
if !meta.input.peek(Token![=]) {
return Err(meta.error(format!(
"Redact derive for `{type_name}` field `{field_name}` requires \
`keyed_by = sibling_field`",
)));
}
let key: Ident = meta.value()?.parse()?;
Ok(FieldMode::KeyedBy(key))
} 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 `level = \"...\"`, `skip`, `nested`, `map`, \
`keyed_by = key`, or `json`",
)))
}
}
#[inline]
fn require_bare(meta: &ParseNestedMeta<'_>, type_name: &Ident, field_name: &str, requirement: &str) -> Result<()> {
if meta.input.peek(Token![=]) || meta.input.peek(Paren) {
Err(meta.error(format!(
"Redact derive for `{type_name}` field `{field_name}` requires {requirement}",
)))
} else {
Ok(())
}
}
#[inline]
fn select_mode(
meta: &ParseNestedMeta<'_>,
type_name: &Ident,
field_name: &str,
selected: &mut Option<FieldMode>,
mode: FieldMode,
) -> Result<()> {
if let (
Some(FieldMode::MapLevels { key, value }),
FieldMode::MapLevels {
key: new_key,
value: new_value,
},
) = (selected.as_mut(), &mode)
{
if (key.is_some() && new_key.is_some()) || (value.is_some() && new_value.is_some()) {
return Err(meta.error(format!(
"Redact derive for `{type_name}` field `{field_name}` repeats a map sensitivity",
)));
}
*key = key.or(*new_key);
*value = value.or(*new_value);
return Ok(());
}
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 `level = \"...\"`, `skip`, \
`nested`, `map`, `keyed_by = key`, or `json`; map and keyed 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) -> Error {
Error::new_spanned(
tokens,
format!("Redact derive for `{type_name}` field `{field_name}`: {message}"),
)
}