tap_msg_derive/
lib.rs

1//! Procedural derive macro for implementing TAP message traits.
2//!
3//! This crate provides the `#[derive(TapMessage)]` macro that automatically
4//! implements both `TapMessage` and `MessageContext` traits based on field attributes.
5
6use proc_macro::TokenStream;
7use proc_macro2::TokenStream as TokenStream2;
8use quote::quote;
9use syn::{parse_macro_input, Data, DeriveInput, Field, Fields};
10
11/// Procedural derive macro for implementing TapMessage, MessageContext, and optionally TapMessageBody traits.
12///
13/// # Usage
14///
15/// ## Basic Usage (TapMessage + MessageContext only)
16///
17/// ```ignore
18/// use tap_msg::TapMessage;
19/// use tap_msg::message::{Party, Agent};
20/// use tap_caip::AssetId;
21///
22/// #[derive(TapMessage)]
23/// pub struct Transfer {
24///     #[tap(participant)]
25///     pub originator: Party,
26///     
27///     #[tap(participant)]
28///     pub beneficiary: Option<Party>,
29///     
30///     #[tap(participant_list)]
31///     pub agents: Vec<Agent>,
32///     
33///     #[tap(transaction_id)]
34///     pub transaction_id: String,
35///     
36///     // regular fields don't need attributes
37///     pub amount: String,
38///     pub asset: AssetId,
39/// }
40/// ```
41///
42/// ## Full Usage (includes TapMessageBody with auto-generated to_didcomm)
43///
44/// ```ignore
45/// use tap_msg::TapMessage;
46/// use tap_msg::message::{Party, Agent};
47/// use serde::{Serialize, Deserialize};
48///
49/// #[derive(Debug, Clone, Serialize, Deserialize, TapMessage)]
50/// #[tap(message_type = "https://tap.rsvp/schema/1.0#Transfer", initiator, authorizable, transactable)]
51/// pub struct Transfer {
52///     #[tap(participant)]
53///     pub originator: Party,
54///     
55///     #[tap(participant)]
56///     pub beneficiary: Option<Party>,
57///     
58///     #[tap(participant_list)]
59///     pub agents: Vec<Agent>,
60///     
61///     #[tap(transaction_id)]
62///     pub transaction_id: String,
63///     
64///     pub amount: String,
65/// }
66///
67/// // TapMessageBody is automatically implemented with:
68/// // - message_type() returning the specified string
69/// // - validate() with basic validation (can be overridden)
70/// // - to_didcomm() with automatic participant extraction and message construction
71/// // - Authorizable trait (if authorizable attribute is present)
72/// // - Transaction trait (if transactable attribute is present)
73/// ```
74///
75/// # Supported Attributes
76///
77/// ## Struct-level Attributes
78/// - `#[tap(message_type = "url")]` - TAP message type URL (enables TapMessageBody generation)
79/// - `#[tap(initiator)]` - Marks this as a conversation-initiating message
80/// - `#[tap(authorizable)]` - Auto-generates Authorizable trait implementation
81/// - `#[tap(transactable)]` - Auto-generates Transaction trait implementation
82/// - `#[tap(builder)]` - Auto-generates builder pattern
83///
84/// ## Field-level Attributes
85/// - `#[tap(participant)]` - Single participant field (Party or Agent, required or optional)
86/// - `#[tap(participant_list)]` - Vec<Agent> field for lists of agents
87/// - `#[tap(transaction_id)]` - Transaction ID field (creates new transaction for initiators)
88/// - `#[tap(thread_id)]` - Thread ID field (references existing transaction for replies)
89/// - `#[tap(connection_id)]` - Connection ID field (for linking to Connect messages)
90#[proc_macro_derive(TapMessage, attributes(tap))]
91pub fn derive_tap_message(input: TokenStream) -> TokenStream {
92    let input = parse_macro_input!(input as DeriveInput);
93    let expanded = impl_tap_message(&input);
94    TokenStream::from(expanded)
95}
96
97#[proc_macro_derive(TapMessageBody, attributes(tap))]
98pub fn derive_tap_message_body(input: TokenStream) -> TokenStream {
99    let input = parse_macro_input!(input as DeriveInput);
100    let expanded = impl_tap_message_body_only(&input);
101    TokenStream::from(expanded)
102}
103
104fn impl_tap_message(input: &DeriveInput) -> TokenStream2 {
105    let name = &input.ident;
106    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
107
108    let fields = match &input.data {
109        Data::Struct(data) => match &data.fields {
110            Fields::Named(fields) => &fields.named,
111            _ => panic!("TapMessage can only be derived for structs with named fields"),
112        },
113        _ => panic!("TapMessage can only be derived for structs"),
114    };
115
116    let field_info = analyze_fields(fields, &input.attrs);
117
118    // Check if we're inside the tap-msg crate or external
119    let is_internal = std::env::var("CARGO_CRATE_NAME").unwrap_or_default() == "tap_msg";
120
121    let tap_message_body_impl = if field_info.message_type.is_some() {
122        impl_tap_message_body_trait(
123            name,
124            &field_info,
125            &impl_generics,
126            &ty_generics,
127            where_clause,
128            is_internal,
129        )
130    } else {
131        quote! {}
132    };
133
134    let tap_message_impl = impl_tap_message_trait(
135        name,
136        &field_info,
137        &impl_generics,
138        &ty_generics,
139        where_clause,
140        is_internal,
141    );
142
143    let message_context_impl = impl_message_context_trait(
144        name,
145        &field_info,
146        &impl_generics,
147        &ty_generics,
148        where_clause,
149        is_internal,
150    );
151
152    let authorizable_impl = if field_info.is_authorizable {
153        impl_authorizable_trait(
154            name,
155            &field_info,
156            &impl_generics,
157            &ty_generics,
158            where_clause,
159            is_internal,
160        )
161    } else {
162        quote! {}
163    };
164
165    let transaction_impl = if field_info.is_transactable {
166        impl_transaction_trait(
167            name,
168            &field_info,
169            &impl_generics,
170            &ty_generics,
171            where_clause,
172            is_internal,
173        )
174    } else {
175        quote! {}
176    };
177
178    let connectable_impl = if field_info.connection_id_field.is_some() || field_info.is_initiator {
179        impl_connectable_trait(
180            name,
181            &field_info,
182            &impl_generics,
183            &ty_generics,
184            where_clause,
185            is_internal,
186        )
187    } else {
188        quote! {}
189    };
190
191    quote! {
192        #tap_message_impl
193        #message_context_impl
194        #tap_message_body_impl
195        #authorizable_impl
196        #transaction_impl
197        #connectable_impl
198    }
199}
200
201fn impl_connectable_trait(
202    name: &syn::Ident,
203    field_info: &FieldInfo,
204    impl_generics: &syn::ImplGenerics,
205    ty_generics: &syn::TypeGenerics,
206    where_clause: Option<&syn::WhereClause>,
207    is_internal: bool,
208) -> TokenStream2 {
209    let crate_path = if is_internal {
210        quote! { crate }
211    } else {
212        quote! { ::tap_msg }
213    };
214
215    if let Some(ref conn_field) = field_info.connection_id_field {
216        // Has explicit connection_id field
217        quote! {
218            impl #impl_generics #crate_path::message::tap_message_trait::Connectable for #name #ty_generics #where_clause {
219                fn with_connection(&mut self, connection_id: &str) -> &mut Self {
220                    self.#conn_field = Some(connection_id.to_string());
221                    self
222                }
223
224                fn has_connection(&self) -> bool {
225                    self.#conn_field.is_some()
226                }
227
228                fn connection_id(&self) -> Option<&str> {
229                    self.#conn_field.as_deref()
230                }
231            }
232        }
233    } else {
234        // Initiator messages don't have connections
235        quote! {
236            impl #impl_generics #crate_path::message::tap_message_trait::Connectable for #name #ty_generics #where_clause {
237                fn with_connection(&mut self, _connection_id: &str) -> &mut Self {
238                    // Initiator messages don't have connection IDs
239                    self
240                }
241
242                fn has_connection(&self) -> bool {
243                    false
244                }
245
246                fn connection_id(&self) -> Option<&str> {
247                    None
248                }
249            }
250        }
251    }
252}
253
254#[derive(Debug)]
255struct FieldInfo {
256    participant_fields: Vec<syn::Ident>,
257    optional_participant_fields: Vec<syn::Ident>,
258    participant_list_fields: Vec<syn::Ident>,
259    transaction_id_field: Option<syn::Ident>,
260    optional_transaction_id_field: Option<syn::Ident>,
261    thread_id_field: Option<syn::Ident>,
262    optional_thread_id_field: Option<syn::Ident>,
263    connection_id_field: Option<syn::Ident>,
264    has_generated_id: bool,
265    message_type: Option<String>,
266    is_initiator: bool,
267    is_authorizable: bool,
268    is_transactable: bool,
269    generate_builder: bool,
270    custom_validation: bool,
271}
272
273fn analyze_fields(
274    fields: &syn::punctuated::Punctuated<Field, syn::Token![,]>,
275    struct_attrs: &[syn::Attribute],
276) -> FieldInfo {
277    let mut field_info = FieldInfo {
278        participant_fields: Vec::new(),
279        optional_participant_fields: Vec::new(),
280        participant_list_fields: Vec::new(),
281        transaction_id_field: None,
282        optional_transaction_id_field: None,
283        thread_id_field: None,
284        optional_thread_id_field: None,
285        connection_id_field: None,
286        has_generated_id: false,
287        message_type: None,
288        is_initiator: false,
289        is_authorizable: false,
290        is_transactable: false,
291        generate_builder: false,
292        custom_validation: false,
293    };
294
295    // First check struct-level attributes
296    for attr in struct_attrs {
297        if attr.path().is_ident("tap") {
298            let _ = attr.parse_nested_meta(|meta| {
299                if meta.path.is_ident("generated_id") {
300                    field_info.has_generated_id = true;
301                } else if meta.path.is_ident("message_type") {
302                    if let Ok(lit) = meta.value() {
303                        if let Ok(lit_str) = lit.parse::<syn::LitStr>() {
304                            field_info.message_type = Some(lit_str.value());
305                        }
306                    }
307                } else if meta.path.is_ident("initiator") {
308                    field_info.is_initiator = true;
309                } else if meta.path.is_ident("authorizable") {
310                    field_info.is_authorizable = true;
311                } else if meta.path.is_ident("transactable") {
312                    field_info.is_transactable = true;
313                } else if meta.path.is_ident("builder") {
314                    field_info.generate_builder = true;
315                } else if meta.path.is_ident("custom_validation") {
316                    field_info.custom_validation = true;
317                }
318                Ok(())
319            });
320        }
321    }
322
323    for field in fields {
324        let field_name = field.ident.as_ref().expect("Field must have a name");
325
326        for attr in &field.attrs {
327            if attr.path().is_ident("tap") {
328                let _ = attr.parse_nested_meta(|meta| {
329                    if meta.path.is_ident("participant") {
330                        // Check if the field type is Option<Participant>
331                        if is_optional_type(&field.ty) {
332                            field_info
333                                .optional_participant_fields
334                                .push(field_name.clone());
335                        } else {
336                            field_info.participant_fields.push(field_name.clone());
337                        }
338                    } else if meta.path.is_ident("participant_list") {
339                        field_info.participant_list_fields.push(field_name.clone());
340                    } else if meta.path.is_ident("transaction_id") {
341                        field_info.transaction_id_field = Some(field_name.clone());
342                    } else if meta.path.is_ident("optional_transaction_id") {
343                        field_info.optional_transaction_id_field = Some(field_name.clone());
344                    } else if meta.path.is_ident("thread_id") {
345                        // Check if the field type is Option<String>
346                        if is_optional_type(&field.ty) {
347                            field_info.optional_thread_id_field = Some(field_name.clone());
348                        } else {
349                            field_info.thread_id_field = Some(field_name.clone());
350                        }
351                    } else if meta.path.is_ident("connection_id") {
352                        field_info.connection_id_field = Some(field_name.clone());
353                    } else if meta.path.is_ident("generated_id") {
354                        field_info.has_generated_id = true;
355                    }
356                    Ok(())
357                });
358            }
359        }
360    }
361
362    field_info
363}
364
365fn is_optional_type(ty: &syn::Type) -> bool {
366    if let syn::Type::Path(type_path) = ty {
367        if let Some(segment) = type_path.path.segments.last() {
368            return segment.ident == "Option";
369        }
370    }
371    false
372}
373
374fn impl_tap_message_trait(
375    name: &syn::Ident,
376    field_info: &FieldInfo,
377    impl_generics: &syn::ImplGenerics,
378    ty_generics: &syn::TypeGenerics,
379    where_clause: Option<&syn::WhereClause>,
380    is_internal: bool,
381) -> TokenStream2 {
382    let thread_id_impl = generate_thread_id_impl(field_info);
383    let message_id_impl = generate_message_id_impl(field_info);
384    let get_all_participants_impl = generate_get_all_participants_impl(field_info);
385
386    let crate_path = if is_internal {
387        quote! { crate }
388    } else {
389        quote! { ::tap_msg }
390    };
391
392    // message_type is no longer part of TapMessage trait
393
394    quote! {
395        impl #impl_generics #crate_path::message::tap_message_trait::TapMessage for #name #ty_generics #where_clause {
396            fn validate(&self) -> #crate_path::error::Result<()> {
397                <Self as #crate_path::message::tap_message_trait::TapMessageBody>::validate(self)
398            }
399
400            fn is_tap_message(&self) -> bool {
401                <Self as #crate_path::message::tap_message_trait::TapMessageBody>::message_type()
402                    .starts_with("https://tap.rsvp/schema/1.0#")
403            }
404
405            fn get_tap_type(&self) -> Option<String> {
406                Some(
407                    <Self as #crate_path::message::tap_message_trait::TapMessageBody>::message_type()
408                        .to_string(),
409                )
410            }
411
412            fn body_as<T: #crate_path::message::tap_message_trait::TapMessageBody>(
413                &self,
414            ) -> #crate_path::error::Result<T> {
415                unimplemented!()
416            }
417
418            fn get_all_participants(&self) -> Vec<String> {
419                // Import TapParticipant trait for .id() method access
420                use #crate_path::message::agent::TapParticipant;
421                #get_all_participants_impl
422            }
423
424            fn create_reply<T: #crate_path::message::tap_message_trait::TapMessageBody>(
425                &self,
426                body: &T,
427                creator_did: &str,
428            ) -> #crate_path::error::Result<#crate_path::didcomm::PlainMessage> {
429                // Create the base message with creator as sender
430                let mut message = body.to_didcomm(creator_did)?;
431
432                // message is already PlainMessage<Value> from to_didcomm
433
434                // Set the thread ID to maintain the conversation thread
435                if let Some(thread_id) = self.thread_id() {
436                    message.thid = Some(thread_id.to_string());
437                } else {
438                    // If no thread ID exists, use the original message ID as the thread ID
439                    message.thid = Some(self.message_id().to_string());
440                }
441
442                // Set the parent thread ID if this thread is part of a larger transaction
443                if let Some(parent_thread_id) = self.parent_thread_id() {
444                    message.pthid = Some(parent_thread_id.to_string());
445                }
446
447                Ok(message)
448            }
449
450
451            fn thread_id(&self) -> Option<&str> {
452                #thread_id_impl
453            }
454
455            fn parent_thread_id(&self) -> Option<&str> {
456                None
457            }
458
459            fn message_id(&self) -> &str {
460                #message_id_impl
461            }
462        }
463    }
464}
465
466fn impl_message_context_trait(
467    name: &syn::Ident,
468    field_info: &FieldInfo,
469    impl_generics: &syn::ImplGenerics,
470    ty_generics: &syn::TypeGenerics,
471    where_clause: Option<&syn::WhereClause>,
472    is_internal: bool,
473) -> TokenStream2 {
474    let participant_dids_impl = generate_participant_dids_impl(field_info);
475    let transaction_context_impl = generate_transaction_context_impl(field_info, is_internal);
476
477    let crate_path = if is_internal {
478        quote! { crate }
479    } else {
480        quote! { ::tap_msg }
481    };
482
483    quote! {
484        impl #impl_generics #crate_path::message::MessageContext for #name #ty_generics #where_clause {
485            fn participant_dids(&self) -> Vec<String> {
486                // Import TapParticipant trait for .id() method access
487                use #crate_path::message::agent::TapParticipant;
488                #participant_dids_impl
489            }
490
491            fn transaction_context(&self) -> Option<#crate_path::message::TransactionContext> {
492                #transaction_context_impl
493            }
494        }
495    }
496}
497
498fn generate_participant_dids_impl(field_info: &FieldInfo) -> TokenStream2 {
499    let mut did_extracts = Vec::new();
500
501    // Add required participants (Agent, Party, or Participant types)
502    for field in &field_info.participant_fields {
503        did_extracts.push(quote! {
504            // Use the TapParticipant trait to get ID - works with Agent, Party, or Participant
505            dids.push(self.#field.id().to_string());
506        });
507    }
508
509    // Add optional participants
510    for field in &field_info.optional_participant_fields {
511        did_extracts.push(quote! {
512            if let Some(ref participant) = self.#field {
513                dids.push(participant.id().to_string());
514            }
515        });
516    }
517
518    // Add participant lists (typically Vec<Agent> or Vec<Participant>)
519    for field in &field_info.participant_list_fields {
520        did_extracts.push(quote! {
521            for participant in &self.#field {
522                dids.push(participant.id().to_string());
523            }
524        });
525    }
526
527    quote! {
528        let mut dids = Vec::new();
529        #(#did_extracts)*
530        dids
531    }
532}
533
534fn generate_get_all_participants_impl(field_info: &FieldInfo) -> TokenStream2 {
535    let mut participant_extracts = Vec::new();
536
537    // Add required participants
538    for field in &field_info.participant_fields {
539        participant_extracts.push(quote! {
540            participants.push(self.#field.id().to_string());
541        });
542    }
543
544    // Add optional participants
545    for field in &field_info.optional_participant_fields {
546        participant_extracts.push(quote! {
547            if let Some(ref participant) = self.#field {
548                participants.push(participant.id().to_string());
549            }
550        });
551    }
552
553    // Add participant lists
554    for field in &field_info.participant_list_fields {
555        participant_extracts.push(quote! {
556            for participant in &self.#field {
557                participants.push(participant.id().to_string());
558            }
559        });
560    }
561
562    quote! {
563        let mut participants = Vec::new();
564        #(#participant_extracts)*
565        participants
566    }
567}
568
569fn generate_thread_id_impl(field_info: &FieldInfo) -> TokenStream2 {
570    if let Some(ref thread_field) = field_info.thread_id_field {
571        quote! { Some(&self.#thread_field) }
572    } else if let Some(ref opt_thread_field) = field_info.optional_thread_id_field {
573        quote! { self.#opt_thread_field.as_deref() }
574    } else if field_info.is_initiator {
575        // Initiators don't have a thread_id - they start the thread
576        quote! { None }
577    } else if let Some(ref tx_field) = field_info.transaction_id_field {
578        quote! { Some(&self.#tx_field) }
579    } else if let Some(ref opt_tx_field) = field_info.optional_transaction_id_field {
580        quote! { self.#opt_tx_field.as_deref() }
581    } else {
582        quote! { None }
583    }
584}
585
586fn generate_message_id_impl(field_info: &FieldInfo) -> TokenStream2 {
587    if let Some(tx_field) = &field_info.transaction_id_field {
588        quote! { &self.#tx_field }
589    } else if let Some(thread_field) = &field_info.thread_id_field {
590        quote! { &self.#thread_field }
591    } else if let Some(opt_tx_field) = &field_info.optional_transaction_id_field {
592        quote! {
593            self.#opt_tx_field.as_deref().unwrap_or("")
594        }
595    } else {
596        quote! {
597            // For types without an ID field, we'll use a static string
598            // This isn't ideal but it satisfies the API contract
599            static FALLBACK_ID: &str = "00000000-0000-0000-0000-000000000000";
600            FALLBACK_ID
601        }
602    }
603}
604
605fn generate_transaction_context_impl(field_info: &FieldInfo, is_internal: bool) -> TokenStream2 {
606    let crate_path = if is_internal {
607        quote! { crate }
608    } else {
609        quote! { ::tap_msg }
610    };
611
612    if let Some(tx_field) = &field_info.transaction_id_field {
613        quote! {
614            Some(#crate_path::message::TransactionContext::new(
615                self.#tx_field.clone(),
616                <Self as #crate_path::message::tap_message_trait::TapMessageBody>::message_type().to_string(),
617            ))
618        }
619    } else if let Some(opt_tx_field) = &field_info.optional_transaction_id_field {
620        quote! {
621            self.#opt_tx_field.as_ref().map(|tx_id| {
622                #crate_path::message::TransactionContext::new(
623                    tx_id.clone(),
624                    <Self as #crate_path::message::tap_message_trait::TapMessageBody>::message_type().to_string(),
625                )
626            })
627        }
628    } else {
629        quote! { None }
630    }
631}
632
633fn impl_tap_message_body_trait(
634    name: &syn::Ident,
635    field_info: &FieldInfo,
636    impl_generics: &syn::ImplGenerics,
637    ty_generics: &syn::TypeGenerics,
638    where_clause: Option<&syn::WhereClause>,
639    is_internal: bool,
640) -> TokenStream2 {
641    let crate_path = if is_internal {
642        quote! { crate }
643    } else {
644        quote! { ::tap_msg }
645    };
646
647    let message_type = field_info
648        .message_type
649        .as_ref()
650        .expect("message_type attribute is required for TapMessageBody");
651
652    let validate_impl = if field_info.custom_validation {
653        // If custom_validation is specified, delegate to a validate_<struct_name_lowercase> method
654        let method_name = syn::Ident::new(
655            &format!("validate_{}", name.to_string().to_lowercase()),
656            name.span(),
657        );
658        quote! {
659            self.#method_name()
660        }
661    } else {
662        quote! {
663            // Basic validation - users can override this by implementing custom validation
664            Ok(())
665        }
666    };
667
668    let to_didcomm_impl = generate_to_didcomm_impl(field_info, is_internal);
669
670    quote! {
671        impl #impl_generics #crate_path::message::tap_message_trait::TapMessageBody for #name #ty_generics #where_clause {
672            fn message_type() -> &'static str {
673                #message_type
674            }
675
676            fn validate(&self) -> #crate_path::error::Result<()> {
677                #validate_impl
678            }
679
680            fn to_didcomm(&self, from_did: &str) -> #crate_path::error::Result<#crate_path::didcomm::PlainMessage> {
681                #to_didcomm_impl
682            }
683        }
684    }
685}
686
687fn generate_to_didcomm_impl(field_info: &FieldInfo, is_internal: bool) -> TokenStream2 {
688    let crate_path = if is_internal {
689        quote! { crate }
690    } else {
691        quote! { ::tap_msg }
692    };
693
694    // Generate participant extraction
695    let participant_extraction = if !field_info.participant_fields.is_empty()
696        || !field_info.optional_participant_fields.is_empty()
697        || !field_info.participant_list_fields.is_empty()
698    {
699        let mut extracts = Vec::new();
700
701        // Required participants
702        for field in &field_info.participant_fields {
703            extracts.push(quote! {
704                recipient_dids.push(self.#field.id().to_string());
705            });
706        }
707
708        // Optional participants
709        for field in &field_info.optional_participant_fields {
710            extracts.push(quote! {
711                if let Some(ref participant) = self.#field {
712                    recipient_dids.push(participant.id().to_string());
713                }
714            });
715        }
716
717        // Participant lists
718        for field in &field_info.participant_list_fields {
719            extracts.push(quote! {
720                for participant in &self.#field {
721                    recipient_dids.push(participant.id().to_string());
722                }
723            });
724        }
725
726        quote! {
727            let mut recipient_dids = Vec::new();
728            #(#extracts)*
729
730            // Remove duplicates and sender
731            recipient_dids.sort();
732            recipient_dids.dedup();
733            recipient_dids.retain(|did| did != from_did);
734        }
735    } else {
736        quote! {
737            let recipient_dids: Vec<String> = Vec::new();
738        }
739    };
740
741    // Generate thread ID assignment
742    let thread_assignment = if field_info.is_initiator {
743        // Initiators create a new thread - their transaction_id becomes the thid for replies
744        if let Some(tx_field) = &field_info.transaction_id_field {
745            quote! {
746                thid: Some(self.#tx_field.clone()),
747            }
748        } else {
749            quote! {
750                thid: None,
751            }
752        }
753    } else if let Some(thread_field) = &field_info.thread_id_field {
754        // Reply messages use thread_id to reference the existing transaction
755        quote! {
756            thid: Some(self.#thread_field.clone()),
757        }
758    } else if let Some(opt_thread_field) = &field_info.optional_thread_id_field {
759        // Optional thread_id field
760        quote! {
761            thid: self.#opt_thread_field.clone(),
762        }
763    } else if let Some(tx_field) = &field_info.transaction_id_field {
764        // Fallback for backwards compatibility
765        quote! {
766            thid: Some(self.#tx_field.clone()),
767        }
768    } else if let Some(opt_tx_field) = &field_info.optional_transaction_id_field {
769        quote! {
770            thid: self.#opt_tx_field.clone(),
771        }
772    } else {
773        quote! {
774            thid: None,
775        }
776    };
777
778    // Generate pthid assignment for connection linking
779    let pthid_assignment = if let Some(conn_field) = &field_info.connection_id_field {
780        quote! {
781            pthid: self.#conn_field.clone(),
782        }
783    } else {
784        quote! {
785            pthid: None,
786        }
787    };
788
789    quote! {
790        // Import TapParticipant trait for .id() method access
791        use #crate_path::message::agent::TapParticipant;
792
793        // Serialize the message body to JSON
794        let mut body_json = serde_json::to_value(self)
795            .map_err(|e| #crate_path::error::Error::SerializationError(e.to_string()))?;
796
797        // Ensure the @type field is correctly set in the body
798        if let Some(body_obj) = body_json.as_object_mut() {
799            body_obj.insert(
800                "@type".to_string(),
801                serde_json::Value::String(Self::message_type().to_string()),
802            );
803        }
804
805        // Extract recipient DIDs from participants
806        #participant_extraction
807
808        let now = chrono::Utc::now().timestamp() as u64;
809
810        // Create the PlainMessage
811        Ok(#crate_path::didcomm::PlainMessage {
812            id: uuid::Uuid::new_v4().to_string(),
813            typ: "application/didcomm-plain+json".to_string(),
814            type_: Self::message_type().to_string(),
815            body: body_json,
816            from: from_did.to_string(),
817            to: recipient_dids,
818            #thread_assignment
819            #pthid_assignment
820            created_time: Some(now),
821            expires_time: None,
822            extra_headers: std::collections::HashMap::new(),
823            from_prior: None,
824            attachments: None,
825        })
826    }
827}
828
829fn impl_tap_message_body_only(input: &DeriveInput) -> TokenStream2 {
830    let name = &input.ident;
831    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
832
833    let fields = match &input.data {
834        Data::Struct(data) => match &data.fields {
835            Fields::Named(fields) => &fields.named,
836            _ => panic!("TapMessageBody can only be derived for structs with named fields"),
837        },
838        _ => panic!("TapMessageBody can only be derived for structs"),
839    };
840
841    let field_info = analyze_fields(fields, &input.attrs);
842
843    // Check if we're inside the tap-msg crate or external
844    let is_internal = std::env::var("CARGO_CRATE_NAME").unwrap_or_default() == "tap_msg";
845
846    // TapMessageBody can only be derived if message_type is specified
847    if field_info.message_type.is_none() {
848        panic!("TapMessageBody derive macro requires #[tap(message_type = \"...\")] attribute");
849    }
850
851    impl_tap_message_body_trait(
852        name,
853        &field_info,
854        &impl_generics,
855        &ty_generics,
856        where_clause,
857        is_internal,
858    )
859}
860
861fn impl_authorizable_trait(
862    name: &syn::Ident,
863    field_info: &FieldInfo,
864    impl_generics: &syn::ImplGenerics,
865    ty_generics: &syn::TypeGenerics,
866    where_clause: Option<&syn::WhereClause>,
867    is_internal: bool,
868) -> TokenStream2 {
869    let crate_path = if is_internal {
870        quote! { crate }
871    } else {
872        quote! { ::tap_msg }
873    };
874
875    // Get the transaction ID field access
876    let tx_id_access = if let Some(ref tx_field) = field_info.transaction_id_field {
877        quote! { &self.#tx_field }
878    } else if let Some(ref thread_field) = field_info.thread_id_field {
879        quote! { &self.#thread_field }
880    } else {
881        quote! { "" }
882    };
883
884    quote! {
885        impl #impl_generics #crate_path::message::tap_message_trait::Authorizable for #name #ty_generics #where_clause {
886            fn authorize(
887                &self,
888                creator_did: &str,
889                settlement_address: Option<&str>,
890                expiry: Option<&str>,
891            ) -> #crate_path::didcomm::PlainMessage<#crate_path::message::Authorize> {
892                let authorize = #crate_path::message::Authorize::with_all(#tx_id_access, settlement_address, expiry);
893                let original_message = self
894                    .to_didcomm(creator_did)
895                    .expect("Failed to create DIDComm message");
896                let reply = original_message
897                    .create_reply(&authorize, creator_did)
898                    .expect("Failed to create reply");
899                #crate_path::message::tap_message_trait::typed_plain_message(reply, authorize)
900            }
901
902            fn cancel(&self, creator_did: &str, by: &str, reason: Option<&str>) -> #crate_path::didcomm::PlainMessage<#crate_path::message::Cancel> {
903                let cancel = if let Some(reason) = reason {
904                    #crate_path::message::Cancel::with_reason(#tx_id_access, by, reason)
905                } else {
906                    #crate_path::message::Cancel::new(#tx_id_access, by)
907                };
908                let original_message = self
909                    .to_didcomm(creator_did)
910                    .expect("Failed to create DIDComm message");
911                let reply = original_message
912                    .create_reply(&cancel, creator_did)
913                    .expect("Failed to create reply");
914                #crate_path::message::tap_message_trait::typed_plain_message(reply, cancel)
915            }
916
917            fn reject(&self, creator_did: &str, reason: &str) -> #crate_path::didcomm::PlainMessage<#crate_path::message::Reject> {
918                let reject = #crate_path::message::Reject {
919                    transaction_id: (#tx_id_access).to_string(),
920                    reason: reason.to_string(),
921                };
922                let original_message = self
923                    .to_didcomm(creator_did)
924                    .expect("Failed to create DIDComm message");
925                let reply = original_message
926                    .create_reply(&reject, creator_did)
927                    .expect("Failed to create reply");
928                #crate_path::message::tap_message_trait::typed_plain_message(reply, reject)
929            }
930        }
931    }
932}
933
934fn impl_transaction_trait(
935    name: &syn::Ident,
936    field_info: &FieldInfo,
937    impl_generics: &syn::ImplGenerics,
938    ty_generics: &syn::TypeGenerics,
939    where_clause: Option<&syn::WhereClause>,
940    is_internal: bool,
941) -> TokenStream2 {
942    let crate_path = if is_internal {
943        quote! { crate }
944    } else {
945        quote! { ::tap_msg }
946    };
947
948    // Get the transaction ID field access
949    let tx_id_access = if let Some(ref tx_field) = field_info.transaction_id_field {
950        quote! { &self.#tx_field }
951    } else if let Some(ref thread_field) = field_info.thread_id_field {
952        quote! { &self.#thread_field }
953    } else {
954        quote! { "" }
955    };
956
957    quote! {
958        impl #impl_generics #crate_path::message::tap_message_trait::Transaction for #name #ty_generics #where_clause {
959            fn settle(
960                &self,
961                creator_did: &str,
962                settlement_id: &str,
963                amount: Option<&str>,
964            ) -> #crate_path::didcomm::PlainMessage<#crate_path::message::Settle> {
965                let settle = #crate_path::message::Settle {
966                    transaction_id: (#tx_id_access).to_string(),
967                    settlement_id: settlement_id.to_string(),
968                    amount: amount.map(|s| s.to_string()),
969                };
970                let original_message = self
971                    .to_didcomm(creator_did)
972                    .expect("Failed to create DIDComm message");
973                let reply = original_message
974                    .create_reply(&settle, creator_did)
975                    .expect("Failed to create reply");
976                #crate_path::message::tap_message_trait::typed_plain_message(reply, settle)
977            }
978
979            fn revert(
980                &self,
981                creator_did: &str,
982                settlement_address: &str,
983                reason: &str,
984            ) -> #crate_path::didcomm::PlainMessage<#crate_path::message::Revert> {
985                let revert = #crate_path::message::Revert {
986                    transaction_id: (#tx_id_access).to_string(),
987                    settlement_address: settlement_address.to_string(),
988                    reason: reason.to_string(),
989                };
990                let original_message = self
991                    .to_didcomm(creator_did)
992                    .expect("Failed to create DIDComm message");
993                let reply = original_message
994                    .create_reply(&revert, creator_did)
995                    .expect("Failed to create reply");
996                #crate_path::message::tap_message_trait::typed_plain_message(reply, revert)
997            }
998
999            fn add_agents(&self, creator_did: &str, agents: Vec<#crate_path::message::Agent>) -> #crate_path::didcomm::PlainMessage<#crate_path::message::AddAgents> {
1000                let add_agents = #crate_path::message::AddAgents {
1001                    transaction_id: (#tx_id_access).to_string(),
1002                    agents,
1003                };
1004                let original_message = self
1005                    .to_didcomm(creator_did)
1006                    .expect("Failed to create DIDComm message");
1007                let reply = original_message
1008                    .create_reply(&add_agents, creator_did)
1009                    .expect("Failed to create reply");
1010                #crate_path::message::tap_message_trait::typed_plain_message(reply, add_agents)
1011            }
1012
1013            fn replace_agent(
1014                &self,
1015                creator_did: &str,
1016                original_agent: &str,
1017                replacement: #crate_path::message::Agent,
1018            ) -> #crate_path::didcomm::PlainMessage<#crate_path::message::ReplaceAgent> {
1019                let replace_agent = #crate_path::message::ReplaceAgent {
1020                    transaction_id: (#tx_id_access).to_string(),
1021                    original: original_agent.to_string(),
1022                    replacement,
1023                };
1024                let original_message = self
1025                    .to_didcomm(creator_did)
1026                    .expect("Failed to create DIDComm message");
1027                let reply = original_message
1028                    .create_reply(&replace_agent, creator_did)
1029                    .expect("Failed to create reply");
1030                #crate_path::message::tap_message_trait::typed_plain_message(reply, replace_agent)
1031            }
1032
1033            fn remove_agent(&self, creator_did: &str, agent: &str) -> #crate_path::didcomm::PlainMessage<#crate_path::message::RemoveAgent> {
1034                let remove_agent = #crate_path::message::RemoveAgent {
1035                    transaction_id: (#tx_id_access).to_string(),
1036                    agent: agent.to_string(),
1037                };
1038                let original_message = self
1039                    .to_didcomm(creator_did)
1040                    .expect("Failed to create DIDComm message");
1041                let reply = original_message
1042                    .create_reply(&remove_agent, creator_did)
1043                    .expect("Failed to create reply");
1044                #crate_path::message::tap_message_trait::typed_plain_message(reply, remove_agent)
1045            }
1046
1047            fn update_party(
1048                &self,
1049                creator_did: &str,
1050                party_type: &str,
1051                party: #crate_path::message::Party,
1052            ) -> #crate_path::didcomm::PlainMessage<#crate_path::message::UpdateParty> {
1053                let update_party = #crate_path::message::UpdateParty {
1054                    transaction_id: (#tx_id_access).to_string(),
1055                    party_type: party_type.to_string(),
1056                    party,
1057                    context: None,
1058                };
1059                let original_message = self
1060                    .to_didcomm(creator_did)
1061                    .expect("Failed to create DIDComm message");
1062                let reply = original_message
1063                    .create_reply(&update_party, creator_did)
1064                    .expect("Failed to create reply");
1065                #crate_path::message::tap_message_trait::typed_plain_message(reply, update_party)
1066            }
1067
1068            fn update_policies(
1069                &self,
1070                creator_did: &str,
1071                policies: Vec<#crate_path::message::Policy>,
1072            ) -> #crate_path::didcomm::PlainMessage<#crate_path::message::UpdatePolicies> {
1073                let update_policies = #crate_path::message::UpdatePolicies {
1074                    transaction_id: (#tx_id_access).to_string(),
1075                    policies,
1076                };
1077                let original_message = self
1078                    .to_didcomm(creator_did)
1079                    .expect("Failed to create DIDComm message");
1080                let reply = original_message
1081                    .create_reply(&update_policies, creator_did)
1082                    .expect("Failed to create reply");
1083                #crate_path::message::tap_message_trait::typed_plain_message(reply, update_policies)
1084            }
1085
1086            fn confirm_relationship(
1087                &self,
1088                creator_did: &str,
1089                agent_did: &str,
1090                relationship_type: &str,
1091            ) -> #crate_path::didcomm::PlainMessage<#crate_path::message::ConfirmRelationship> {
1092                let confirm_relationship = #crate_path::message::ConfirmRelationship {
1093                    transaction_id: (#tx_id_access).to_string(),
1094                    agent_id: agent_did.to_string(),
1095                    relationship_type: relationship_type.to_string(),
1096                };
1097                let original_message = self
1098                    .to_didcomm(creator_did)
1099                    .expect("Failed to create DIDComm message");
1100                let reply = original_message
1101                    .create_reply(&confirm_relationship, creator_did)
1102                    .expect("Failed to create reply");
1103                #crate_path::message::tap_message_trait::typed_plain_message(reply, confirm_relationship)
1104            }
1105        }
1106    }
1107}