use syn::{Attribute, Error, Ident, LitStr};
use crate::utils::skip_unknown_meta;
#[derive(Debug, Clone, Copy)]
pub(crate) enum Grammar {
Field,
Type,
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum NameSource<'a> {
Rename(&'a LitStr),
Ident(&'a Ident),
}
pub(crate) fn validate(
name: &str,
grammar: Grammar,
what: &str,
source: NameSource<'_>,
) -> Result<(), Error> {
if !grammar.accepts(name) {
let hint = match source {
NameSource::Rename(_) => "",
NameSource::Ident(_) => ", so name it explicitly with `#[zlink(rename = \"...\")]`",
};
let msg = format!(
"`{name}` is not a valid Varlink {what}: it must match `{}`{hint}",
grammar.pattern(),
);
return Err(match source {
NameSource::Rename(lit) => Error::new_spanned(lit, msg),
NameSource::Ident(ident) => Error::new_spanned(ident, msg),
});
}
Ok(())
}
pub(crate) fn validate_interface(lit: &LitStr) -> Result<(), Error> {
let name = lit.value();
if !zlink_idl::is_valid_interface_name(&name) {
return Err(Error::new_spanned(
lit,
format!(
"`{name}` is not a valid Varlink interface name: it must be in reverse-domain \
notation, e.g. `org.example.Foo`"
),
));
}
Ok(())
}
impl Grammar {
fn accepts(self, name: &str) -> bool {
match self {
Self::Field => zlink_idl::is_valid_field_name(name),
Self::Type => zlink_idl::is_valid_type_name(name),
}
}
fn pattern(self) -> &'static str {
match self {
Self::Field => "[A-Za-z][A-Za-z0-9_]*",
Self::Type => "[A-Z][A-Za-z0-9]*",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RenameAll {
Lower,
Upper,
Pascal,
Camel,
Snake,
ScreamingSnake,
Kebab,
ScreamingKebab,
}
impl RenameAll {
pub(crate) fn apply_to_field(self, field: &str) -> String {
match self {
Self::Lower | Self::Snake => field.to_owned(),
Self::Upper | Self::ScreamingSnake => field.to_ascii_uppercase(),
Self::Pascal => {
let mut pascal = String::new();
let mut capitalize = true;
for ch in field.chars() {
if ch == '_' {
capitalize = true;
} else if capitalize {
pascal.push(ch.to_ascii_uppercase());
capitalize = false;
} else {
pascal.push(ch);
}
}
pascal
}
Self::Camel => {
let pascal = Self::Pascal.apply_to_field(field);
match pascal.get(..1) {
Some(first) => first.to_ascii_lowercase() + &pascal[1..],
None => pascal,
}
}
Self::Kebab => field.replace('_', "-"),
Self::ScreamingKebab => Self::ScreamingSnake.apply_to_field(field).replace('_', "-"),
}
}
pub(crate) fn apply_to_variant(self, variant: &str) -> String {
match self {
Self::Pascal => variant.to_owned(),
Self::Lower => variant.to_ascii_lowercase(),
Self::Upper => variant.to_ascii_uppercase(),
Self::Camel => match variant.get(..1) {
Some(first) => first.to_ascii_lowercase() + &variant[1..],
None => variant.to_owned(),
},
Self::Snake => {
let mut snake = String::new();
for (i, ch) in variant.char_indices() {
if i > 0 && ch.is_uppercase() {
snake.push('_');
}
snake.push(ch.to_ascii_lowercase());
}
snake
}
Self::ScreamingSnake => Self::Snake.apply_to_variant(variant).to_ascii_uppercase(),
Self::Kebab => Self::Snake.apply_to_variant(variant).replace('_', "-"),
Self::ScreamingKebab => Self::ScreamingSnake
.apply_to_variant(variant)
.replace('_', "-"),
}
}
fn can_produce(self, grammar: Grammar) -> bool {
grammar.accepts(&self.apply_to_variant("Word"))
}
pub(crate) fn parse(lit: &LitStr) -> Result<Self, Error> {
let rule = match lit.value().as_str() {
"lowercase" => Self::Lower,
"UPPERCASE" => Self::Upper,
"PascalCase" => Self::Pascal,
"camelCase" => Self::Camel,
"snake_case" => Self::Snake,
"SCREAMING_SNAKE_CASE" => Self::ScreamingSnake,
"kebab-case" => Self::Kebab,
"SCREAMING-KEBAB-CASE" => Self::ScreamingKebab,
unknown => {
return Err(Error::new_spanned(
lit,
format!(
"`{unknown}` is not a known case convention, expected one of: {}",
VALID_RENAME_ALL.join(", "),
),
));
}
};
Ok(rule)
}
}
pub(crate) fn field_name(
attrs: &[Attribute],
ident: &Ident,
rename_all: Option<RenameAll>,
) -> Result<String, Error> {
resolve(
attrs,
ident,
rename_all,
RenameAll::apply_to_field,
Grammar::Field,
"field name",
)
}
#[cfg(feature = "introspection")]
pub(crate) fn enum_variant_name(
attrs: &[Attribute],
ident: &Ident,
rename_all: Option<RenameAll>,
) -> Result<String, Error> {
resolve(
attrs,
ident,
rename_all,
RenameAll::apply_to_variant,
Grammar::Field,
"enum variant name",
)
}
pub(crate) fn error_name(
attrs: &[Attribute],
ident: &Ident,
rename_all: Option<RenameAll>,
) -> Result<String, Error> {
resolve(
attrs,
ident,
rename_all,
RenameAll::apply_to_variant,
Grammar::Type,
"error name",
)
}
pub(crate) fn reject_container_rename(attrs: &[Attribute], msg: &str) -> Result<(), Error> {
match parse_rename(attrs)? {
Some(lit) => Err(Error::new_spanned(lit, msg)),
None => Ok(()),
}
}
pub(crate) fn parse_rename_all(
attrs: &[Attribute],
grammar: Grammar,
) -> Result<Option<RenameAll>, Error> {
let Some(lit) = parse_zlink_lit_str(attrs, "rename_all")? else {
return Ok(None);
};
let rule = RenameAll::parse(&lit)?;
if !rule.can_produce(grammar) {
return Err(Error::new_spanned(
&lit,
format!(
"`{}` can never produce a name matching the Varlink grammar `{}`, so it cannot \
apply here. Drop `rename_all`, use a convention that fits (e.g. `UPPERCASE` or \
`PascalCase`), or name items individually with `#[zlink(rename = \"...\")]`",
lit.value(),
grammar.pattern(),
),
));
}
Ok(Some(rule))
}
pub(crate) fn parse_rename(attrs: &[Attribute]) -> Result<Option<LitStr>, Error> {
parse_zlink_lit_str(attrs, "rename")
}
pub(crate) fn unraw(ident: &Ident) -> String {
let name = ident.to_string();
match name.strip_prefix("r#") {
Some(stripped) => stripped.to_owned(),
None => name,
}
}
fn resolve<F>(
attrs: &[Attribute],
ident: &Ident,
rename_all: Option<RenameAll>,
apply: F,
grammar: Grammar,
what: &str,
) -> Result<String, Error>
where
F: FnOnce(RenameAll, &str) -> String,
{
if let Some(lit) = parse_rename(attrs)? {
let name = lit.value();
validate(&name, grammar, what, NameSource::Rename(&lit))?;
return Ok(name);
}
let unrawed = unraw(ident);
let name = match rename_all {
Some(rule) => apply(rule, &unrawed),
None => unrawed,
};
validate(&name, grammar, what, NameSource::Ident(ident))?;
Ok(name)
}
fn parse_zlink_lit_str(attrs: &[Attribute], key: &str) -> Result<Option<LitStr>, Error> {
let mut result = None;
for attr in attrs {
if !attr.path().is_ident("zlink") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident(key) {
let lit: LitStr = meta.value()?.parse()?;
if result.is_some() {
return Err(meta.error(format!("duplicate `{key}` attribute")));
}
result = Some(lit);
} else {
skip_unknown_meta(&meta)?;
}
Ok(())
})?;
}
Ok(result)
}
const VALID_RENAME_ALL: &[&str] = &[
"lowercase",
"UPPERCASE",
"PascalCase",
"camelCase",
"snake_case",
"SCREAMING_SNAKE_CASE",
"kebab-case",
"SCREAMING-KEBAB-CASE",
];
#[cfg(test)]
mod tests {
use super::*;
use syn::parse_quote;
#[test]
fn field_conventions() {
let cases = [
(RenameAll::Lower, "user_name"),
(RenameAll::Upper, "USER_NAME"),
(RenameAll::Pascal, "UserName"),
(RenameAll::Camel, "userName"),
(RenameAll::Snake, "user_name"),
(RenameAll::ScreamingSnake, "USER_NAME"),
(RenameAll::Kebab, "user-name"),
(RenameAll::ScreamingKebab, "USER-NAME"),
];
for (rule, expected) in cases {
assert_eq!(rule.apply_to_field("user_name"), expected, "rule: {rule:?}");
}
}
#[test]
fn variant_conventions() {
let cases = [
(RenameAll::Lower, "username"),
(RenameAll::Upper, "USERNAME"),
(RenameAll::Pascal, "UserName"),
(RenameAll::Camel, "userName"),
(RenameAll::Snake, "user_name"),
(RenameAll::ScreamingSnake, "USER_NAME"),
(RenameAll::Kebab, "user-name"),
(RenameAll::ScreamingKebab, "USER-NAME"),
];
for (rule, expected) in cases {
assert_eq!(
rule.apply_to_variant("UserName"),
expected,
"rule: {rule:?}"
);
}
}
#[test]
fn rename_beats_rename_all() {
let attrs: Vec<Attribute> = vec![parse_quote!(#[zlink(rename = "ID")])];
let ident: Ident = parse_quote!(user_id);
let name = field_name(&attrs, &ident, Some(RenameAll::Camel)).unwrap();
assert_eq!(name, "ID");
}
#[test]
fn rename_all_applies_without_rename() {
let attrs: Vec<Attribute> = vec![];
let ident: Ident = parse_quote!(user_id);
let name = field_name(&attrs, &ident, Some(RenameAll::Camel)).unwrap();
assert_eq!(name, "userId");
}
#[test]
fn ident_used_without_any_attr() {
let attrs: Vec<Attribute> = vec![];
let ident: Ident = parse_quote!(user_id);
assert_eq!(field_name(&attrs, &ident, None).unwrap(), "user_id");
}
#[test]
fn unraw_strips_the_prefix() {
let ident: Ident = parse_quote!(r#type);
assert_eq!(unraw(&ident), "type");
}
#[test]
fn unraw_leaves_a_normal_ident_unchanged() {
let ident: Ident = parse_quote!(user_id);
assert_eq!(unraw(&ident), "user_id");
}
#[test]
fn rename_all_parsed_alongside_other_keys() {
let attrs: Vec<Attribute> =
vec![parse_quote!(#[zlink(crate = "crate", rename_all = "camelCase")])];
assert_eq!(
parse_rename_all(&attrs, Grammar::Field).unwrap(),
Some(RenameAll::Camel)
);
}
#[test]
fn unknown_rename_all_value_rejected() {
let attrs: Vec<Attribute> = vec![parse_quote!(#[zlink(rename_all = "bogus")])];
let err = parse_rename_all(&attrs, Grammar::Field)
.unwrap_err()
.to_string();
assert!(
err.contains("bogus"),
"message should name the bad value: {err}"
);
assert!(
err.contains("camelCase"),
"message should list valid values: {err}"
);
}
#[test]
fn container_rename_rejected_with_message() {
let attrs: Vec<Attribute> = vec![parse_quote!(#[zlink(rename = "Foo")])];
let err = reject_container_rename(&attrs, "nope")
.unwrap_err()
.to_string();
assert_eq!(err, "nope");
}
#[test]
fn raw_ident_field_resolves_then_validates() {
let attrs: Vec<Attribute> = vec![];
let ident: Ident = parse_quote!(r#type);
assert_eq!(field_name(&attrs, &ident, None).unwrap(), "type");
}
#[test]
fn raw_ident_error_variant_resolves_then_validates() {
let attrs: Vec<Attribute> = vec![];
let ident: Ident = parse_quote!(r#Fn);
assert_eq!(error_name(&attrs, &ident, None).unwrap(), "Fn");
}
#[cfg(feature = "introspection")]
#[test]
fn enum_variant_lowercase_is_accepted() {
let attrs: Vec<Attribute> = vec![];
let ident: Ident = parse_quote!(Active);
assert_eq!(
enum_variant_name(&attrs, &ident, Some(RenameAll::Lower)).unwrap(),
"active"
);
}
#[test]
fn error_name_lowercase_is_rejected() {
let attrs: Vec<Attribute> = vec![];
let ident: Ident = parse_quote!(Active);
let err = error_name(&attrs, &ident, Some(RenameAll::Lower))
.unwrap_err()
.to_string();
assert!(err.contains("`active`"), "must name the bad name: {err}");
assert!(err.contains("error name"), "must name the context: {err}");
}
#[test]
fn invalid_rename_reports_the_literal_not_the_ident() {
let attrs: Vec<Attribute> = vec![parse_quote!(#[zlink(rename = "not valid!")])];
let ident: Ident = parse_quote!(good_field);
let err = field_name(&attrs, &ident, None).unwrap_err().to_string();
assert!(err.contains("`not valid!`"), "must quote the rename: {err}");
assert!(
!err.contains("good_field"),
"must not blame the ident: {err}"
);
assert!(
!err.contains("`#[zlink(rename"),
"no hint on a rename: {err}"
);
}
#[test]
fn invalid_ident_suggests_rename() {
let attrs: Vec<Attribute> = vec![];
let ident: Ident = parse_quote!(_foo);
let err = field_name(&attrs, &ident, None).unwrap_err().to_string();
assert!(err.contains("`_foo`"), "must name the bad ident: {err}");
assert!(err.contains("rename"), "must offer the escape hatch: {err}");
}
#[test]
fn interface_names_validated() {
let good: LitStr = parse_quote!("org.example.Foo");
assert!(validate_interface(&good).is_ok());
let bad: LitStr = parse_quote!("org.example.bad_name");
let err = validate_interface(&bad).unwrap_err().to_string();
assert!(
err.contains("`org.example.bad_name`"),
"must quote it: {err}"
);
assert!(
err.contains("interface name"),
"must name the context: {err}"
);
}
#[test]
fn rename_all_that_can_sometimes_fit_the_type_grammar_is_accepted() {
let attrs: Vec<Attribute> =
vec![parse_quote!(#[zlink(rename_all = "SCREAMING_SNAKE_CASE")])];
assert_eq!(
parse_rename_all(&attrs, Grammar::Type).unwrap(),
Some(RenameAll::ScreamingSnake)
);
}
#[test]
fn rename_all_that_never_fits_the_type_grammar_is_rejected() {
let attrs: Vec<Attribute> = vec![parse_quote!(#[zlink(rename_all = "snake_case")])];
let err = parse_rename_all(&attrs, Grammar::Type)
.unwrap_err()
.to_string();
assert!(
err.contains("`snake_case`"),
"must name the convention: {err}"
);
assert!(
err.contains("[A-Z][A-Za-z0-9]*"),
"must name the grammar it cannot meet: {err}"
);
}
#[test]
fn every_rename_all_fits_the_field_grammar() {
for value in VALID_RENAME_ALL {
let attr: Attribute = parse_quote!(#[zlink(rename_all = #value)]);
assert!(
parse_rename_all(&[attr], Grammar::Field).unwrap().is_some(),
"`{value}` should be accepted for a field name"
);
}
}
}