use proc_macro2::{Span, TokenStream as TokenStream2, TokenTree};
use quote::{quote, quote_spanned};
use unsynn::*;
keyword! {
KCratePath = "crate_path";
KEnumName = "enum_name";
KVariants = "variants";
KName = "name";
KRest = "rest";
KUnit = "unit";
KNewtype = "newtype";
KNewtypeStr = "newtype_str";
KNewtypeOptChar = "newtype_opt_char";
KNewtypeI64 = "newtype_i64";
KNewtypeUsize = "newtype_usize";
KRec = "rec";
KArbitrary = "arbitrary";
KMakeT = "make_t";
KPredicate = "predicate";
KValidator = "validator";
KFnPtr = "fn_ptr";
KShapeType = "shape_type";
KOptStr = "opt_str";
}
operator! {
At = "@";
Col = ":";
}
unsynn! {
struct DispatchAttrInput {
crate_path_section: CratePathSection,
enum_name_section: EnumNameSection,
variants_section: VariantsSection,
name_section: NameSection,
rest_section: RestSection,
}
struct CratePathSection {
_at: At,
_kw: KCratePath,
content: BraceGroup,
}
struct EnumNameSection {
_at: At,
_kw: KEnumName,
content: BraceGroupContaining<Ident>,
}
struct VariantsSection {
_at: At,
_kw: KVariants,
content: BraceGroupContaining<CommaDelimitedVec<VariantDef>>,
}
struct NameSection {
_at: At,
_kw: KName,
content: BraceGroupContaining<Ident>,
}
struct RestSection {
_at: At,
_kw: KRest,
content: BraceGroup,
}
struct VariantDef {
name: Ident,
_colon: Col,
kind: VariantKindDef,
}
enum VariantKindDef {
Unit(KUnit),
Newtype(KNewtype),
NewtypeStr(KNewtypeStr),
NewtypeOptChar(KNewtypeOptChar),
NewtypeI64(KNewtypeI64),
NewtypeUsize(KNewtypeUsize),
Struct(StructVariantDef),
Arbitrary(KArbitrary),
MakeT(KMakeT),
Predicate(KPredicate),
Validator(KValidator),
FnPtr(KFnPtr),
ShapeType(KShapeType),
OptStr(KOptStr),
}
struct StructVariantDef {
_rec: KRec,
struct_name: Ident,
fields: BraceGroup,
}
}
struct ParsedDispatchInput {
crate_path: TokenStream2,
#[allow(dead_code)]
enum_name: Ident,
variants: Vec<ParsedVariant>,
attr_name: Ident,
rest: TokenStream2,
}
#[derive(Clone)]
struct ParsedVariant {
name: Ident,
kind: ParsedVariantKind,
}
#[derive(Clone)]
enum ParsedVariantKind {
Unit,
Newtype,
NewtypeStr,
NewtypeOptChar,
NewtypeI64,
NewtypeUsize,
Struct {
struct_name: Ident,
fields: Vec<ParsedFieldDef>,
},
Arbitrary,
MakeT,
Predicate,
Validator,
FnPtr,
ShapeType,
OptStr,
}
#[derive(Clone)]
struct ParsedFieldDef {
name: Ident,
kind: FieldKind,
#[allow(dead_code)]
doc: Option<String>,
}
#[derive(Clone, Copy)]
enum FieldKind {
Bool,
String,
OptString,
OptBool,
OptChar,
I64,
OptI64,
ListString,
ListI64,
Ident,
}
impl DispatchAttrInput {
fn to_parsed(&self) -> std::result::Result<ParsedDispatchInput, String> {
let crate_path = self.crate_path_section.content.0.stream();
let enum_name = self.enum_name_section.content.content.clone();
let variants: std::result::Result<Vec<_>, _> = self
.variants_section
.content
.content
.iter()
.map(|d| d.value.to_parsed())
.collect();
let attr_name = self.name_section.content.content.clone();
let rest = self.rest_section.content.0.stream();
Ok(ParsedDispatchInput {
crate_path,
enum_name,
variants: variants?,
attr_name,
rest,
})
}
}
impl VariantDef {
fn to_parsed(&self) -> std::result::Result<ParsedVariant, String> {
let kind = match &self.kind {
VariantKindDef::Unit(_) => ParsedVariantKind::Unit,
VariantKindDef::Newtype(_) => ParsedVariantKind::Newtype,
VariantKindDef::NewtypeStr(_) => ParsedVariantKind::NewtypeStr,
VariantKindDef::NewtypeOptChar(_) => ParsedVariantKind::NewtypeOptChar,
VariantKindDef::NewtypeI64(_) => ParsedVariantKind::NewtypeI64,
VariantKindDef::NewtypeUsize(_) => ParsedVariantKind::NewtypeUsize,
VariantKindDef::Arbitrary(_) => ParsedVariantKind::Arbitrary,
VariantKindDef::MakeT(_) => ParsedVariantKind::MakeT,
VariantKindDef::Predicate(_) => ParsedVariantKind::Predicate,
VariantKindDef::Validator(_) => ParsedVariantKind::Validator,
VariantKindDef::FnPtr(_) => ParsedVariantKind::FnPtr,
VariantKindDef::ShapeType(_) => ParsedVariantKind::ShapeType,
VariantKindDef::OptStr(_) => ParsedVariantKind::OptStr,
VariantKindDef::Struct(s) => {
let fields = parse_fields_with_docs(&s.fields.0.stream())?;
ParsedVariantKind::Struct {
struct_name: s.struct_name.clone(),
fields,
}
}
};
Ok(ParsedVariant {
name: self.name.clone(),
kind,
})
}
}
fn parse_fields_with_docs(
tokens: &TokenStream2,
) -> std::result::Result<Vec<ParsedFieldDef>, String> {
let tokens: Vec<TokenTree> = tokens.clone().into_iter().collect();
let mut fields = Vec::new();
let mut i = 0;
let mut current_doc: Option<String> = None;
while i < tokens.len() {
if let TokenTree::Punct(p) = &tokens[i]
&& p.as_char() == ','
{
i += 1;
continue;
}
if let TokenTree::Punct(p) = &tokens[i]
&& p.as_char() == '#'
&& i + 1 < tokens.len()
&& let TokenTree::Group(g) = &tokens[i + 1]
&& g.delimiter() == proc_macro2::Delimiter::Bracket
&& let Some(doc) = extract_doc_from_attr(&g.stream())
{
let trimmed = doc.trim();
if let Some(existing) = &mut current_doc {
existing.push(' ');
existing.push_str(trimmed);
} else {
current_doc = Some(trimmed.to_string());
}
i += 2;
continue;
}
let name = match &tokens[i] {
TokenTree::Ident(ident) => ident.clone(),
other => return Err(format!("expected field name, found `{other}`")),
};
i += 1;
if i >= tokens.len() {
return Err(format!("expected `:` after field name `{name}`"));
}
if let TokenTree::Punct(p) = &tokens[i] {
if p.as_char() != ':' {
return Err(format!(
"expected `:` after field name `{name}`, found `{p}`"
));
}
} else {
return Err(format!("expected `:` after field name `{name}`"));
}
i += 1;
if i >= tokens.len() {
return Err(format!("expected field kind after `{name}:`"));
}
let kind_ident = match &tokens[i] {
TokenTree::Ident(ident) => ident.clone(),
other => return Err(format!("expected field kind, found `{other}`")),
};
i += 1;
let kind_str = kind_ident.to_string();
let kind = match kind_str.as_str() {
"bool" => FieldKind::Bool,
"string" => FieldKind::String,
"opt_string" => FieldKind::OptString,
"opt_bool" => FieldKind::OptBool,
"opt_char" => FieldKind::OptChar,
"i64" => FieldKind::I64,
"opt_i64" => FieldKind::OptI64,
"list_string" => FieldKind::ListString,
"list_i64" => FieldKind::ListI64,
"ident" => FieldKind::Ident,
_ => return Err(format!("unknown field kind: {kind_str}")),
};
fields.push(ParsedFieldDef {
name,
kind,
doc: current_doc.take(),
});
}
Ok(fields)
}
fn unescape_string(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\\' {
match chars.next() {
Some('\\') => out.push('\\'),
Some('"') => out.push('"'),
Some('\'') => out.push('\''),
Some('n') => out.push('\n'),
Some('r') => out.push('\r'),
Some('t') => out.push('\t'),
Some('0') => out.push('\0'),
Some(other) => {
out.push('\\');
out.push(other);
}
None => out.push('\\'),
}
} else {
out.push(c);
}
}
out
}
fn extract_doc_from_attr(tokens: &TokenStream2) -> Option<String> {
let tokens: Vec<TokenTree> = tokens.clone().into_iter().collect();
if tokens.len() >= 3
&& let TokenTree::Ident(ident) = &tokens[0]
&& *ident == "doc"
&& let TokenTree::Punct(p) = &tokens[1]
&& p.as_char() == '='
&& let TokenTree::Literal(lit) = &tokens[2]
{
let lit_str = lit.to_string();
if lit_str.starts_with('"') && lit_str.ends_with('"') {
let inner = &lit_str[1..lit_str.len() - 1];
return Some(unescape_string(inner.trim_start()));
}
}
None
}
pub fn dispatch_attr(input: TokenStream2) -> TokenStream2 {
let mut iter = input.to_token_iter();
let parsed_input: DispatchAttrInput = match iter.parse() {
Ok(i) => i,
Err(e) => {
let msg = e.to_string();
return quote! { compile_error!(#msg); };
}
};
let input = match parsed_input.to_parsed() {
Ok(i) => i,
Err(e) => {
return quote! { compile_error!(#e); };
}
};
let crate_path = &input.crate_path;
let attr_name = &input.attr_name;
let attr_name_str = attr_name.to_string();
let attr_span = attr_name.span();
let rest = &input.rest;
for variant in &input.variants {
let variant_snake = to_snake_case(&variant.name.to_string());
if variant_snake == attr_name_str {
let variant_name = &variant.name;
let variant_pascal = to_pascal_case(&variant_name.to_string());
let variant_ident = proc_macro2::Ident::new(&variant_pascal, variant_name.span());
return match &variant.kind {
ParsedVariantKind::Unit => {
generate_unit_value(crate_path, &variant_ident, attr_name, rest, attr_span)
}
ParsedVariantKind::Newtype => {
generate_newtype_value(crate_path, &variant_ident, attr_name, rest, attr_span)
}
ParsedVariantKind::NewtypeStr => {
quote_spanned!(attr_span =>
compile_error!("Internal error: newtype_str attributes should be handled directly in __attr, not through __dispatch_attr")
)
}
ParsedVariantKind::NewtypeOptChar => generate_newtype_opt_char_value(
crate_path,
&variant_ident,
attr_name,
rest,
attr_span,
),
ParsedVariantKind::Arbitrary => {
generate_arbitrary_value(crate_path, &variant_ident, attr_name, rest, attr_span)
}
ParsedVariantKind::MakeT => {
generate_make_t_value(crate_path, &variant_ident, attr_name, rest, attr_span)
}
ParsedVariantKind::FnPtr => {
generate_fn_ptr_value(crate_path, &variant_ident, attr_name, rest, attr_span)
}
ParsedVariantKind::Predicate => {
quote_spanned!(attr_span =>
compile_error!("Internal error: predicate attributes should be handled directly in __attr, not through __dispatch_attr")
)
}
ParsedVariantKind::Validator => {
quote_spanned!(attr_span =>
compile_error!("Internal error: validator attributes should be handled directly in __attr, not through __dispatch_attr")
)
}
ParsedVariantKind::OptStr => {
quote_spanned!(attr_span =>
compile_error!("Internal error: opt_str attributes should be handled directly in __attr, not through __dispatch_attr")
)
}
ParsedVariantKind::NewtypeI64 => {
quote_spanned!(attr_span =>
compile_error!("Internal error: newtype_i64 attributes should be handled directly in __attr, not through __dispatch_attr")
)
}
ParsedVariantKind::NewtypeUsize => {
quote_spanned!(attr_span =>
compile_error!("Internal error: newtype_usize attributes should be handled directly in __attr, not through __dispatch_attr")
)
}
ParsedVariantKind::ShapeType => generate_shape_type_value(
crate_path,
&variant_ident,
attr_name,
rest,
attr_span,
),
ParsedVariantKind::Struct {
struct_name,
fields,
} => generate_struct_value(
crate_path,
&variant_ident,
struct_name,
fields,
attr_name,
rest,
attr_span,
),
};
}
}
let known_names: Vec<_> = input
.variants
.iter()
.map(|v| to_snake_case(&v.name.to_string()))
.collect();
let suggestion = find_closest(&attr_name_str, &known_names);
let msg = if let Some(s) = suggestion {
format!("unknown attribute `{attr_name_str}`; did you mean `{s}`?")
} else {
format!(
"unknown attribute `{}`; expected one of: {}",
attr_name_str,
known_names.join(", ")
)
};
let expanded = quote_spanned! { attr_span =>
compile_error!(#msg)
};
expanded
}
fn to_pascal_case(s: &str) -> String {
let mut result = String::new();
let mut capitalize_next = true;
for c in s.chars() {
if c == '_' {
capitalize_next = true;
} else if capitalize_next {
result.push(c.to_ascii_uppercase());
capitalize_next = false;
} else {
result.push(c);
}
}
result
}
fn to_snake_case(s: &str) -> String {
let mut result = String::new();
for (i, c) in s.chars().enumerate() {
if c.is_uppercase() {
if i > 0 {
result.push('_');
}
result.push(c.to_ascii_lowercase());
} else {
result.push(c);
}
}
result
}
fn generate_unit_value(
ns_path: &TokenStream2,
variant_ident: &proc_macro2::Ident,
attr_name: &Ident,
rest: &TokenStream2,
span: Span,
) -> TokenStream2 {
let rest_tokens: Vec<TokenTree> = rest.clone().into_iter().collect();
if rest_tokens.is_empty() {
return quote_spanned! { span =>
#ns_path::Attr::#variant_ident
};
}
if let Some(TokenTree::Group(g)) = rest_tokens.first()
&& g.delimiter() == proc_macro2::Delimiter::Parenthesis
&& g.stream().is_empty()
{
return quote_spanned! { span =>
#ns_path::Attr::#variant_ident
};
}
let msg = format!("`{attr_name}` does not take arguments; use just `{attr_name}`");
quote_spanned! { span =>
compile_error!(#msg)
}
}
fn generate_newtype_value(
ns_path: &TokenStream2,
variant_ident: &proc_macro2::Ident,
attr_name: &Ident,
rest: &TokenStream2,
span: Span,
) -> TokenStream2 {
let rest_tokens: Vec<TokenTree> = rest.clone().into_iter().collect();
let attr_str = attr_name.to_string();
if let Some(TokenTree::Group(g)) = rest_tokens.first()
&& g.delimiter() == proc_macro2::Delimiter::Parenthesis
{
let inner: Vec<TokenTree> = g.stream().into_iter().collect();
if inner.len() == 1
&& let TokenTree::Literal(lit) = &inner[0]
{
let lit_str = lit.to_string();
if lit_str.starts_with('\"') {
return quote_spanned! { span =>
#ns_path::Attr::#variant_ident(#lit)
};
}
}
let msg = format!("`{attr_str}` expects a string literal: `{attr_str}(\"name\")`");
return quote_spanned! { span =>
compile_error!(#msg)
};
}
if rest_tokens.len() == 1
&& let TokenTree::Literal(lit) = &rest_tokens[0]
{
let lit_str = lit.to_string();
if lit_str.starts_with('\"') {
return quote_spanned! { span =>
#ns_path::Attr::#variant_ident(#lit)
};
}
}
if rest_tokens.len() >= 2
&& let TokenTree::Punct(p) = &rest_tokens[0]
&& p.as_char() == '='
{
if let TokenTree::Literal(lit) = &rest_tokens[1] {
let lit_str = lit.to_string();
if lit_str.starts_with('\"') {
return quote_spanned! { span =>
#ns_path::Attr::#variant_ident(#lit)
};
}
}
let msg = format!("`{attr_str}` expects a string literal: `{attr_str} = \"name\"`");
return quote_spanned! { span =>
compile_error!(#msg)
};
}
let msg = format!(
"`{attr_str}` requires a string value: `{attr_str}(\"name\")` or `{attr_str} = \"name\"`"
);
quote_spanned! { span =>
compile_error!(#msg)
}
}
fn generate_arbitrary_value(
ns_path: &TokenStream2,
variant_ident: &proc_macro2::Ident,
_attr_name: &Ident,
rest: &TokenStream2,
span: Span,
) -> TokenStream2 {
let rest_tokens: Vec<TokenTree> = rest.clone().into_iter().collect();
if let Some(TokenTree::Group(g)) = rest_tokens.first()
&& g.delimiter() == proc_macro2::Delimiter::Parenthesis
{
let inner = g.stream();
if !inner.is_empty() {
return quote_spanned! { span =>
#ns_path::Attr::#variant_ident(𝟋Some(#inner))
};
}
return quote_spanned! { span =>
#ns_path::Attr::#variant_ident(𝟋None)
};
}
if !rest_tokens.is_empty() {
let value_tokens: TokenStream2 = if let Some(TokenTree::Punct(p)) = rest_tokens.first() {
if p.as_char() == '=' {
rest_tokens[1..].iter().cloned().collect()
} else {
rest.clone()
}
} else {
rest.clone()
};
if !value_tokens.is_empty() {
return quote_spanned! { span =>
#ns_path::Attr::#variant_ident(𝟋Some(#value_tokens))
};
}
}
quote_spanned! { span =>
#ns_path::Attr::#variant_ident(𝟋None)
}
}
fn generate_make_t_value(
ns_path: &TokenStream2,
variant_ident: &proc_macro2::Ident,
_attr_name: &Ident,
rest: &TokenStream2,
span: Span,
) -> TokenStream2 {
let rest_tokens: Vec<TokenTree> = rest.clone().into_iter().collect();
if let Some(TokenTree::Group(g)) = rest_tokens.first()
&& g.delimiter() == proc_macro2::Delimiter::Parenthesis
{
let inner = g.stream();
if !inner.is_empty() {
return quote_spanned! { span =>
#ns_path::Attr::#variant_ident(𝟋Some(
|__ptr| unsafe { __ptr.put(#inner) }
))
};
}
return quote_spanned! { span =>
#ns_path::Attr::#variant_ident(𝟋None)
};
}
if !rest_tokens.is_empty() {
let value_tokens: TokenStream2 = if let Some(TokenTree::Punct(p)) = rest_tokens.first() {
if p.as_char() == '=' {
rest_tokens[1..].iter().cloned().collect()
} else {
rest.clone()
}
} else {
rest.clone()
};
if !value_tokens.is_empty() {
return quote_spanned! { span =>
#ns_path::Attr::#variant_ident(𝟋Some(
|__ptr| unsafe { __ptr.put(#value_tokens) }
))
};
}
}
quote_spanned! { span =>
#ns_path::Attr::#variant_ident(𝟋None)
}
}
fn generate_fn_ptr_value(
ns_path: &TokenStream2,
variant_ident: &proc_macro2::Ident,
_attr_name: &Ident,
rest: &TokenStream2,
span: Span,
) -> TokenStream2 {
let rest_tokens: Vec<TokenTree> = rest.clone().into_iter().collect();
if let Some(TokenTree::Group(g)) = rest_tokens.first()
&& g.delimiter() == proc_macro2::Delimiter::Parenthesis
{
let inner = g.stream();
if !inner.is_empty() {
return quote_spanned! { span =>
#ns_path::Attr::#variant_ident(𝟋Some(#inner))
};
}
return quote_spanned! { span =>
#ns_path::Attr::#variant_ident(𝟋None)
};
}
if !rest_tokens.is_empty() {
let value_tokens: TokenStream2 = if let Some(TokenTree::Punct(p)) = rest_tokens.first() {
if p.as_char() == '=' {
rest_tokens[1..].iter().cloned().collect()
} else {
rest.clone()
}
} else {
rest.clone()
};
if !value_tokens.is_empty() {
return quote_spanned! { span =>
#ns_path::Attr::#variant_ident(𝟋Some(#value_tokens))
};
}
}
quote_spanned! { span =>
#ns_path::Attr::#variant_ident(𝟋None)
}
}
fn generate_shape_type_value(
_ns_path: &TokenStream2,
_variant_ident: &proc_macro2::Ident,
attr_name: &Ident,
rest: &TokenStream2,
span: Span,
) -> TokenStream2 {
let rest_tokens: Vec<TokenTree> = rest.clone().into_iter().collect();
let attr_str = attr_name.to_string();
if let Some(TokenTree::Group(g)) = rest_tokens.first()
&& g.delimiter() == proc_macro2::Delimiter::Parenthesis
{
let inner = g.stream();
if !inner.is_empty() {
return quote_spanned! { span =>
<#inner as ::facet::Facet>::SHAPE
};
}
let msg = format!("`{attr_str}` requires a type: `{attr_str}(MyType)`");
return quote_spanned! { span =>
compile_error!(#msg)
};
}
if !rest_tokens.is_empty() {
let type_tokens: TokenStream2 = if let Some(TokenTree::Punct(p)) = rest_tokens.first() {
if p.as_char() == '=' {
rest_tokens[1..].iter().cloned().collect()
} else {
rest.clone()
}
} else {
rest.clone()
};
if !type_tokens.is_empty() {
return quote_spanned! { span =>
<#type_tokens as ::facet::Facet>::SHAPE
};
}
}
let msg =
format!("`{attr_str}` requires a type: `{attr_str}(MyType)` or `{attr_str} = MyType`");
quote_spanned! { span =>
compile_error!(#msg)
}
}
fn generate_newtype_opt_char_value(
ns_path: &TokenStream2,
variant_ident: &proc_macro2::Ident,
attr_name: &Ident,
rest: &TokenStream2,
span: Span,
) -> TokenStream2 {
let rest_tokens: Vec<TokenTree> = rest.clone().into_iter().collect();
let attr_str = attr_name.to_string();
if rest_tokens.is_empty() {
return quote_spanned! { span =>
#ns_path::Attr::#variant_ident(𝟋None)
};
}
if let Some(TokenTree::Group(g)) = rest_tokens.first()
&& g.delimiter() == proc_macro2::Delimiter::Parenthesis
&& g.stream().is_empty()
{
return quote_spanned! { span =>
#ns_path::Attr::#variant_ident(𝟋None)
};
}
if let Some(TokenTree::Group(g)) = rest_tokens.first()
&& g.delimiter() == proc_macro2::Delimiter::Parenthesis
{
let inner: Vec<TokenTree> = g.stream().into_iter().collect();
if inner.len() == 1
&& let TokenTree::Literal(lit) = &inner[0]
{
let lit_str = lit.to_string();
if lit_str.starts_with('\'') && lit_str.ends_with('\'') {
return quote_spanned! { span =>
#ns_path::Attr::#variant_ident(𝟋Some(#lit))
};
}
}
let msg = format!("`{attr_str}` expects a char literal: `{attr_str}('v')`");
return quote_spanned! { span =>
compile_error!(#msg)
};
}
if rest_tokens.len() == 1
&& let TokenTree::Literal(lit) = &rest_tokens[0]
{
let lit_str = lit.to_string();
if lit_str.starts_with('\'') && lit_str.ends_with('\'') {
return quote_spanned! { span =>
#ns_path::Attr::#variant_ident(𝟋Some(#lit))
};
}
}
if rest_tokens.len() >= 2
&& let TokenTree::Punct(p) = &rest_tokens[0]
&& p.as_char() == '='
{
if let TokenTree::Literal(lit) = &rest_tokens[1] {
let lit_str = lit.to_string();
if lit_str.starts_with('\'') && lit_str.ends_with('\'') {
return quote_spanned! { span =>
#ns_path::Attr::#variant_ident(𝟋Some(#lit))
};
}
}
let msg = format!("`{attr_str}` expects a char literal: `{attr_str} = 'v'`");
return quote_spanned! { span =>
compile_error!(#msg)
};
}
let msg = format!(
"`{attr_str}` expects either no value or a char: `{attr_str}` or `{attr_str} = 'v'`"
);
quote_spanned! { span =>
compile_error!(#msg)
}
}
#[allow(clippy::too_many_arguments)]
fn generate_struct_value(
ns_path: &TokenStream2,
variant_ident: &proc_macro2::Ident,
struct_name: &Ident,
fields: &[ParsedFieldDef],
_attr_name: &Ident,
rest: &TokenStream2,
span: Span,
) -> TokenStream2 {
let rest_tokens: Vec<TokenTree> = rest.clone().into_iter().collect();
let default_fields: Vec<TokenStream2> = fields
.iter()
.map(|f| {
let name = &f.name;
let default = match f.kind {
FieldKind::Bool => quote! { false },
FieldKind::String => quote! { "" },
FieldKind::OptString => quote! { 𝟋None },
FieldKind::OptBool => quote! { 𝟋None },
FieldKind::OptChar => quote! { 𝟋None },
FieldKind::I64 => quote! { 0 },
FieldKind::OptI64 => quote! { 𝟋None },
FieldKind::ListString => quote! { &[] },
FieldKind::ListI64 => quote! { &[] },
FieldKind::Ident => quote! { "" },
};
quote! { #name: #default }
})
.collect();
let fields_meta: Vec<TokenStream2> = fields
.iter()
.map(|f| {
let name = &f.name;
let kind = match f.kind {
FieldKind::Bool => quote! { bool },
FieldKind::String => quote! { string },
FieldKind::OptString => quote! { opt_string },
FieldKind::OptBool => quote! { opt_bool },
FieldKind::OptChar => quote! { opt_char },
FieldKind::I64 => quote! { i64 },
FieldKind::OptI64 => quote! { opt_i64 },
FieldKind::ListString => quote! { list_string },
FieldKind::ListI64 => quote! { list_i64 },
FieldKind::Ident => quote! { ident },
};
quote! { #name: #kind }
})
.collect();
if rest_tokens.is_empty() {
return quote_spanned! { span =>
#ns_path::Attr::#variant_ident(#ns_path::#struct_name {
#(#default_fields),*
})
};
}
if let Some(TokenTree::Group(g)) = rest_tokens.first()
&& g.delimiter() == proc_macro2::Delimiter::Parenthesis
{
let inner = g.stream();
if inner.is_empty() {
return quote_spanned! { span =>
#ns_path::Attr::#variant_ident(#ns_path::#struct_name {
#(#default_fields),*
})
};
}
return quote_spanned! { span =>
::facet::__build_struct_fields! {
@krate { #ns_path }
@enum_name { Attr }
@variant_name { #variant_ident }
@struct_name { #struct_name }
@fields { #(#fields_meta),* }
@input { #inner }
}
};
}
quote_spanned! { span =>
::facet::__build_struct_fields! {
@krate { #ns_path }
@enum_name { Attr }
@variant_name { #variant_ident }
@struct_name { #struct_name }
@fields { #(#fields_meta),* }
@input { #rest }
}
}
}
#[cfg(feature = "helpful-derive")]
fn find_closest<'a>(target: &str, candidates: &'a [String]) -> Option<&'a str> {
candidates
.iter()
.filter_map(|c| {
let dist = strsim::levenshtein(target, c);
if dist <= 3 {
Some((c.as_str(), dist))
} else {
None
}
})
.min_by_key(|(_, d)| *d)
.map(|(s, _)| s)
}
#[cfg(not(feature = "helpful-derive"))]
fn find_closest<'a>(_target: &str, _candidates: &'a [String]) -> Option<&'a str> {
None
}