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, Copy, PartialEq, Eq, Default)]
pub(crate) struct FeatureGates {
pub(crate) json: Option<&'static str>,
pub(crate) views: Option<&'static str>,
pub(crate) text: Option<&'static str>,
pub(crate) reflect: Option<&'static str>,
}
impl FeatureGates {
pub(crate) fn for_config(config: &CodeGenConfig) -> Self {
let gate_all = config.gate_impls_on_crate_features;
let gate_reflect = gate_all || config.gate_reflect_on_crate_feature;
Self {
json: (gate_all && config.generate_json).then_some(JSON_FEATURE),
views: (gate_all && config.generate_views).then_some(VIEWS_FEATURE),
text: (gate_all && config.generate_text).then_some(TEXT_FEATURE),
reflect: (gate_reflect && config.generate_reflection).then_some(REFLECT_FEATURE),
}
}
pub(crate) fn json_or_text(&self) -> Vec<&'static str> {
let mut v = Vec::with_capacity(2);
if let Some(f) = self.json {
v.push(f);
}
if let Some(f) = self.text {
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 gates = FeatureGates::for_config(&gated_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 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());
}
}