use proc_macro2::TokenStream;
use quote::quote;
use crate::CodeGenConfig;
pub(crate) const JSON_FEATURE: &str = "json";
pub(crate) const VIEWS_FEATURE: &str = "views";
pub(crate) const TEXT_FEATURE: &str = "text";
pub(crate) const REFLECT_FEATURE: &str = "reflect";
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct FeatureGateNames {
pub json: String,
pub views: String,
pub text: String,
pub reflect: String,
}
impl FeatureGateNames {
#[must_use]
pub fn is_valid_name(name: &str) -> bool {
is_valid_feature_name(name)
}
}
impl Default for FeatureGateNames {
fn default() -> Self {
Self {
json: JSON_FEATURE.to_string(),
views: VIEWS_FEATURE.to_string(),
text: TEXT_FEATURE.to_string(),
reflect: REFLECT_FEATURE.to_string(),
}
}
}
pub(crate) fn is_valid_feature_name(name: &str) -> bool {
let mut chars = name.chars();
chars
.next()
.is_some_and(|c| c.is_ascii_alphanumeric() || c == '_')
&& chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '+' | '.'))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) struct FeatureGates<'a> {
pub(crate) json: Option<&'a str>,
pub(crate) views: Option<&'a str>,
pub(crate) text: Option<&'a str>,
pub(crate) reflect: Option<&'a str>,
}
impl<'a> FeatureGates<'a> {
pub(crate) fn for_config(config: &'a CodeGenConfig) -> Self {
let gate_all = config.gate_impls_on_crate_features;
let gate_reflect = gate_all || config.gate_reflect_on_crate_feature;
let names = &config.feature_gate_names;
Self {
json: (gate_all && config.generate_json).then_some(names.json.as_str()),
views: (gate_all && config.generate_views).then_some(names.views.as_str()),
text: (gate_all && config.generate_text).then_some(names.text.as_str()),
reflect: (gate_reflect && config.generate_reflection).then_some(names.reflect.as_str()),
}
}
pub(crate) fn validate(&self) -> Result<(), (&'static str, &'a str)> {
[
("json", self.json),
("views", self.views),
("text", self.text),
("reflect", self.reflect),
]
.into_iter()
.filter_map(|(kind, name)| Some((kind, name?)))
.try_for_each(|(kind, name)| {
if is_valid_feature_name(name) {
Ok(())
} else {
Err((kind, name))
}
})
}
pub(crate) fn json_or_text(&self) -> Vec<&'a str> {
let mut v = Vec::with_capacity(2);
if let Some(f) = self.json {
v.push(f);
}
if let Some(f) = self.text {
if self.json != Some(f) {
v.push(f);
}
}
v
}
}
pub(crate) fn cfg_block(tokens: TokenStream, gate: Option<&str>) -> TokenStream {
match gate {
Some(feature) if !tokens.is_empty() => {
debug_assert!(
syn::parse2::<syn::Item>(tokens.clone()).is_ok()
|| syn::parse2::<syn::Stmt>(tokens.clone()).is_ok(),
"cfg_block applied to a token stream that is not a single item/statement; \
trailing siblings would leak ungated. Use cfg_const_block. tokens: {tokens}"
);
quote! {
#[cfg(feature = #feature)]
#tokens
}
}
_ => tokens,
}
}
pub(crate) fn cfg_block_any(tokens: TokenStream, gates: &[&str]) -> TokenStream {
match gates {
[] => tokens,
[single] => cfg_block(tokens, Some(single)),
many if !tokens.is_empty() => {
let preds = many.iter().map(|f| quote! { feature = #f });
quote! {
#[cfg(any(#(#preds),*))]
#tokens
}
}
_ => tokens,
}
}
pub(crate) fn cfg_const_block(tokens: TokenStream, gate: Option<&str>) -> TokenStream {
match gate {
Some(feature) if !tokens.is_empty() => quote! {
#[cfg(feature = #feature)]
const _: () = {
#tokens
};
},
_ => tokens,
}
}
pub(crate) fn cfg_attr(attr_body: TokenStream, gate: Option<&str>) -> TokenStream {
if attr_body.is_empty() {
return TokenStream::new();
}
match gate {
Some(feature) => quote! { #[cfg_attr(feature = #feature, #attr_body)] },
None => quote! { #[#attr_body] },
}
}
#[cfg(test)]
mod tests {
use super::*;
fn gated_config() -> CodeGenConfig {
CodeGenConfig {
generate_json: true,
generate_views: true,
generate_text: true,
gate_impls_on_crate_features: true,
..CodeGenConfig::default()
}
}
#[test]
fn for_config_off_by_default() {
let config = CodeGenConfig {
generate_json: true,
generate_views: true,
generate_text: true,
..CodeGenConfig::default()
};
assert_eq!(FeatureGates::for_config(&config), FeatureGates::default());
}
#[test]
fn for_config_gates_only_enabled_kinds() {
let config = CodeGenConfig {
generate_json: true,
generate_views: false,
generate_text: false,
gate_impls_on_crate_features: true,
..CodeGenConfig::default()
};
let gates = FeatureGates::for_config(&config);
assert_eq!(gates.json, Some(JSON_FEATURE));
assert_eq!(gates.views, None);
assert_eq!(gates.text, None);
}
#[test]
fn for_config_all_gated() {
let config = gated_config();
let gates = FeatureGates::for_config(&config);
assert_eq!(gates.json, Some(JSON_FEATURE));
assert_eq!(gates.views, Some(VIEWS_FEATURE));
assert_eq!(gates.text, Some(TEXT_FEATURE));
assert_eq!(gates.json_or_text(), vec![JSON_FEATURE, TEXT_FEATURE]);
}
#[test]
fn for_config_reflect_only_gate() {
let config = CodeGenConfig {
generate_json: true,
generate_views: true,
generate_text: true,
generate_reflection: true,
gate_reflect_on_crate_feature: true,
..CodeGenConfig::default()
};
let gates = FeatureGates::for_config(&config);
assert_eq!(gates.json, None);
assert_eq!(gates.views, None);
assert_eq!(gates.text, None);
assert_eq!(gates.reflect, Some(REFLECT_FEATURE));
}
#[test]
fn for_config_reflect_gate_requires_generate_reflection() {
let config = CodeGenConfig {
generate_reflection: false,
gate_reflect_on_crate_feature: true,
..CodeGenConfig::default()
};
assert_eq!(FeatureGates::for_config(&config).reflect, None);
}
#[test]
fn for_config_umbrella_gate_includes_reflect() {
let config = CodeGenConfig {
generate_reflection: true,
gate_impls_on_crate_features: true,
..CodeGenConfig::default()
};
assert_eq!(
FeatureGates::for_config(&config).reflect,
Some(REFLECT_FEATURE)
);
}
#[test]
fn for_config_custom_names() {
let config = CodeGenConfig {
feature_gate_names: FeatureGateNames {
json: "serde".to_string(),
views: "zero-copy".to_string(),
text: "textproto".to_string(),
reflect: "reflection".to_string(),
},
generate_reflection: true,
..gated_config()
};
let gates = FeatureGates::for_config(&config);
assert_eq!(gates.json, Some("serde"));
assert_eq!(gates.views, Some("zero-copy"));
assert_eq!(gates.text, Some("textproto"));
assert_eq!(gates.reflect, Some("reflection"));
assert_eq!(gates.json_or_text(), vec!["serde", "textproto"]);
}
#[test]
fn custom_names_inert_without_gating() {
let config = CodeGenConfig {
generate_json: true,
feature_gate_names: FeatureGateNames {
json: "serde".to_string(),
..FeatureGateNames::default()
},
..CodeGenConfig::default()
};
assert_eq!(FeatureGates::for_config(&config), FeatureGates::default());
}
#[test]
fn json_or_text_dedups_shared_name() {
let shared = FeatureGates {
json: Some("serde"),
text: Some("serde"),
..Default::default()
};
assert_eq!(shared.json_or_text(), vec!["serde"]);
}
#[test]
fn public_validator_matches_internal_rule() {
for name in ["json", "zero-copy", "", "-leading", "with space"] {
assert_eq!(
FeatureGateNames::is_valid_name(name),
is_valid_feature_name(name)
);
}
}
#[test]
fn feature_name_validity() {
assert!(is_valid_feature_name("json"));
assert!(is_valid_feature_name("zero-copy"));
assert!(is_valid_feature_name("a_b.c+d2"));
assert!(is_valid_feature_name("_private"));
assert!(!is_valid_feature_name(""));
assert!(!is_valid_feature_name("with space"));
assert!(!is_valid_feature_name("quo\"te"));
assert!(!is_valid_feature_name("-leading"));
assert!(!is_valid_feature_name(".leading"));
assert!(!is_valid_feature_name("+leading"));
}
#[test]
fn validate_reports_first_invalid_active_name() {
let config = CodeGenConfig {
feature_gate_names: FeatureGateNames {
views: String::new(),
..FeatureGateNames::default()
},
..gated_config()
};
assert_eq!(
FeatureGates::for_config(&config).validate(),
Err(("views", ""))
);
}
#[test]
fn validate_ignores_inactive_invalid_names() {
let config = CodeGenConfig {
feature_gate_names: FeatureGateNames {
reflect: "not valid".to_string(),
..FeatureGateNames::default()
},
..gated_config() };
let gates = FeatureGates::for_config(&config);
assert_eq!(gates.reflect, None);
assert_eq!(gates.validate(), Ok(()));
}
#[test]
fn default_names_match_constants() {
let names = FeatureGateNames::default();
assert_eq!(names.json, JSON_FEATURE);
assert_eq!(names.views, VIEWS_FEATURE);
assert_eq!(names.text, TEXT_FEATURE);
assert_eq!(names.reflect, REFLECT_FEATURE);
}
#[test]
fn json_or_text_subsets() {
let none = FeatureGates::default();
assert!(none.json_or_text().is_empty());
let json_only = FeatureGates {
json: Some(JSON_FEATURE),
..Default::default()
};
assert_eq!(json_only.json_or_text(), vec![JSON_FEATURE]);
let text_only = FeatureGates {
text: Some(TEXT_FEATURE),
..Default::default()
};
assert_eq!(text_only.json_or_text(), vec![TEXT_FEATURE]);
}
#[test]
fn cfg_block_any_dispatches_by_arity() {
let inner = quote! { pub fn f() {} };
assert_eq!(
cfg_block_any(inner.clone(), &[]).to_string(),
inner.to_string()
);
assert_eq!(
cfg_block_any(inner.clone(), &["json"]).to_string(),
quote! { #[cfg(feature = "json")] pub fn f() {} }.to_string()
);
assert_eq!(
cfg_block_any(inner.clone(), &["json", "text"]).to_string(),
quote! { #[cfg(any(feature = "json", feature = "text"))] pub fn f() {} }.to_string()
);
assert!(cfg_block_any(TokenStream::new(), &["json", "text"]).is_empty());
}
#[test]
#[should_panic(expected = "cfg_block applied to a token stream that is not a single item")]
#[cfg(debug_assertions)]
fn cfg_block_rejects_multiple_siblings() {
cfg_block(quote! { struct A; struct B; }, Some("json"));
}
#[test]
fn cfg_block_wraps_when_gated() {
let inner = quote! { impl Foo for Bar {} };
let wrapped = cfg_block(inner.clone(), Some("json"));
assert_eq!(
wrapped.to_string(),
quote! { #[cfg(feature = "json")] impl Foo for Bar {} }.to_string()
);
assert_eq!(
cfg_block(inner.clone(), None).to_string(),
inner.to_string()
);
assert!(cfg_block(TokenStream::new(), Some("json")).is_empty());
}
#[test]
fn cfg_const_block_wraps_siblings() {
let inner = quote! { impl A for X {} impl B for X {} };
let wrapped = cfg_const_block(inner.clone(), Some("json"));
assert_eq!(
wrapped.to_string(),
quote! {
#[cfg(feature = "json")]
const _: () = { impl A for X {} impl B for X {} };
}
.to_string()
);
assert_eq!(
cfg_const_block(inner.clone(), None).to_string(),
inner.to_string()
);
assert!(cfg_const_block(TokenStream::new(), Some("json")).is_empty());
}
#[test]
fn cfg_attr_wraps_when_gated() {
let body = quote! { derive(::serde::Serialize) };
assert_eq!(
cfg_attr(body.clone(), Some("json")).to_string(),
quote! { #[cfg_attr(feature = "json", derive(::serde::Serialize))] }.to_string()
);
assert_eq!(
cfg_attr(body.clone(), None).to_string(),
quote! { #[derive(::serde::Serialize)] }.to_string()
);
assert!(cfg_attr(TokenStream::new(), Some("json")).is_empty());
assert!(cfg_attr(TokenStream::new(), None).is_empty());
}
}