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                        // Check if the field type is Option<String>
342                        if is_optional_type(&field.ty) {
343                            field_info.optional_transaction_id_field = Some(field_name.clone());
344                        } else {
345                            field_info.transaction_id_field = Some(field_name.clone());
346                        }
347                    } else if meta.path.is_ident("optional_transaction_id") {
348                        field_info.optional_transaction_id_field = Some(field_name.clone());
349                    } else if meta.path.is_ident("thread_id") {
350                        // Check if the field type is Option<String>
351                        if is_optional_type(&field.ty) {
352                            field_info.optional_thread_id_field = Some(field_name.clone());
353                        } else {
354                            field_info.thread_id_field = Some(field_name.clone());
355                        }
356                    } else if meta.path.is_ident("connection_id") {
357                        field_info.connection_id_field = Some(field_name.clone());
358                    } else if meta.path.is_ident("generated_id") {
359                        field_info.has_generated_id = true;
360                    }
361                    Ok(())
362                });
363            }
364        }
365    }
366
367    field_info
368}
369
370fn is_optional_type(ty: &syn::Type) -> bool {
371    if let syn::Type::Path(type_path) = ty {
372        if let Some(segment) = type_path.path.segments.last() {
373            return segment.ident == "Option";
374        }
375    }
376    false
377}
378
379fn impl_tap_message_trait(
380    name: &syn::Ident,
381    field_info: &FieldInfo,
382    impl_generics: &syn::ImplGenerics,
383    ty_generics: &syn::TypeGenerics,
384    where_clause: Option<&syn::WhereClause>,
385    is_internal: bool,
386) -> TokenStream2 {
387    let thread_id_impl = generate_thread_id_impl(field_info);
388    let message_id_impl = generate_message_id_impl(field_info);
389    let get_all_participants_impl = generate_get_all_participants_impl(field_info);
390
391    let crate_path = if is_internal {
392        quote! { crate }
393    } else {
394        quote! { ::tap_msg }
395    };
396
397    // message_type is no longer part of TapMessage trait
398
399    quote! {
400        impl #impl_generics #crate_path::message::tap_message_trait::TapMessage for #name #ty_generics #where_clause {
401            fn validate(&self) -> #crate_path::error::Result<()> {
402                <Self as #crate_path::message::tap_message_trait::TapMessageBody>::validate(self)
403            }
404
405            fn is_tap_message(&self) -> bool {
406                <Self as #crate_path::message::tap_message_trait::TapMessageBody>::message_type()
407                    .starts_with("https://tap.rsvp/schema/1.0#")
408            }
409
410            fn get_tap_type(&self) -> Option<String> {
411                Some(
412                    <Self as #crate_path::message::tap_message_trait::TapMessageBody>::message_type()
413                        .to_string(),
414                )
415            }
416
417            fn body_as<T: #crate_path::message::tap_message_trait::TapMessageBody>(
418                &self,
419            ) -> #crate_path::error::Result<T> {
420                unimplemented!()
421            }
422
423            fn get_all_participants(&self) -> Vec<String> {
424                // Import TapParticipant trait for .id() method access
425                use #crate_path::message::agent::TapParticipant;
426                #get_all_participants_impl
427            }
428
429            fn create_reply<T: #crate_path::message::tap_message_trait::TapMessageBody>(
430                &self,
431                body: &T,
432                creator_did: &str,
433            ) -> #crate_path::error::Result<#crate_path::didcomm::PlainMessage> {
434                // Create the base message with creator as sender
435                let mut message = body.to_didcomm(creator_did)?;
436
437                // message is already PlainMessage<Value> from to_didcomm
438
439                // Set the thread ID to maintain the conversation thread
440                if let Some(thread_id) = self.thread_id() {
441                    message.thid = Some(thread_id.to_string());
442                } else {
443                    // If no thread ID exists, use the original message ID as the thread ID
444                    message.thid = Some(self.message_id().to_string());
445                }
446
447                // Set the parent thread ID if this thread is part of a larger transaction
448                if let Some(parent_thread_id) = self.parent_thread_id() {
449                    message.pthid = Some(parent_thread_id.to_string());
450                }
451
452                Ok(message)
453            }
454
455
456            fn thread_id(&self) -> Option<&str> {
457                #thread_id_impl
458            }
459
460            fn parent_thread_id(&self) -> Option<&str> {
461                None
462            }
463
464            fn message_id(&self) -> &str {
465                #message_id_impl
466            }
467        }
468    }
469}
470
471fn impl_message_context_trait(
472    name: &syn::Ident,
473    field_info: &FieldInfo,
474    impl_generics: &syn::ImplGenerics,
475    ty_generics: &syn::TypeGenerics,
476    where_clause: Option<&syn::WhereClause>,
477    is_internal: bool,
478) -> TokenStream2 {
479    let participant_dids_impl = generate_participant_dids_impl(field_info);
480    let transaction_context_impl = generate_transaction_context_impl(field_info, is_internal);
481
482    let crate_path = if is_internal {
483        quote! { crate }
484    } else {
485        quote! { ::tap_msg }
486    };
487
488    quote! {
489        impl #impl_generics #crate_path::message::MessageContext for #name #ty_generics #where_clause {
490            fn participant_dids(&self) -> Vec<String> {
491                // Import TapParticipant trait for .id() method access
492                use #crate_path::message::agent::TapParticipant;
493                #participant_dids_impl
494            }
495
496            fn transaction_context(&self) -> Option<#crate_path::message::TransactionContext> {
497                #transaction_context_impl
498            }
499        }
500    }
501}
502
503fn generate_participant_dids_impl(field_info: &FieldInfo) -> TokenStream2 {
504    let mut did_extracts = Vec::new();
505
506    // Add required participants (Agent, Party, or Participant types)
507    for field in &field_info.participant_fields {
508        did_extracts.push(quote! {
509            // Use the TapParticipant trait to get ID - works with Agent, Party, or Participant
510            dids.push(self.#field.id().to_string());
511        });
512    }
513
514    // Add optional participants
515    for field in &field_info.optional_participant_fields {
516        did_extracts.push(quote! {
517            if let Some(ref participant) = self.#field {
518                dids.push(participant.id().to_string());
519            }
520        });
521    }
522
523    // Add participant lists (typically Vec<Agent> or Vec<Participant>)
524    for field in &field_info.participant_list_fields {
525        did_extracts.push(quote! {
526            for participant in &self.#field {
527                dids.push(participant.id().to_string());
528            }
529        });
530    }
531
532    quote! {
533        let mut dids = Vec::new();
534        #(#did_extracts)*
535        dids
536    }
537}
538
539fn generate_get_all_participants_impl(field_info: &FieldInfo) -> TokenStream2 {
540    let mut participant_extracts = Vec::new();
541
542    // Add required participants
543    for field in &field_info.participant_fields {
544        participant_extracts.push(quote! {
545            participants.push(self.#field.id().to_string());
546        });
547    }
548
549    // Add optional participants
550    for field in &field_info.optional_participant_fields {
551        participant_extracts.push(quote! {
552            if let Some(ref participant) = self.#field {
553                participants.push(participant.id().to_string());
554            }
555        });
556    }
557
558    // Add participant lists
559    for field in &field_info.participant_list_fields {
560        participant_extracts.push(quote! {
561            for participant in &self.#field {
562                participants.push(participant.id().to_string());
563            }
564        });
565    }
566
567    quote! {
568        let mut participants = Vec::new();
569        #(#participant_extracts)*
570        participants
571    }
572}
573
574fn generate_thread_id_impl(field_info: &FieldInfo) -> TokenStream2 {
575    if let Some(ref thread_field) = field_info.thread_id_field {
576        quote! { Some(&self.#thread_field) }
577    } else if let Some(ref opt_thread_field) = field_info.optional_thread_id_field {
578        quote! { self.#opt_thread_field.as_deref() }
579    } else if field_info.is_initiator {
580        // Initiators don't have a thread_id - they start the thread
581        quote! { None }
582    } else if let Some(ref tx_field) = field_info.transaction_id_field {
583        quote! { Some(&self.#tx_field) }
584    } else if let Some(ref opt_tx_field) = field_info.optional_transaction_id_field {
585        quote! { self.#opt_tx_field.as_deref() }
586    } else {
587        quote! { None }
588    }
589}
590
591fn generate_message_id_impl(field_info: &FieldInfo) -> TokenStream2 {
592    if let Some(tx_field) = &field_info.transaction_id_field {
593        quote! { &self.#tx_field }
594    } else if let Some(thread_field) = &field_info.thread_id_field {
595        quote! { &self.#thread_field }
596    } else if let Some(opt_tx_field) = &field_info.optional_transaction_id_field {
597        quote! {
598            self.#opt_tx_field.as_deref().unwrap_or("")
599        }
600    } else {
601        quote! {
602            // For types without an ID field, we'll use a static string
603            // This isn't ideal but it satisfies the API contract
604            static FALLBACK_ID: &str = "00000000-0000-0000-0000-000000000000";
605            FALLBACK_ID
606        }
607    }
608}
609
610fn generate_transaction_context_impl(field_info: &FieldInfo, is_internal: bool) -> TokenStream2 {
611    let crate_path = if is_internal {
612        quote! { crate }
613    } else {
614        quote! { ::tap_msg }
615    };
616
617    if let Some(tx_field) = &field_info.transaction_id_field {
618        quote! {
619            Some(#crate_path::message::TransactionContext::new(
620                self.#tx_field.clone(),
621                <Self as #crate_path::message::tap_message_trait::TapMessageBody>::message_type().to_string(),
622            ))
623        }
624    } else if let Some(opt_tx_field) = &field_info.optional_transaction_id_field {
625        quote! {
626            self.#opt_tx_field.as_ref().map(|tx_id| {
627                #crate_path::message::TransactionContext::new(
628                    tx_id.clone(),
629                    <Self as #crate_path::message::tap_message_trait::TapMessageBody>::message_type().to_string(),
630                )
631            })
632        }
633    } else {
634        quote! { None }
635    }
636}
637
638fn impl_tap_message_body_trait(
639    name: &syn::Ident,
640    field_info: &FieldInfo,
641    impl_generics: &syn::ImplGenerics,
642    ty_generics: &syn::TypeGenerics,
643    where_clause: Option<&syn::WhereClause>,
644    is_internal: bool,
645) -> TokenStream2 {
646    let crate_path = if is_internal {
647        quote! { crate }
648    } else {
649        quote! { ::tap_msg }
650    };
651
652    let message_type = field_info
653        .message_type
654        .as_ref()
655        .expect("message_type attribute is required for TapMessageBody");
656
657    let validate_impl = if field_info.custom_validation {
658        // If custom_validation is specified, delegate to a validate_<struct_name_lowercase> method
659        let method_name = syn::Ident::new(
660            &format!("validate_{}", name.to_string().to_lowercase()),
661            name.span(),
662        );
663        quote! {
664            self.#method_name()
665        }
666    } else {
667        quote! {
668            // Basic validation - users can override this by implementing custom validation
669            Ok(())
670        }
671    };
672
673    let to_didcomm_impl = generate_to_didcomm_impl(field_info, is_internal);
674
675    quote! {
676        impl #impl_generics #crate_path::message::tap_message_trait::TapMessageBody for #name #ty_generics #where_clause {
677            fn message_type() -> &'static str {
678                #message_type
679            }
680
681            fn validate(&self) -> #crate_path::error::Result<()> {
682                #validate_impl
683            }
684
685            fn to_didcomm(&self, from_did: &str) -> #crate_path::error::Result<#crate_path::didcomm::PlainMessage> {
686                #to_didcomm_impl
687            }
688        }
689    }
690}
691
692fn generate_to_didcomm_impl(field_info: &FieldInfo, is_internal: bool) -> TokenStream2 {
693    let crate_path = if is_internal {
694        quote! { crate }
695    } else {
696        quote! { ::tap_msg }
697    };
698
699    // Generate participant extraction
700    let participant_extraction = if !field_info.participant_fields.is_empty()
701        || !field_info.optional_participant_fields.is_empty()
702        || !field_info.participant_list_fields.is_empty()
703    {
704        let mut extracts = Vec::new();
705
706        // Required participants
707        for field in &field_info.participant_fields {
708            extracts.push(quote! {
709                recipient_dids.push(self.#field.id().to_string());
710            });
711        }
712
713        // Optional participants
714        for field in &field_info.optional_participant_fields {
715            extracts.push(quote! {
716                if let Some(ref participant) = self.#field {
717                    recipient_dids.push(participant.id().to_string());
718                }
719            });
720        }
721
722        // Participant lists
723        for field in &field_info.participant_list_fields {
724            extracts.push(quote! {
725                for participant in &self.#field {
726                    recipient_dids.push(participant.id().to_string());
727                }
728            });
729        }
730
731        quote! {
732            let mut recipient_dids = Vec::new();
733            #(#extracts)*
734
735            // Remove duplicates and sender
736            recipient_dids.sort();
737            recipient_dids.dedup();
738            recipient_dids.retain(|did| did != from_did);
739        }
740    } else {
741        quote! {
742            let recipient_dids: Vec<String> = Vec::new();
743        }
744    };
745
746    // Generate thread ID assignment
747    let thread_assignment = if field_info.is_initiator {
748        // Initiators create a new thread - their transaction_id becomes the thid for replies
749        if let Some(tx_field) = &field_info.transaction_id_field {
750            quote! {
751                thid: Some(self.#tx_field.clone()),
752            }
753        } else {
754            quote! {
755                thid: None,
756            }
757        }
758    } else if let Some(thread_field) = &field_info.thread_id_field {
759        // Reply messages use thread_id to reference the existing transaction
760        quote! {
761            thid: Some(self.#thread_field.clone()),
762        }
763    } else if let Some(opt_thread_field) = &field_info.optional_thread_id_field {
764        // Optional thread_id field
765        quote! {
766            thid: self.#opt_thread_field.clone(),
767        }
768    } else if let Some(tx_field) = &field_info.transaction_id_field {
769        // Fallback for backwards compatibility
770        quote! {
771            thid: Some(self.#tx_field.clone()),
772        }
773    } else if let Some(opt_tx_field) = &field_info.optional_transaction_id_field {
774        quote! {
775            thid: self.#opt_tx_field.clone(),
776        }
777    } else {
778        quote! {
779            thid: None,
780        }
781    };
782
783    // Generate pthid assignment for connection linking
784    let pthid_assignment = if let Some(conn_field) = &field_info.connection_id_field {
785        quote! {
786            pthid: self.#conn_field.clone(),
787        }
788    } else {
789        quote! {
790            pthid: None,
791        }
792    };
793
794    quote! {
795        // Import TapParticipant trait for .id() method access
796        use #crate_path::message::agent::TapParticipant;
797
798        // Serialize the message body to JSON
799        let mut body_json = serde_json::to_value(self)
800            .map_err(|e| #crate_path::error::Error::SerializationError(e.to_string()))?;
801
802        // Ensure the @type field is correctly set in the body
803        if let Some(body_obj) = body_json.as_object_mut() {
804            body_obj.insert(
805                "@type".to_string(),
806                serde_json::Value::String(Self::message_type().to_string()),
807            );
808        }
809
810        // Extract recipient DIDs from participants
811        #participant_extraction
812
813        let now = chrono::Utc::now().timestamp() as u64;
814
815        // Create the PlainMessage
816        Ok(#crate_path::didcomm::PlainMessage {
817            id: uuid::Uuid::new_v4().to_string(),
818            typ: "application/didcomm-plain+json".to_string(),
819            type_: Self::message_type().to_string(),
820            body: body_json,
821            from: from_did.to_string(),
822            to: recipient_dids,
823            #thread_assignment
824            #pthid_assignment
825            created_time: Some(now),
826            expires_time: None,
827            extra_headers: std::collections::HashMap::new(),
828            from_prior: None,
829            attachments: None,
830        })
831    }
832}
833
834fn impl_tap_message_body_only(input: &DeriveInput) -> TokenStream2 {
835    let name = &input.ident;
836    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
837
838    let fields = match &input.data {
839        Data::Struct(data) => match &data.fields {
840            Fields::Named(fields) => &fields.named,
841            _ => panic!("TapMessageBody can only be derived for structs with named fields"),
842        },
843        _ => panic!("TapMessageBody can only be derived for structs"),
844    };
845
846    let field_info = analyze_fields(fields, &input.attrs);
847
848    // Check if we're inside the tap-msg crate or external
849    let is_internal = std::env::var("CARGO_CRATE_NAME").unwrap_or_default() == "tap_msg";
850
851    // TapMessageBody can only be derived if message_type is specified
852    if field_info.message_type.is_none() {
853        panic!("TapMessageBody derive macro requires #[tap(message_type = \"...\")] attribute");
854    }
855
856    impl_tap_message_body_trait(
857        name,
858        &field_info,
859        &impl_generics,
860        &ty_generics,
861        where_clause,
862        is_internal,
863    )
864}
865
866fn impl_authorizable_trait(
867    name: &syn::Ident,
868    field_info: &FieldInfo,
869    impl_generics: &syn::ImplGenerics,
870    ty_generics: &syn::TypeGenerics,
871    where_clause: Option<&syn::WhereClause>,
872    is_internal: bool,
873) -> TokenStream2 {
874    let crate_path = if is_internal {
875        quote! { crate }
876    } else {
877        quote! { ::tap_msg }
878    };
879
880    // Get the transaction ID field access
881    let tx_id_access = if let Some(ref tx_field) = field_info.transaction_id_field {
882        quote! { &self.#tx_field }
883    } else if let Some(ref opt_tx_field) = field_info.optional_transaction_id_field {
884        quote! { self.#opt_tx_field.as_deref().unwrap_or("") }
885    } else if let Some(ref thread_field) = field_info.thread_id_field {
886        quote! { &self.#thread_field }
887    } else {
888        quote! { "" }
889    };
890
891    quote! {
892        impl #impl_generics #crate_path::message::tap_message_trait::Authorizable for #name #ty_generics #where_clause {
893            fn authorize(
894                &self,
895                creator_did: &str,
896                settlement_address: Option<&str>,
897                expiry: Option<&str>,
898            ) -> #crate_path::didcomm::PlainMessage<#crate_path::message::Authorize> {
899                let authorize = #crate_path::message::Authorize::with_all(#tx_id_access, settlement_address, expiry);
900                let original_message = self
901                    .to_didcomm(creator_did)
902                    .expect("Failed to create DIDComm message");
903                let reply = original_message
904                    .create_reply(&authorize, creator_did)
905                    .expect("Failed to create reply");
906                #crate_path::message::tap_message_trait::typed_plain_message(reply, authorize)
907            }
908
909            fn cancel(&self, creator_did: &str, by: &str, reason: Option<&str>) -> #crate_path::didcomm::PlainMessage<#crate_path::message::Cancel> {
910                let cancel = if let Some(reason) = reason {
911                    #crate_path::message::Cancel::with_reason(#tx_id_access, by, reason)
912                } else {
913                    #crate_path::message::Cancel::new(#tx_id_access, by)
914                };
915                let original_message = self
916                    .to_didcomm(creator_did)
917                    .expect("Failed to create DIDComm message");
918                let reply = original_message
919                    .create_reply(&cancel, creator_did)
920                    .expect("Failed to create reply");
921                #crate_path::message::tap_message_trait::typed_plain_message(reply, cancel)
922            }
923
924            fn reject(&self, creator_did: &str, reason: &str) -> #crate_path::didcomm::PlainMessage<#crate_path::message::Reject> {
925                let reject = #crate_path::message::Reject {
926                    transaction_id: (#tx_id_access).to_string(),
927                    reason: Some(reason.to_string()),
928                };
929                let original_message = self
930                    .to_didcomm(creator_did)
931                    .expect("Failed to create DIDComm message");
932                let reply = original_message
933                    .create_reply(&reject, creator_did)
934                    .expect("Failed to create reply");
935                #crate_path::message::tap_message_trait::typed_plain_message(reply, reject)
936            }
937        }
938    }
939}
940
941fn impl_transaction_trait(
942    name: &syn::Ident,
943    field_info: &FieldInfo,
944    impl_generics: &syn::ImplGenerics,
945    ty_generics: &syn::TypeGenerics,
946    where_clause: Option<&syn::WhereClause>,
947    is_internal: bool,
948) -> TokenStream2 {
949    let crate_path = if is_internal {
950        quote! { crate }
951    } else {
952        quote! { ::tap_msg }
953    };
954
955    // Get the transaction ID field access
956    let tx_id_access = if let Some(ref tx_field) = field_info.transaction_id_field {
957        quote! { &self.#tx_field }
958    } else if let Some(ref opt_tx_field) = field_info.optional_transaction_id_field {
959        quote! { self.#opt_tx_field.as_deref().unwrap_or("") }
960    } else if let Some(ref thread_field) = field_info.thread_id_field {
961        quote! { &self.#thread_field }
962    } else {
963        quote! { "" }
964    };
965
966    quote! {
967        impl #impl_generics #crate_path::message::tap_message_trait::Transaction for #name #ty_generics #where_clause {
968            fn settle(
969                &self,
970                creator_did: &str,
971                settlement_id: &str,
972                amount: Option<&str>,
973            ) -> #crate_path::didcomm::PlainMessage<#crate_path::message::Settle> {
974                let settle = #crate_path::message::Settle {
975                    transaction_id: (#tx_id_access).to_string(),
976                    settlement_id: Some(settlement_id.to_string()),
977                    amount: amount.map(|s| s.to_string()),
978                };
979                let original_message = self
980                    .to_didcomm(creator_did)
981                    .expect("Failed to create DIDComm message");
982                let reply = original_message
983                    .create_reply(&settle, creator_did)
984                    .expect("Failed to create reply");
985                #crate_path::message::tap_message_trait::typed_plain_message(reply, settle)
986            }
987
988            fn revert(
989                &self,
990                creator_did: &str,
991                settlement_address: &str,
992                reason: &str,
993            ) -> #crate_path::didcomm::PlainMessage<#crate_path::message::Revert> {
994                let revert = #crate_path::message::Revert {
995                    transaction_id: (#tx_id_access).to_string(),
996                    settlement_address: settlement_address.to_string(),
997                    reason: reason.to_string(),
998                };
999                let original_message = self
1000                    .to_didcomm(creator_did)
1001                    .expect("Failed to create DIDComm message");
1002                let reply = original_message
1003                    .create_reply(&revert, creator_did)
1004                    .expect("Failed to create reply");
1005                #crate_path::message::tap_message_trait::typed_plain_message(reply, revert)
1006            }
1007
1008            fn add_agents(&self, creator_did: &str, agents: Vec<#crate_path::message::Agent>) -> #crate_path::didcomm::PlainMessage<#crate_path::message::AddAgents> {
1009                let add_agents = #crate_path::message::AddAgents {
1010                    transaction_id: (#tx_id_access).to_string(),
1011                    agents,
1012                };
1013                let original_message = self
1014                    .to_didcomm(creator_did)
1015                    .expect("Failed to create DIDComm message");
1016                let reply = original_message
1017                    .create_reply(&add_agents, creator_did)
1018                    .expect("Failed to create reply");
1019                #crate_path::message::tap_message_trait::typed_plain_message(reply, add_agents)
1020            }
1021
1022            fn replace_agent(
1023                &self,
1024                creator_did: &str,
1025                original_agent: &str,
1026                replacement: #crate_path::message::Agent,
1027            ) -> #crate_path::didcomm::PlainMessage<#crate_path::message::ReplaceAgent> {
1028                let replace_agent = #crate_path::message::ReplaceAgent {
1029                    transaction_id: (#tx_id_access).to_string(),
1030                    original: original_agent.to_string(),
1031                    replacement,
1032                };
1033                let original_message = self
1034                    .to_didcomm(creator_did)
1035                    .expect("Failed to create DIDComm message");
1036                let reply = original_message
1037                    .create_reply(&replace_agent, creator_did)
1038                    .expect("Failed to create reply");
1039                #crate_path::message::tap_message_trait::typed_plain_message(reply, replace_agent)
1040            }
1041
1042            fn remove_agent(&self, creator_did: &str, agent: &str) -> #crate_path::didcomm::PlainMessage<#crate_path::message::RemoveAgent> {
1043                let remove_agent = #crate_path::message::RemoveAgent {
1044                    transaction_id: (#tx_id_access).to_string(),
1045                    agent: agent.to_string(),
1046                };
1047                let original_message = self
1048                    .to_didcomm(creator_did)
1049                    .expect("Failed to create DIDComm message");
1050                let reply = original_message
1051                    .create_reply(&remove_agent, creator_did)
1052                    .expect("Failed to create reply");
1053                #crate_path::message::tap_message_trait::typed_plain_message(reply, remove_agent)
1054            }
1055
1056            fn update_party(
1057                &self,
1058                creator_did: &str,
1059                party_type: &str,
1060                party: #crate_path::message::Party,
1061            ) -> #crate_path::didcomm::PlainMessage<#crate_path::message::UpdateParty> {
1062                let update_party = #crate_path::message::UpdateParty {
1063                    transaction_id: (#tx_id_access).to_string(),
1064                    party_type: party_type.to_string(),
1065                    party,
1066                    context: None,
1067                };
1068                let original_message = self
1069                    .to_didcomm(creator_did)
1070                    .expect("Failed to create DIDComm message");
1071                let reply = original_message
1072                    .create_reply(&update_party, creator_did)
1073                    .expect("Failed to create reply");
1074                #crate_path::message::tap_message_trait::typed_plain_message(reply, update_party)
1075            }
1076
1077            fn update_policies(
1078                &self,
1079                creator_did: &str,
1080                policies: Vec<#crate_path::message::Policy>,
1081            ) -> #crate_path::didcomm::PlainMessage<#crate_path::message::UpdatePolicies> {
1082                let update_policies = #crate_path::message::UpdatePolicies {
1083                    transaction_id: (#tx_id_access).to_string(),
1084                    policies,
1085                };
1086                let original_message = self
1087                    .to_didcomm(creator_did)
1088                    .expect("Failed to create DIDComm message");
1089                let reply = original_message
1090                    .create_reply(&update_policies, creator_did)
1091                    .expect("Failed to create reply");
1092                #crate_path::message::tap_message_trait::typed_plain_message(reply, update_policies)
1093            }
1094
1095            fn confirm_relationship(
1096                &self,
1097                creator_did: &str,
1098                agent_did: &str,
1099                for_entity: &str,
1100            ) -> #crate_path::didcomm::PlainMessage<#crate_path::message::ConfirmRelationship> {
1101                let confirm_relationship = #crate_path::message::ConfirmRelationship {
1102                    transaction_id: (#tx_id_access).to_string(),
1103                    agent_id: agent_did.to_string(),
1104                    for_entity: for_entity.to_string(),
1105                    role: None,
1106                };
1107                let original_message = self
1108                    .to_didcomm(creator_did)
1109                    .expect("Failed to create DIDComm message");
1110                let reply = original_message
1111                    .create_reply(&confirm_relationship, creator_did)
1112                    .expect("Failed to create reply");
1113                #crate_path::message::tap_message_trait::typed_plain_message(reply, confirm_relationship)
1114            }
1115        }
1116    }
1117}