Skip to main content

packr_derive/
lib.rs

1//! Derive macros for pack-abi Value conversion.
2//!
3//! This crate provides `#[derive(GraphValue)]` which generates implementations
4//! of `From<T> for Value` and `TryFrom<Value> for T`.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use packr_abi::{GraphValue, Value};
10//!
11//! #[derive(GraphValue)]
12//! struct Point {
13//!     x: i64,
14//!     y: i64,
15//! }
16//!
17//! let point = Point { x: 10, y: 20 };
18//! let value: Value = point.into();
19//! let back: Point = value.try_into().unwrap();
20//! ```
21//!
22//! # Crate Path
23//!
24//! By default, the macro expects `packr_abi` to be in scope. For `no_std` guests
25//! using `packr_guest`, specify the crate path:
26//!
27//! ```ignore
28//! use packr_guest::GraphValue;
29//!
30//! #[derive(GraphValue)]
31//! #[graph(crate = "packr_guest::composite_abi")]
32//! struct MyState {
33//!     count: i32,
34//! }
35//! ```
36
37use proc_macro::TokenStream;
38use quote::{format_ident, quote};
39use syn::{parse_macro_input, Attribute, Data, DeriveInput, Fields, Meta};
40
41/// Extract the crate path from `#[graph(crate = "...")]` attribute.
42/// Defaults to `packr_abi` if not specified.
43fn get_crate_path(attrs: &[Attribute]) -> proc_macro2::TokenStream {
44    for attr in attrs {
45        if attr.path().is_ident("graph") {
46            if let Meta::List(list) = &attr.meta {
47                let tokens = list.tokens.to_string();
48                // The `graph(...)` list can carry multiple comma-separated args
49                // (e.g. `crate = "...", forward_compatible`), so scan each part
50                // for `crate = "..."` rather than assuming it is the whole list —
51                // otherwise a trailing arg would leave the string not ending in a
52                // quote and silently fall through to the default crate.
53                for part in tokens.split(',') {
54                    let part = part.trim();
55                    if let Some(rest) = part.strip_prefix("crate") {
56                        let rest = rest.trim();
57                        if let Some(rest) = rest.strip_prefix('=') {
58                            let rest = rest.trim();
59                            if rest.len() >= 2 && rest.starts_with('"') && rest.ends_with('"') {
60                                let path_str = &rest[1..rest.len() - 1];
61                                // Convert string path to token stream
62                                let path: syn::Path =
63                                    syn::parse_str(path_str).expect("Invalid crate path");
64                                return quote! { #path };
65                            }
66                        }
67                    }
68                }
69            }
70        }
71    }
72    // Default to packr_abi
73    quote! { packr_abi }
74}
75
76/// Derive macro for converting between Rust types and `Value`.
77///
78/// # Structs
79///
80/// Structs are converted to `Value::Record` with field names as keys.
81///
82/// ```ignore
83/// #[derive(GraphValue)]
84/// struct Person {
85///     name: String,
86///     age: i64,
87/// }
88/// ```
89///
90/// # Enums
91///
92/// Enums are converted to `Value::Variant` with the variant index as tag.
93///
94/// ```ignore
95/// #[derive(GraphValue)]
96/// enum Shape {
97///     Circle(f64),           // tag 0, payload = radius
98///     Rectangle(f64, f64),   // tag 1, payload = tuple(width, height)
99///     Point,                 // tag 2, no payload
100/// }
101/// ```
102///
103/// # Attributes
104///
105/// - `#[graph(crate = "path")]` - Specify the crate path (default: `packr_abi`)
106/// - `#[graph(rename = "name")]` - Use a different name for field/variant
107/// - `#[graph(tag = N)]` - Use explicit tag number for variant
108/// - `#[graph(forward_compatible)]` - Tolerant decode for schema evolution on a
109///   STRUCT: a missing field defaults and an extra field is ignored, so appending
110///   a field is decode-safe both ways (old build reads new data; new build reads
111///   old data — no rollback data loss, no hand-written pad-missing migration).
112///   Default (absent) is a strict field-count decode. Named structs match fields
113///   by name (add/remove/reorder tolerated); tuple structs are positional, so only
114///   APPENDING a trailing field is safe. Encode is unchanged (wire-compatible).
115#[proc_macro_derive(GraphValue, attributes(graph))]
116pub fn derive_graph_value(input: TokenStream) -> TokenStream {
117    let input = parse_macro_input!(input as DeriveInput);
118    let crate_path = get_crate_path(&input.attrs);
119
120    let expanded = match &input.data {
121        Data::Struct(data) => derive_struct(&input, data, &crate_path),
122        Data::Enum(data) => derive_enum(&input, data, &crate_path),
123        Data::Union(_) => {
124            return syn::Error::new_spanned(&input, "GraphValue cannot be derived for unions")
125                .to_compile_error()
126                .into();
127        }
128    };
129
130    expanded.into()
131}
132
133/// Whether `#[graph(forward_compatible)]` is present. It opts a struct into a
134/// schema-evolution-tolerant decode: a MISSING field defaults instead of erroring,
135/// and EXTRA fields are ignored, so appending a field stays decode-safe in BOTH
136/// directions (an old build reads new data; a new build reads old data). Default
137/// (attr absent) keeps the strict field-count decode so genuine field-count bugs
138/// still fail loud. For NAMED structs fields are matched by NAME, so add/remove/
139/// reorder are all tolerated; for TUPLE structs decode is POSITIONAL, so only
140/// APPENDING a trailing field is safe (never mid-insert or reorder).
141fn has_forward_compatible(attrs: &[Attribute]) -> bool {
142    for attr in attrs {
143        if attr.path().is_ident("graph") {
144            if let Meta::List(list) = &attr.meta {
145                let tokens = list.tokens.to_string();
146                if tokens.split(',').any(|t| t.trim() == "forward_compatible") {
147                    return true;
148                }
149            }
150        }
151    }
152    false
153}
154
155/// Clone a type's generics and add the trait bounds that every generated impl
156/// needs on each generic type parameter, so `#[derive(GraphValue)]` works on
157/// generic types (e.g. `struct Pair<A, B>`). A parameter `A` must be able to
158/// round-trip through `Value` and describe itself, so we require:
159///   - `A: Into<Value>`                             (for `From<T> for Value`)
160///   - `A: TryFrom<Value, Error = ConversionError>` (for `TryFrom<Value> for T`)
161///   - `A: KnownValueType`                          (for tuple/enum `KnownValueType`)
162///
163/// These are exactly the bounds packr's blanket container impls require of an
164/// element type, so fields like `Vec<A>` also satisfy their impls. The encode
165/// direction is emitted as `Into::<Value>::into(field)` (not `Value::from`) so
166/// that a `A: Into<Value>` where-bound doesn't get mis-selected for a container
167/// or concrete field whose own `Into<Value>` impl should be used instead.
168///
169/// The same (superset) where-clause is shared by all three impls, which is why
170/// every bound is added even though a given impl may only use some of them.
171/// Non-generic inputs get no added predicates, so existing derives are
172/// byte-for-byte unchanged.
173fn augmented_generics(generics: &syn::Generics, krate: &proc_macro2::TokenStream) -> syn::Generics {
174    let mut generics = generics.clone();
175    let type_idents: Vec<syn::Ident> = generics.type_params().map(|tp| tp.ident.clone()).collect();
176    if type_idents.is_empty() {
177        return generics;
178    }
179    let where_clause = generics.make_where_clause();
180    for ident in type_idents {
181        where_clause
182            .predicates
183            .push(syn::parse_quote!(#ident: ::core::convert::Into<#krate::Value>));
184        where_clause.predicates.push(syn::parse_quote!(
185            #ident: #krate::__private::TryFrom<#krate::Value, Error = #krate::ConversionError>
186        ));
187        where_clause
188            .predicates
189            .push(syn::parse_quote!(#ident: #krate::KnownValueType));
190    }
191    generics
192}
193
194/// If `ty` is `Box<Inner>` (by any path — `Box`, `::alloc::boxed::Box`, etc.),
195/// return `Inner`.
196fn box_inner(ty: &syn::Type) -> Option<&syn::Type> {
197    let syn::Type::Path(tp) = ty else {
198        return None;
199    };
200    let seg = tp.path.segments.last()?;
201    if seg.ident != "Box" {
202        return None;
203    }
204    let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
205        return None;
206    };
207    args.args.iter().find_map(|a| match a {
208        syn::GenericArgument::Type(t) => Some(t),
209        _ => None,
210    })
211}
212
213/// Decode a field's `Value` into `field_type`, yielding `Result<field_type,
214/// ConversionError>` so the caller's `.map_err(..)?` still applies.
215///
216/// A `Box<Inner>` field decodes its inner type and re-boxes: packr has no
217/// `FromValue for Box<T>` (a blanket impl conflicts with the `TryFrom`-based
218/// blanket at `Box<Value>`), so a boxed self-reference — the shape a directly
219/// recursive variant or record produces — is handled here instead.
220fn decode_field(
221    field_type: &syn::Type,
222    value: proc_macro2::TokenStream,
223    krate: &proc_macro2::TokenStream,
224) -> proc_macro2::TokenStream {
225    if let Some(inner) = box_inner(field_type) {
226        quote! {
227            <#inner as #krate::FromValue>::from_value(#value).map(#krate::__private::Box::new)
228        }
229    } else {
230        quote! {
231            <#field_type as #krate::FromValue>::from_value(#value)
232        }
233    }
234}
235
236fn derive_struct(
237    input: &DeriveInput,
238    data: &syn::DataStruct,
239    krate: &proc_macro2::TokenStream,
240) -> proc_macro2::TokenStream {
241    let name = &input.ident;
242    let generics = augmented_generics(&input.generics, krate);
243    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
244    let forward_compatible = has_forward_compatible(&input.attrs);
245
246    match &data.fields {
247        Fields::Named(fields) => {
248            // Generate TryFrom<Value> for T
249            let field_from_value: Vec<_> = fields
250                .named
251                .iter()
252                .map(|f| {
253                    let field_name = f.ident.as_ref().unwrap();
254                    let field_name_str =
255                        get_rename(&f.attrs).unwrap_or_else(|| field_name.to_string());
256                    let field_type = &f.ty;
257                    let decode = decode_field(field_type, quote! { field_value }, krate);
258                    if forward_compatible {
259                        // A missing field defaults instead of erroring (extra fields are
260                        // simply never looked up, since decode is by name).
261                        quote! {
262                            #field_name: match fields.iter()
263                                .find(|(name, _)| name == #field_name_str)
264                                .map(|(_, v)| v.clone())
265                            {
266                                #krate::__private::Some(field_value) =>
267                                    #decode
268                                        .map_err(|e| #krate::ConversionError::FieldError(
269                                            #krate::__private::String::from(#field_name_str),
270                                            #krate::__private::Box::new(e)
271                                        ))?,
272                                #krate::__private::None =>
273                                    <#field_type as ::core::default::Default>::default(),
274                            }
275                        }
276                    } else {
277                        quote! {
278                            #field_name: {
279                                let field_value = fields.iter()
280                                    .find(|(name, _)| name == #field_name_str)
281                                    .map(|(_, v)| v.clone())
282                                    .ok_or_else(|| #krate::ConversionError::MissingField(
283                                        #krate::__private::String::from(#field_name_str)
284                                    ))?;
285                                #decode
286                                    .map_err(|e| #krate::ConversionError::FieldError(
287                                        #krate::__private::String::from(#field_name_str),
288                                        #krate::__private::Box::new(e)
289                                    ))?
290                            }
291                        }
292                    }
293                })
294                .collect();
295
296            let field_count = fields.named.len();
297
298            // Generate field accessors for From impl
299            let field_accessors: Vec<_> = fields
300                .named
301                .iter()
302                .map(|f| {
303                    let field_name = f.ident.as_ref().unwrap();
304                    let field_name_str =
305                        get_rename(&f.attrs).unwrap_or_else(|| field_name.to_string());
306                    quote! {
307                        (
308                            #krate::__private::String::from(#field_name_str),
309                            ::core::convert::Into::<#krate::Value>::into(value.#field_name)
310                        )
311                    }
312                })
313                .collect();
314
315            let type_name_str = name.to_string();
316
317            // A forward-compatible record does not check the field count: extra
318            // fields are ignored and missing ones default (above), which is what
319            // makes appending a field decode-safe both ways.
320            let count_check = if forward_compatible {
321                quote! {}
322            } else {
323                quote! {
324                    if fields.len() != #field_count {
325                        return #krate::__private::Err(#krate::ConversionError::WrongFieldCount {
326                            expected: #field_count,
327                            got: fields.len(),
328                        });
329                    }
330                }
331            };
332
333            quote! {
334                impl #impl_generics #krate::__private::From<#name #ty_generics> for #krate::Value #where_clause {
335                    fn from(value: #name #ty_generics) -> #krate::Value {
336                        #krate::Value::Record {
337                            type_name: #krate::__private::String::from(#type_name_str),
338                            fields: #krate::__private::vec![
339                                #(#field_accessors),*
340                            ],
341                        }
342                    }
343                }
344
345                impl #impl_generics #krate::__private::TryFrom<#krate::Value> for #name #ty_generics #where_clause {
346                    type Error = #krate::ConversionError;
347
348                    fn try_from(value: #krate::Value) -> #krate::__private::Result<Self, #krate::ConversionError> {
349                        match value {
350                            #krate::Value::Record { fields, .. } => {
351                                #count_check
352                                #krate::__private::Ok(Self {
353                                    #(#field_from_value),*
354                                })
355                            }
356                            other => #krate::__private::Err(#krate::ConversionError::ExpectedRecord(
357                                #krate::__private::format!("{:?}", other)
358                            )),
359                        }
360                    }
361                }
362
363                impl #impl_generics #krate::KnownValueType for #name #ty_generics #where_clause {
364                    fn known_value_type() -> #krate::ValueType {
365                        #krate::ValueType::Record(
366                            #krate::__private::String::from(#type_name_str)
367                        )
368                    }
369                }
370            }
371        }
372        Fields::Unnamed(fields) => {
373            // Tuple struct -> Value::Tuple
374            let field_indices: Vec<_> = (0..fields.unnamed.len()).map(syn::Index::from).collect();
375
376            let field_from_value: Vec<_> = fields.unnamed.iter().enumerate().map(|(i, f)| {
377                let field_type = &f.ty;
378                if forward_compatible {
379                    // Decode is positional: a missing trailing index defaults, an
380                    // extra trailing element is ignored (below). Only APPENDING a
381                    // trailing field is safe for a tuple struct.
382                    let decode = decode_field(field_type, quote! { field_value }, krate);
383                    quote! {
384                        match fields.get(#i).cloned() {
385                            #krate::__private::Some(field_value) =>
386                                #decode
387                                    .map_err(|e| #krate::ConversionError::IndexError(#i, #krate::__private::Box::new(e)))?,
388                            #krate::__private::None =>
389                                <#field_type as ::core::default::Default>::default(),
390                        }
391                    }
392                } else {
393                    let decode = decode_field(
394                        field_type,
395                        quote! { fields.get(#i).cloned().ok_or_else(|| #krate::ConversionError::MissingIndex(#i))? },
396                        krate,
397                    );
398                    quote! {
399                        #decode.map_err(|e| #krate::ConversionError::IndexError(#i, #krate::__private::Box::new(e)))?
400                    }
401                }
402            }).collect();
403
404            let field_count = fields.unnamed.len();
405            let field_types: Vec<_> = fields.unnamed.iter().map(|f| &f.ty).collect();
406
407            let count_check = if forward_compatible {
408                quote! {}
409            } else {
410                quote! {
411                    if fields.len() != #field_count {
412                        return #krate::__private::Err(#krate::ConversionError::WrongFieldCount {
413                            expected: #field_count,
414                            got: fields.len(),
415                        });
416                    }
417                }
418            };
419
420            quote! {
421                impl #impl_generics #krate::__private::From<#name #ty_generics> for #krate::Value #where_clause {
422                    fn from(value: #name #ty_generics) -> #krate::Value {
423                        #krate::Value::Tuple(#krate::__private::vec![
424                            #(::core::convert::Into::<#krate::Value>::into(value.#field_indices)),*
425                        ])
426                    }
427                }
428
429                impl #impl_generics #krate::__private::TryFrom<#krate::Value> for #name #ty_generics #where_clause {
430                    type Error = #krate::ConversionError;
431
432                    fn try_from(value: #krate::Value) -> #krate::__private::Result<Self, #krate::ConversionError> {
433                        match value {
434                            #krate::Value::Tuple(fields) => {
435                                #count_check
436                                #krate::__private::Ok(Self(
437                                    #(#field_from_value),*
438                                ))
439                            }
440                            other => #krate::__private::Err(#krate::ConversionError::ExpectedTuple(
441                                #krate::__private::format!("{:?}", other)
442                            )),
443                        }
444                    }
445                }
446
447                impl #impl_generics #krate::KnownValueType for #name #ty_generics #where_clause {
448                    fn known_value_type() -> #krate::ValueType {
449                        #krate::ValueType::Tuple(#krate::__private::vec![
450                            #(<#field_types as #krate::KnownValueType>::known_value_type()),*
451                        ])
452                    }
453                }
454            }
455        }
456        Fields::Unit => {
457            // Unit struct -> Value::Tuple([])
458            quote! {
459                impl #impl_generics #krate::__private::From<#name #ty_generics> for #krate::Value #where_clause {
460                    fn from(_: #name #ty_generics) -> #krate::Value {
461                        #krate::Value::Tuple(#krate::__private::vec![])
462                    }
463                }
464
465                impl #impl_generics #krate::__private::TryFrom<#krate::Value> for #name #ty_generics #where_clause {
466                    type Error = #krate::ConversionError;
467
468                    fn try_from(value: #krate::Value) -> #krate::__private::Result<Self, #krate::ConversionError> {
469                        match value {
470                            #krate::Value::Tuple(fields) if fields.is_empty() => {
471                                #krate::__private::Ok(Self)
472                            }
473                            #krate::Value::Tuple(fields) => {
474                                #krate::__private::Err(#krate::ConversionError::WrongFieldCount {
475                                    expected: 0,
476                                    got: fields.len(),
477                                })
478                            }
479                            other => #krate::__private::Err(#krate::ConversionError::ExpectedTuple(
480                                #krate::__private::format!("{:?}", other)
481                            )),
482                        }
483                    }
484                }
485
486                impl #impl_generics #krate::KnownValueType for #name #ty_generics #where_clause {
487                    fn known_value_type() -> #krate::ValueType {
488                        #krate::ValueType::Tuple(#krate::__private::vec![])
489                    }
490                }
491            }
492        }
493    }
494}
495
496fn derive_enum(
497    input: &DeriveInput,
498    data: &syn::DataEnum,
499    krate: &proc_macro2::TokenStream,
500) -> proc_macro2::TokenStream {
501    let name = &input.ident;
502    let type_name_str = name.to_string();
503    let generics = augmented_generics(&input.generics, krate);
504    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
505
506    // Generate match arms for From<T> for Value
507    let to_value_arms: Vec<_> = data
508        .variants
509        .iter()
510        .enumerate()
511        .map(|(default_tag, variant)| {
512            let variant_name = &variant.ident;
513            let case_name_str = variant_name.to_string();
514            let tag = get_tag(&variant.attrs).unwrap_or(default_tag);
515
516            match &variant.fields {
517                Fields::Named(fields) => {
518                    let field_names: Vec<_> = fields
519                        .named
520                        .iter()
521                        .map(|f| f.ident.as_ref().unwrap())
522                        .collect();
523                    // For named fields, we wrap in a Record as the single payload element
524                    let field_to_value: Vec<_> = fields
525                        .named
526                        .iter()
527                        .map(|f| {
528                            let field_name = f.ident.as_ref().unwrap();
529                            let field_name_str =
530                                get_rename(&f.attrs).unwrap_or_else(|| field_name.to_string());
531                            quote! {
532                                (
533                                    #krate::__private::String::from(#field_name_str),
534                                    ::core::convert::Into::<#krate::Value>::into(#field_name)
535                                )
536                            }
537                        })
538                        .collect();
539
540                    quote! {
541                        #name::#variant_name { #(#field_names),* } => {
542                            #krate::Value::Variant {
543                                type_name: #krate::__private::String::from(#type_name_str),
544                                case_name: #krate::__private::String::from(#case_name_str),
545                                tag: #tag,
546                                payload: #krate::__private::vec![
547                                    #krate::Value::Record {
548                                        type_name: #krate::__private::String::from(#case_name_str),
549                                        fields: #krate::__private::vec![#(#field_to_value),*],
550                                    }
551                                ],
552                            }
553                        }
554                    }
555                }
556                Fields::Unnamed(fields) => {
557                    let field_names: Vec<_> = (0..fields.unnamed.len())
558                        .map(|i| format_ident!("f{}", i))
559                        .collect();
560
561                    // Payload is a vec of all the field values
562                    quote! {
563                        #name::#variant_name(#(#field_names),*) => {
564                            #krate::Value::Variant {
565                                type_name: #krate::__private::String::from(#type_name_str),
566                                case_name: #krate::__private::String::from(#case_name_str),
567                                tag: #tag,
568                                payload: #krate::__private::vec![
569                                    #(::core::convert::Into::<#krate::Value>::into(#field_names)),*
570                                ],
571                            }
572                        }
573                    }
574                }
575                Fields::Unit => {
576                    quote! {
577                        #name::#variant_name => {
578                            #krate::Value::Variant {
579                                type_name: #krate::__private::String::from(#type_name_str),
580                                case_name: #krate::__private::String::from(#case_name_str),
581                                tag: #tag,
582                                payload: #krate::__private::vec![],
583                            }
584                        }
585                    }
586                }
587            }
588        })
589        .collect();
590
591    // Generate match arms for TryFrom<Value> for T
592    let from_value_arms: Vec<_> = data.variants.iter().enumerate().map(|(default_tag, variant)| {
593        let variant_name = &variant.ident;
594        let tag = get_tag(&variant.attrs).unwrap_or(default_tag);
595
596        match &variant.fields {
597            Fields::Named(fields) => {
598                let field_from_value: Vec<_> = fields.named.iter().map(|f| {
599                    let field_name = f.ident.as_ref().unwrap();
600                    let field_name_str = get_rename(&f.attrs).unwrap_or_else(|| field_name.to_string());
601                    let field_type = &f.ty;
602                    let decode = decode_field(field_type, quote! { field_value }, krate);
603                    quote! {
604                        #field_name: {
605                            let field_value = record_fields.iter()
606                                .find(|(name, _)| name == #field_name_str)
607                                .map(|(_, v)| v.clone())
608                                .ok_or_else(|| #krate::ConversionError::MissingField(
609                                    #krate::__private::String::from(#field_name_str)
610                                ))?;
611                            #decode
612                                .map_err(|e| #krate::ConversionError::FieldError(
613                                    #krate::__private::String::from(#field_name_str),
614                                    #krate::__private::Box::new(e)
615                                ))?
616                        }
617                    }
618                }).collect();
619
620                quote! {
621                    #tag => {
622                        // For named fields, payload should contain a single Record
623                        if payload.len() != 1 {
624                            return #krate::__private::Err(#krate::ConversionError::WrongFieldCount {
625                                expected: 1,
626                                got: payload.len(),
627                            });
628                        }
629                        match &payload[0] {
630                            #krate::Value::Record { fields: record_fields, .. } => {
631                                #krate::__private::Ok(#name::#variant_name {
632                                    #(#field_from_value),*
633                                })
634                            }
635                            other => #krate::__private::Err(#krate::ConversionError::ExpectedRecord(
636                                #krate::__private::format!("{:?}", other)
637                            )),
638                        }
639                    }
640                }
641            }
642            Fields::Unnamed(fields) => {
643                let field_count = fields.unnamed.len();
644                let field_conversions: Vec<_> = fields.unnamed.iter().enumerate().map(|(i, f)| {
645                    let field_type = &f.ty;
646                    let decode = decode_field(
647                        field_type,
648                        quote! { payload.get(#i).cloned().ok_or_else(|| #krate::ConversionError::MissingIndex(#i))? },
649                        krate,
650                    );
651                    quote! {
652                        #decode.map_err(|e| #krate::ConversionError::IndexError(#i, #krate::__private::Box::new(e)))?
653                    }
654                }).collect();
655
656                quote! {
657                    #tag => {
658                        if payload.len() != #field_count {
659                            return #krate::__private::Err(#krate::ConversionError::WrongFieldCount {
660                                expected: #field_count,
661                                got: payload.len(),
662                            });
663                        }
664                        #krate::__private::Ok(#name::#variant_name(
665                            #(#field_conversions),*
666                        ))
667                    }
668                }
669            }
670            Fields::Unit => {
671                quote! {
672                    #tag => {
673                        if !payload.is_empty() {
674                            return #krate::__private::Err(#krate::ConversionError::UnexpectedPayload);
675                        }
676                        #krate::__private::Ok(#name::#variant_name)
677                    }
678                }
679            }
680        }
681    }).collect();
682
683    let variant_count = data.variants.len();
684
685    quote! {
686        impl #impl_generics #krate::__private::From<#name #ty_generics> for #krate::Value #where_clause {
687            fn from(value: #name #ty_generics) -> #krate::Value {
688                match value {
689                    #(#to_value_arms),*
690                }
691            }
692        }
693
694        impl #impl_generics #krate::__private::TryFrom<#krate::Value> for #name #ty_generics #where_clause {
695            type Error = #krate::ConversionError;
696
697            fn try_from(value: #krate::Value) -> #krate::__private::Result<Self, #krate::ConversionError> {
698                match value {
699                    #krate::Value::Variant { tag, payload, .. } => {
700                        match tag {
701                            #(#from_value_arms),*
702                            other => #krate::__private::Err(#krate::ConversionError::UnknownTag {
703                                tag: other,
704                                max: #variant_count,
705                            }),
706                        }
707                    }
708                    other => #krate::__private::Err(#krate::ConversionError::ExpectedVariant(
709                        #krate::__private::format!("{:?}", other)
710                    )),
711                }
712            }
713        }
714
715        impl #impl_generics #krate::KnownValueType for #name #ty_generics #where_clause {
716            fn known_value_type() -> #krate::ValueType {
717                #krate::ValueType::Variant(#krate::__private::String::from(#type_name_str))
718            }
719        }
720    }
721}
722
723/// Extract `#[graph(rename = "...")]` attribute
724fn get_rename(attrs: &[Attribute]) -> Option<String> {
725    for attr in attrs {
726        if attr.path().is_ident("graph") {
727            if let Meta::List(list) = &attr.meta {
728                let tokens = list.tokens.to_string();
729                // Parse rename = "..."
730                if let Some(rest) = tokens.strip_prefix("rename") {
731                    let rest = rest.trim();
732                    if let Some(rest) = rest.strip_prefix('=') {
733                        let rest = rest.trim();
734                        if rest.starts_with('"') && rest.ends_with('"') {
735                            return Some(rest[1..rest.len() - 1].to_string());
736                        }
737                    }
738                }
739            }
740        }
741    }
742    None
743}
744
745/// Extract `#[graph(tag = N)]` attribute
746fn get_tag(attrs: &[Attribute]) -> Option<usize> {
747    for attr in attrs {
748        if attr.path().is_ident("graph") {
749            if let Meta::List(list) = &attr.meta {
750                let tokens = list.tokens.to_string();
751                // Parse tag = N
752                if let Some(rest) = tokens.strip_prefix("tag") {
753                    let rest = rest.trim();
754                    if let Some(rest) = rest.strip_prefix('=') {
755                        let rest = rest.trim();
756                        return rest.parse().ok();
757                    }
758                }
759            }
760        }
761    }
762    None
763}
764
765#[cfg(test)]
766mod tests {
767    use super::{get_crate_path, has_forward_compatible};
768
769    // The COMBINED form must parse the crate correctly (not silently fall back to
770    // the default packr_abi — the bug the comma-split fix addresses) AND detect the
771    // forward_compatible flag.
772    #[test]
773    fn combined_crate_and_forward_compatible() {
774        let attr: syn::Attribute =
775            syn::parse_quote!(#[graph(crate = "foo::bar", forward_compatible)]);
776        let attrs = [attr];
777        assert_eq!(
778            get_crate_path(&attrs).to_string(),
779            quote::quote!(foo::bar).to_string(),
780            "combined form must parse the crate path, not default to packr_abi"
781        );
782        assert!(has_forward_compatible(&attrs));
783    }
784
785    #[test]
786    fn crate_only_parses_and_no_flag() {
787        let attrs = [syn::parse_quote!(#[graph(crate = "foo::bar")])];
788        assert_eq!(
789            get_crate_path(&attrs).to_string(),
790            quote::quote!(foo::bar).to_string()
791        );
792        assert!(!has_forward_compatible(&attrs));
793    }
794
795    #[test]
796    fn flag_only_defaults_crate() {
797        let attrs = [syn::parse_quote!(#[graph(forward_compatible)])];
798        assert_eq!(
799            get_crate_path(&attrs).to_string(),
800            quote::quote!(packr_abi).to_string()
801        );
802        assert!(has_forward_compatible(&attrs));
803    }
804}