A Rust procedural macro for generating TryFrom<&syn::Expr> implementations for
enums. This allows you to parse Rust syntax expressions into strongly-typed enum
values, making it easy to work with configuration DSLs, macro arguments, and
other syntax-based APIs.
- 🚀 Automatic Parser Generation - Derives
TryFrom<&syn::Expr>for your enums - 📦 Multiple Variant Types - Supports unit, tuple, and struct variants
- 🎯 Smart Type Detection - Automatically detects wrapper vs leaf enums
- 🔧 Flexible Parsing - Works with primitives, collections, and custom types
- 💡 Helpful Error Messages - Provides clear error messages for parse failures
- ⚡ Zero Runtime Overhead - All parsing logic is generated at compile time
Add this to your Cargo.toml:
[dependencies]
try_from_expr = "0.2"use try_from_expr::TryFromExpr;
use syn::Expr;
#[derive(TryFromExpr)]
enum ConfigValue {
Enabled,
Threshold(u32),
Range { min: i32, max: i32 },
}
// Parse from a syn::Expr
let expr: Expr = syn::parse_str("ConfigValue::Threshold(42)").unwrap();
let config = ConfigValue::try_from(&expr).unwrap();#[derive(TryFromExpr)]
enum Setting {
// Unit variant
Default,
// Tuple variant with single value
Timeout(u64),
// Tuple variant with multiple values
Coordinate(f64, f64),
// Struct variant
Config {
name: String,
value: i32,
enabled: bool,
},
}
// Parse unit variant
let expr = syn::parse_str("Setting::Default").unwrap();
let setting = Setting::try_from(&expr).unwrap();
// Parse tuple variant
let expr = syn::parse_str("Setting::Timeout(5000)").unwrap();
let setting = Setting::try_from(&expr).unwrap();
// Parse struct variant
let expr = syn::parse_str(r#"Setting::Config {
name: "test",
value: 42,
enabled: true
}"#).unwrap();
let setting = Setting::try_from(&expr).unwrap();The macro automatically detects "wrapper" enums that contain other enum types and generates optimized parsing:
#[derive(TryFromExpr)]
enum StringConstraint {
MinLength(usize),
MaxLength(usize),
Pattern(String),
}
#[derive(TryFromExpr)]
enum NumberConstraint {
Min(i64),
Max(i64),
Range(i64, i64),
}
#[derive(TryFromExpr)]
enum Constraint {
String(StringConstraint),
Number(NumberConstraint),
Required,
}
// The macro detects this is a wrapper and allows parsing nested enums
let expr = syn::parse_str("Constraint::String(StringConstraint::MinLength(10))").unwrap();
let constraint = Constraint::try_from(&expr).unwrap();#[derive(TryFromExpr)]
enum DataType {
Single(String),
Multiple(Vec<String>),
Mapping(HashMap<String, i32>),
}
// Parse Vec
let expr = syn::parse_str(r#"DataType::Multiple(vec!["a", "b", "c"])"#).unwrap();
let data = DataType::try_from(&expr).unwrap();
// Parse HashMap
let expr = syn::parse_str(r#"DataType::Mapping([("key", 42)])"#).unwrap();
let data = DataType::try_from(&expr).unwrap();#[derive(TryFromExpr)]
enum OptionalConfig {
Value(Option<String>),
}
// Explicit Some
let expr = syn::parse_str(r#"OptionalConfig::Value(Some("text"))"#).unwrap();
// Explicit None
let expr = syn::parse_str("OptionalConfig::Value(None)").unwrap();
// Implicit Some (bare value treated as Some)
let expr = syn::parse_str(r#"OptionalConfig::Value("text")"#).unwrap();Every variant also parses from a snake_case form that reads like an attribute
argument, the way #[serde(rename = "..")] does. A value inside a meta form
can itself be in path form, as in regex_literal = Format::Email. The examples
below use these enums:
#[derive(TryFromExpr)]
enum Format {
Email,
Custom(String),
}
#[derive(TryFromExpr)]
enum StringValidator {
Trim,
MaxLength(Option<usize>),
OneOf(Vec<String>),
RegexLiteral(Format),
Length { min: usize, max: Option<usize> },
}
#[derive(TryFromExpr)]
enum NumberValidator {
Positive,
Between(i64, i64),
Labels(HashMap<String, i64>),
}
#[derive(TryFromExpr)]
enum Validator {
String(StringValidator),
Number(NumberValidator),
}A unit variant is its snake_case name. The bare PascalCase name works too.
| Meta form | Parses to |
|---|---|
trim |
StringValidator::Trim |
Trim |
StringValidator::Trim |
A single-field variant takes name = value. An Option field accepts None,
Some(..) or a bare value, and a field holding another enum takes that enum in
either form.
| Meta form | Parses to |
|---|---|
max_length = 64 |
MaxLength(Some(64)) |
max_length = Some(64) |
MaxLength(Some(64)) |
max_length = None |
MaxLength(None) |
one_of = ["draft", "published"] |
OneOf(vec!["draft".into(), "published".into()]) |
one_of = vec!["draft"] |
OneOf(vec!["draft".into()]) |
regex_literal = format(email) |
RegexLiteral(Format::Email) |
regex_literal = Format::Email |
RegexLiteral(Format::Email) |
regex_literal = format(custom = "^a") |
RegexLiteral(Format::Custom("^a".into())) |
A variant with several fields takes them as a tuple, and a map takes an array of key and value pairs.
| Meta form | Parses to |
|---|---|
between = (1, 10) |
NumberValidator::Between(1, 10) |
between = (-5, 5) |
NumberValidator::Between(-5, 5) |
labels = [("low", 0), ("high", 100)] |
NumberValidator::Labels({"low": 0, "high": 100}) |
A struct variant is called with field = value arguments, in any order. A
field of type Option can be left out.
| Meta form | Parses to |
|---|---|
length(min = 1, max = 64) |
Length { min: 1, max: Some(64) } |
length(max = 64, min = 1) |
Length { min: 1, max: Some(64) } |
length(min = 1) |
Length { min: 1, max: None } |
Any meta form can be wrapped in its enum's snake_case name. On the enum itself this changes nothing, but in a wrapper it picks the child explicitly.
| Meta form | Parses to |
|---|---|
string_validator(trim) |
StringValidator::Trim |
string_validator(length(min = 1)) |
StringValidator::Length { min: 1, max: None } |
number_validator(between = (1, 10)) |
NumberValidator::Between(1, 10) |
A wrapper accepts its children's forms. A bare name goes to the child that
accepts it (see Children Sharing a Name), a named
child goes to that child, and the wrapper's own variants take name = value
like any tuple variant.
Meta form given to Validator |
Parses to |
|---|---|
trim |
Validator::String(StringValidator::Trim) |
between = (1, 10) |
Validator::Number(NumberValidator::Between(1, 10)) |
string_validator(max_length = 64) |
Validator::String(StringValidator::MaxLength(Some(64))) |
number = positive |
Validator::Number(NumberValidator::Positive) |
validator(string = trim) |
Validator::String(StringValidator::Trim) |
StringValidator::Trim |
Validator::String(StringValidator::Trim) |
A name that is a Rust keyword takes the raw identifier prefix, so a variant
Type(String) is written r#type = "text" and a variant Ref { r#type: u8 }
is written r#ref(r#type = 3).
The meta forms are ordinary Rust expressions, so an attribute's arguments parse
as a comma-separated list of syn::Expr:
use syn::{Attribute, Expr, Token, punctuated::Punctuated};
let attr: Attribute = syn::parse_quote!(#[validators(
trim,
length(min = 1, max = 64),
string_validator(regex_literal = format(custom = r"^/(?:[^/\\].*)?$")),
)]);
let validators = attr
.parse_args_with(Punctuated::<Expr, Token![,]>::parse_terminated)?
.iter()
.map(Validator::try_from)
.collect::<syn::Result<Vec<_>>>()?;The equivalent path forms parse to the same values:
#[validators(
StringValidator::Trim,
StringValidator::Length { min: 1, max: Some(64) },
StringValidator::RegexLiteral(Format::Custom(r"^/(?:[^/\\].*)?$")),
)]A form that names nothing reports the names that would have worked:
length(min = 1, mx = 64)
Unknown field 'mx' for variant 'Length'. Valid fields: min, max
string_validator(trimm)
Unknown unit variant 'trimm' for enum 'StringValidator'.
Valid options: Trim; in meta form: trim
between = 1
Variant 'Between' expects a tuple of 2 values
When a struct variant shares its enum's snake_case name, name(..) is that
variant when every argument is one of its fields, and the enum wrapper
otherwise:
#[derive(TryFromExpr)]
enum Config {
Config { host: String, port: Option<u16> },
Verbose,
Level(u8),
}| Meta form | Parses to |
|---|---|
config(host = "localhost") |
Config::Config { host: "localhost", port: None } |
config(host = "localhost", port = 8080) |
Config::Config { host: "localhost", port: Some(8080) } |
config(config(host = "localhost")) |
Config::Config { host: "localhost", port: None } |
config(verbose) |
Config::Verbose |
config(level = 3) |
Config::Level(3) |
A misspelled field is not one of the variant's fields, so the call is read as the enum wrapper and reports the variants that would have fit:
config(hots = "localhost")
Unknown tuple variant 'hots' for enum 'Config'. Valid options: level
A field of that variant may not share a meta name with a tuple variant, since
config(level = ..) would then fit both readings. The derive rejects it:
#[derive(TryFromExpr)]
enum Config {
Config { level: u8 },
Level(u8),
}error: Field `level` shares its meta name with a tuple variant, so `config(level = ..)` is ambiguous
--> src/lib.rs:5:14
|
5 | Config { level: u8 },
| ^^^^^
Clippy's default enum_variant_names lint warns about a variant named after
its enum, so a project using this pattern needs to allow that lint.
Every derived enum publishes the names it accepts bare, through the
try_from_expr::meta_names::MetaNames trait. A wrapper uses them to send a bare
name such as trim straight to the one child that accepts it. Names only clash
within a shape: required, required = .. and required(..) are distinct.
When two children accept the same bare name, using it bare is an error that asks for the qualified form:
Ambiguous meta name `required` for enum 'Rule': accepted by TextRule, NumberRule.
Qualify it with `text_rule(..)` or `number_rule(..)`
To rule out overlaps altogether, mark the wrapper all_unique. Any shared name,
including one inside a nested wrapper, then fails to compile:
#[derive(TryFromExpr)]
#[try_from_expr(all_unique)]
enum Rule {
Text(TextRule),
Number(NumberRule),
}A hand-written child type implements MetaNames itself, listing the bare names
its TryFrom<&syn::Expr> accepts:
impl MetaNames for Flag {
const META_NAMES: MetaNameSet = MetaNameSet {
path_names: &["flag"],
..MetaNameSet::empty("Flag")
};
}By default, the macro automatically detects whether your enum is a wrapper or leaf enum. You can override this:
#[derive(TryFromExpr)]
#[try_from_expr(wrapper)] // Force wrapper mode
enum ForceWrapper {
Variant1(CustomType),
Variant2(AnotherType),
}
#[derive(TryFromExpr)]
#[try_from_expr(leaf)] // Force leaf mode
enum ForceLeaf {
Simple,
Complex(String),
}The macro analyzes your enum at compile time and generates a
TryFrom<&syn::Expr> implementation that:
- Unwraps any parentheses or group expressions
- Matches the expression type (path, call, struct, or a meta form)
- Parses the variant name and validates it belongs to your enum
- Extracts and parses any parameters or fields
- Constructs the appropriate enum variant
- Returns helpful error messages for any parsing failures
The macro has built-in support for:
- Primitives:
bool,char,String, all integer types,f32,f64 - Collections:
Vec<T>,HashMap<K, V>,BTreeMap<K, V>,Option<T> - Special:
OrderedFloat<T>from theordered-floatcrate - Custom Types: Any type that implements
TryFrom<&syn::Expr>, plusMetaNameswhen a wrapper enum holds it
The macro provides detailed error messages:
// Unknown variant
"Unknown unit variant 'Invalid' for enum 'Setting'"
// Wrong number of arguments
"Variant 'Coordinate' expects 2 argument(s), but 3 were provided"
// Type parsing failure
"Failed to parse argument 1: Expected an integer literal"
// Missing required field
"Missing required field 'name' for variant 'Config'"
// Unknown meta variant
"Unknown unit variant 'trimm' for enum 'StringValidator'. Valid options: Trim; in meta form: trim"This workspace contains two crates:
try_from_expr- The main library crate that users importtry_from_expr_derive- The procedural macro implementation
This project is licensed under the MIT license.
Please feel free to submit a Pull Request.