mod number;
use crate::prelude::*;
use darling::{Error as DarlingError, FromMeta, ast::NestedMeta};
use syn::{Expr, Lit, LitStr, Path};
pub use number::*;
#[derive(Clone, Debug)]
pub enum Arg {
Bool(bool),
Char(char),
Number(ArgNumber),
ConstPath(Path), FuncPath(Path), String(LitStr),
}
impl FromMeta for Arg {
fn from_value(value: &Lit) -> Result<Self, DarlingError> {
match value {
Lit::Bool(lit) => Ok(Self::Bool(lit.value)),
Lit::Char(lit) => Ok(Self::Char(lit.value())),
Lit::Int(_) | Lit::Float(_) => ArgNumber::from_value(value).map(Self::Number),
Lit::Str(lit) => Ok(Self::String(lit.clone())),
_ => Err(DarlingError::custom(format!(
"Unsupported literal type: {value:?}"
))),
}
}
fn from_expr(expr: &Expr) -> Result<Self, DarlingError> {
match expr {
Expr::Lit(lit) => Self::from_value(&lit.lit),
Expr::Group(group) => Self::from_expr(&group.expr),
Expr::Path(path) if path.qself.is_none() => {
Self::from_nested_meta(&NestedMeta::Meta(syn::Meta::Path(path.path.clone())))
}
_ => Err(DarlingError::unexpected_expr_type(expr)),
}
.map_err(|error| error.with_span(expr))
}
fn from_nested_meta(item: &NestedMeta) -> Result<Self, DarlingError> {
match item {
NestedMeta::Lit(lit) => Self::from_value(lit),
NestedMeta::Meta(syn::Meta::Path(path)) => {
let last = path
.segments
.last()
.ok_or_else(|| DarlingError::custom("path has no segments"))?
.ident
.to_string();
if last.chars().next().is_some_and(char::is_uppercase) {
Ok(Self::ConstPath(path.clone()))
} else {
Ok(Self::FuncPath(path.clone()))
}
}
NestedMeta::Meta(syn::Meta::NameValue(nv)) => Err(DarlingError::custom(format!(
"NameValue not supported here: {nv:?}"
))),
NestedMeta::Meta(syn::Meta::List(list)) => Err(DarlingError::custom(format!(
"Nested list not supported here: {list:?}"
))),
NestedMeta::NameValueInvalidExpr(value) => Self::from_invalid_expr(value),
}
}
}
impl HasSchemaPart for Arg {
fn schema_part(&self, paths: &CratePaths) -> TokenStream {
let model = &paths.model;
match self {
Self::Bool(v) => quote!(#model::node::Arg::Bool(#v)),
Self::Char(v) => quote!(#model::node::Arg::Char(#v)),
Self::Number(v) => {
let num = quote_one(v, |value| ArgNumber::schema_part(value, paths));
quote!(#model::node::Arg::Number(#num))
}
Self::ConstPath(p) => {
let path = quote_one(p, to_str_lit);
quote!(#model::node::Arg::ConstPath(#path))
}
Self::FuncPath(p) => {
let path = quote_one(p, to_str_lit);
quote!(#model::node::Arg::FuncPath(#path))
}
Self::String(v) => quote!(#model::node::Arg::String(#v)),
}
}
}
impl ToTokens for Arg {
fn to_tokens(&self, tokens: &mut TokenStream) {
let q = match self {
Self::Bool(v) => quote!(#v),
Self::Char(v) => quote!(#v), Self::Number(v) => quote!(#v), Self::ConstPath(p) => quote!(#p), Self::FuncPath(p) => quote!(#p()), Self::String(v) => quote!(#v),
};
tokens.extend(q);
}
}
#[derive(Clone, Debug, Default)]
pub struct Args(Vec<Arg>);
impl Args {
#[must_use]
pub(crate) const fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub(crate) fn iter(&self) -> std::slice::Iter<'_, Arg> {
self.0.iter()
}
}
impl FromMeta for Args {
fn from_list(items: &[NestedMeta]) -> Result<Self, DarlingError> {
let mut args = Vec::new();
for item in items {
args.push(Arg::from_nested_meta(item)?);
}
Ok(Self(args))
}
}
impl HasSchemaPart for Args {
fn schema_part(&self, paths: &CratePaths) -> TokenStream {
let model = &paths.model;
let args = quote_slice(&self.0, |value| Arg::schema_part(value, paths));
quote! {
#model::node::Args(#args)
}
}
}
#[cfg(test)]
mod arg_tests {
use super::*;
use syn::parse_quote;
#[test]
fn quoted_paths_remain_literal_defaults_and_arguments() {
for text in ["::1", "a::b", "std::string", "Self::CONST", "42", ""] {
let literal = LitStr::new(text, proc_macro2::Span::call_site());
let arg = Arg::from_meta(&parse_quote!(default = #literal)).unwrap();
assert!(matches!(&arg, Arg::String(value) if value.value() == text));
assert_eq!(
arg.to_token_stream().to_string(),
quote!(#literal).to_string()
);
let args = Args::from_list(&[NestedMeta::Lit(Lit::Str(literal.clone()))]).unwrap();
assert!(matches!(&args.0[0], Arg::String(value) if value.value() == text));
let schema = arg.schema_part(&CratePaths::test());
assert!(schema.to_string().contains("Arg :: String"));
}
}
#[test]
fn bare_paths_work_in_defaults_and_argument_lists() {
for (path, is_constant) in [
(parse_quote!(String::new), false),
(parse_quote!(crate::LIMIT), true),
(parse_quote!(factory), false),
] {
let default = Arg::from_meta(&parse_quote!(default = #path)).unwrap();
let argument = Arg::from_nested_meta(&NestedMeta::Meta(syn::Meta::Path(path))).unwrap();
assert_eq!(
default.to_token_stream().to_string(),
argument.to_token_stream().to_string()
);
assert_eq!(matches!(default, Arg::ConstPath(_)), is_constant);
}
assert!(Arg::from_meta(&parse_quote!(default = factory())).is_err());
assert!(Arg::from_meta(&parse_quote!(default = 1 + 2)).is_err());
}
#[test]
fn test_bool_parsing() {
let lit = parse_quote!(true);
if let Ok(Arg::Bool(b)) = Arg::from_value(&lit) {
assert!(b, "Parsed boolean should be true");
} else {
panic!("Expected Bool variant");
}
}
#[test]
fn test_char_parsing() {
let lit = parse_quote!('a');
if let Ok(Arg::Char(c)) = Arg::from_value(&lit) {
assert_eq!(c, 'a', "Parsed char should be 'a'");
} else {
panic!("Expected Char variant");
}
}
#[test]
fn test_number_parsing() {
let lit = parse_quote!(42);
if let Ok(Arg::Number(num)) = Arg::from_value(&lit) {
assert_eq!(num, ArgNumber::Int32(42), "Parsed number does not match");
} else {
panic!("Expected Number variant");
}
}
#[test]
fn test_const_path_parsing() {
let arg: Arg = Arg::from_nested_meta(&parse_quote!(MY_CONST)).unwrap();
match arg {
Arg::ConstPath(path) => {
assert_eq!(path.segments.last().unwrap().ident.to_string(), "MY_CONST");
}
_ => panic!("Expected ConstPath variant"),
}
}
#[test]
fn test_func_path_parsing() {
let arg: Arg = Arg::from_nested_meta(&parse_quote!(my_func)).unwrap();
match arg {
Arg::FuncPath(path) => {
assert_eq!(path.segments.last().unwrap().ident.to_string(), "my_func");
}
_ => panic!("Expected FuncPath variant"),
}
}
#[test]
fn test_invalid_input() {
let lit = parse_quote!(b"invalid");
assert!(
Arg::from_value(&lit).is_err(),
"Expected an error for unsupported literal type."
);
}
}