Skip to main content

rofr_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::ItemTrait;
4use syn::ReturnType;
5use syn::TraitItem;
6use syn::parse_macro_input;
7
8#[proc_macro_attribute]
9pub fn service(args: TokenStream, input: TokenStream) -> TokenStream {
10    let mut trait_item = parse_macro_input!(input as ItemTrait);
11
12    let mut service_name = None;
13    let mut service_version = None;
14    let parser = syn::meta::parser(|meta| {
15        if meta.path.is_ident("name") {
16            service_name = Some(meta.value()?.parse::<syn::LitStr>()?.value());
17            Ok(())
18        } else if meta.path.is_ident("version") {
19            service_version = Some(meta.value()?.parse::<syn::LitStr>()?.value());
20            Ok(())
21        } else {
22            Err(meta.error("unsupported service attribute"))
23        }
24    });
25    parse_macro_input!(args with parser);
26    let service_name_template = service_name.expect("service attribute requires 'name' parameter");
27    let service_version = service_version.expect("service attribute requires 'version' parameter");
28
29    let service_template_params = extract_template_params(&service_name_template);
30
31    // generate parameter identifiers for the service function signature
32    let param_idents: Vec<syn::Ident> = service_template_params
33        .iter()
34        .map(|p| syn::Ident::new(p, proc_macro2::Span::call_site()))
35        .collect();
36
37    // build the actual service name (without templates, just the plain name)
38    // e.g., "weather.{id}" -> "weather"
39    let service_name = service_name_template
40        .split('.')
41        .next()
42        .unwrap_or(&service_name_template);
43
44    let trait_name = &trait_item.ident;
45    let ext_trait_name = syn::Ident::new(&format!("{}Ext", trait_name), trait_name.span());
46
47    // add trait bounds for the associated Context type
48    let where_clause = trait_item.generics.make_where_clause();
49    where_clause
50        .predicates
51        .push(syn::parse_quote!(Self::Context: ::rofr::ServiceContext));
52
53    let mut endpoint_methods = Vec::new();
54    let mut stream_methods = Vec::new();
55
56    for item in &mut trait_item.items {
57        if let TraitItem::Fn(method) = item {
58            // check if this method has a #[stream] attribute
59            let mut stream_name = None;
60            let mut stream_subject = None;
61            let mut stream_storage = None;
62            let mut stream_message = None;
63
64            // check if this method has an #[endpoint] attribute
65            let mut endpoint_subject = None;
66            method.attrs.retain(|attr| {
67                if attr.path().is_ident("stream") {
68                    let _ = attr.parse_nested_meta(|meta| {
69                        if meta.path.is_ident("name") {
70                            let value = meta.value()?;
71                            let s: syn::LitStr = value.parse()?;
72                            stream_name = Some(s.value());
73                            Ok(())
74                        } else if meta.path.is_ident("subject") {
75                            let value = meta.value()?;
76                            let s: syn::LitStr = value.parse()?;
77                            stream_subject = Some(s.value());
78                            Ok(())
79                        } else if meta.path.is_ident("storage") {
80                            let value = meta.value()?;
81                            let path: syn::Path = value.parse()?;
82                            stream_storage = Some(path);
83                            Ok(())
84                        } else if meta.path.is_ident("message") {
85                            let value = meta.value()?;
86                            let ty: syn::Type = value.parse()?;
87                            stream_message = Some(ty);
88                            Ok(())
89                        } else {
90                            Err(meta.error("unsupported stream attribute"))
91                        }
92                    });
93                    false // remove the attribute
94                } else if attr.path().is_ident("endpoint") {
95                    let _ = attr.parse_nested_meta(|meta| {
96                        if meta.path.is_ident("subject") {
97                            let value = meta.value()?;
98                            let s: syn::LitStr = value.parse()?;
99                            endpoint_subject = Some(s.value());
100                            Ok(())
101                        } else {
102                            Err(meta.error("unsupported endpoint attribute"))
103                        }
104                    });
105                    false // remove the attribute
106                } else {
107                    true // keep other attributes
108                }
109            });
110
111            if let (Some(name), Some(subject)) = (stream_name, stream_subject) {
112                let method_name = method.sig.ident.clone();
113
114                // add `Send` bound to the return type if it's async
115                if method.sig.asyncness.is_some()
116                    && let ReturnType::Type(_, ref mut ty) = method.sig.output
117                {
118                    // wrap the return type with + Send
119                    let original_ty = (**ty).clone();
120                    **ty = syn::parse_quote!(
121                        impl ::std::future::Future<Output = #original_ty> + Send
122                    );
123                    // remove async keyword since we're using impl Future now
124                    method.sig.asyncness = None;
125                }
126
127                stream_methods.push((method_name, name, subject, stream_storage, stream_message));
128            }
129
130            if let Some(subject) = endpoint_subject {
131                let method_name = method.sig.ident.clone();
132                let has_body_param = method.sig.inputs.len() > 1;
133
134                // extract request type if present (from Request<T>)
135                let request_type = if has_body_param
136                    && let syn::FnArg::Typed(arg) = &method.sig.inputs[1]
137                    && let syn::Type::Path(type_path) = &*arg.ty
138                    && let Some(segment) = type_path.path.segments.last()
139                    && segment.ident == "Request"
140                    && let syn::PathArguments::AngleBracketed(args) = &segment.arguments
141                    && let Some(syn::GenericArgument::Type(ty)) = args.args.first()
142                {
143                    ty.clone()
144                } else {
145                    syn::parse_str("()").unwrap()
146                };
147
148                // extract response type from Result<Response<T>, Error>
149                let response_type = if let ReturnType::Type(_, ref ty) = method.sig.output {
150                    extract_response_type(ty).unwrap_or(syn::parse_str("()").unwrap())
151                } else {
152                    syn::parse_str("()").unwrap()
153                };
154
155                // add `Send` bound to the return type if it's async
156                if method.sig.asyncness.is_some()
157                    && let ReturnType::Type(_, ref mut ty) = method.sig.output
158                {
159                    // wrap the return type with + Send
160                    let original_ty = (**ty).clone();
161                    **ty = syn::parse_quote!(
162                        impl ::std::future::Future<Output = #original_ty> + Send
163                    );
164                    // remove async keyword since we're using impl Future now
165                    method.sig.asyncness = None;
166                }
167
168                endpoint_methods.push((
169                    method_name,
170                    subject,
171                    has_body_param,
172                    request_type,
173                    response_type,
174                ));
175            }
176        }
177    }
178
179    // generate endpoint handler structs and registrations
180    let mut handler_structs = Vec::new();
181    let mut handler_debug_impls = Vec::new();
182    let mut handler_impls = Vec::new();
183    let mut endpoint_registrations = Vec::new();
184
185    // generate stream handler structs and registrations
186    let mut stream_handler_structs = Vec::new();
187    let mut stream_handler_debug_impls = Vec::new();
188    let mut stream_handler_impls = Vec::new();
189
190    for (method_name, subject, has_body_param, request_type, response_type) in &endpoint_methods {
191        // convert snake_case to PascalCase for handler name
192        let handler_name = syn::Ident::new(
193            &format!("{}Handler", snake_to_pascal(&method_name.to_string())),
194            method_name.span(),
195        );
196
197        // handler struct definition - generic over T
198        handler_structs.push(quote! {
199            struct #handler_name<T>(::std::marker::PhantomData<T>);
200        });
201
202        // manual Debug implementation
203        handler_debug_impls.push(quote! {
204            impl<T> ::std::fmt::Debug for #handler_name<T> {
205                fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
206                    f.debug_struct(stringify!(#handler_name)).finish()
207                }
208            }
209        });
210
211        // handler implementation - with proper trait bounds
212        let handler_impl = if *has_body_param {
213            quote! {
214                #[::rofr::async_trait::async_trait]
215                impl<T> ::rofr::EndpointHandler<T::Context> for #handler_name<T>
216                where
217                    T: #trait_name + Send + Sync + 'static,
218                    T::Context: ::rofr::ServiceContext,
219                {
220                    async fn handle_request(
221                        &self,
222                        rqctx: ::rofr::RequestContext<T::Context>,
223                        body: ::rofr::Bytes,
224                    ) -> Result<::rofr::Bytes, Box<dyn std::error::Error + Send + Sync>> {
225                        let request: ::rofr::Request<_> = ::serde_json::from_slice(&body)?;
226                        let result = T::#method_name(rqctx, request).await;
227
228                        match result {
229                            Ok(response) => {
230                                let json = ::serde_json::to_vec(&response)?;
231                                Ok(::rofr::Bytes::from(json))
232                            }
233                            Err(e) => Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>),
234                        }
235                    }
236                }
237            }
238        } else {
239            quote! {
240                #[::rofr::async_trait::async_trait]
241                impl<T> ::rofr::EndpointHandler<T::Context> for #handler_name<T>
242                where
243                    T: #trait_name + Send + Sync + 'static,
244                    T::Context: ::rofr::ServiceContext,
245                {
246                    async fn handle_request(
247                        &self,
248                        rqctx: ::rofr::RequestContext<T::Context>,
249                        _body: ::rofr::Bytes,
250                    ) -> Result<::rofr::Bytes, Box<dyn std::error::Error + Send + Sync>> {
251                        let result = T::#method_name(rqctx).await;
252
253                        match result {
254                            Ok(response) => {
255                                let json = ::serde_json::to_vec(&response)?;
256                                Ok(::rofr::Bytes::from(json))
257                            }
258                            Err(e) => Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>),
259                        }
260                    }
261                }
262            }
263        };
264
265        handler_impls.push(handler_impl);
266
267        // build the subject expression by applying service template parameters
268        let subject_expr = build_subject_expr(subject, &service_template_params);
269
270        endpoint_registrations.push(quote! {
271            endpoints.push(::rofr::Endpoint {
272                subject: #subject_expr,
273                handler: ::std::sync::Arc::new(#handler_name::<Self>(::std::marker::PhantomData)),
274                request_schema: ::schemars::schema_for!(#request_type),
275                response_schema: ::schemars::schema_for!(#response_type),
276            });
277        });
278    }
279
280    // build the service function signature with optional parameters
281    // build a Vec of `impl Display` type tokens, one per parameter
282    let param_types: Vec<proc_macro2::TokenStream> = param_idents
283        .iter()
284        .map(|_| quote! { impl ::std::fmt::Display })
285        .collect();
286
287    let service_fn_signature = if service_template_params.is_empty() {
288        quote! {
289            fn service(context: Self::Context) -> ::rofr::Service<Self::Context>
290        }
291    } else {
292        quote! {
293            fn service(context: Self::Context, params: (#(#param_types,)*)) -> ::rofr::Service<Self::Context>
294        }
295    };
296
297    // build the tuple destructuring statement for the service function body
298    let service_fn_body_prelude = if param_idents.is_empty() {
299        quote! {}
300    } else {
301        quote! { let (#(#param_idents,)*) = params; }
302    };
303
304    // generate stream handlers and registrations
305    for (method_name, _stream_name, _stream_subject, _storage_type, _message_type) in
306        &stream_methods
307    {
308        let handler_name = syn::Ident::new(
309            &format!("{}StreamHandler", snake_to_pascal(&method_name.to_string())),
310            method_name.span(),
311        );
312
313        stream_handler_structs.push(quote! {
314            struct #handler_name<T>(::std::marker::PhantomData<T>);
315        });
316
317        stream_handler_debug_impls.push(quote! {
318            impl<T> ::std::fmt::Debug for #handler_name<T> {
319                fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
320                    f.debug_struct(stringify!(#handler_name)).finish()
321                }
322            }
323        });
324
325        stream_handler_impls.push(quote! {
326            #[::rofr::async_trait::async_trait]
327            impl<T> ::rofr::StreamHandler<T::Context> for #handler_name<T>
328            where
329                T: #trait_name + Send + Sync + 'static,
330                T::Context: ::rofr::ServiceContext,
331            {
332                async fn handle_stream(
333                    &self,
334                    ctx: ::rofr::StreamContext<T::Context>,
335                ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
336                    T::#method_name(ctx).await?;
337                    Ok(())
338                }
339            }
340        });
341    }
342
343    // generate stream registrations
344    let mut stream_registrations = Vec::new();
345    for (method_name, stream_name, stream_subject, storage_type, message_type) in &stream_methods {
346        let handler_name = syn::Ident::new(
347            &format!("{}StreamHandler", snake_to_pascal(&method_name.to_string())),
348            method_name.span(),
349        );
350
351        let storage_expr = if let Some(storage) = storage_type {
352            quote! { #storage }
353        } else {
354            quote! { ::async_nats::jetstream::stream::StorageType::File }
355        };
356
357        let message_schema = if let Some(msg_type) = message_type {
358            quote! { ::schemars::schema_for!(#msg_type) }
359        } else {
360            quote! { ::schemars::schema_for!(()) }
361        };
362
363        let subject_prefix_expr = build_subject_prefix_expr(&service_template_params);
364
365        stream_registrations.push(quote! {
366            streams.push(::rofr::Stream {
367                subject_prefix: format!("{}.{}", #service_name, #subject_prefix_expr),
368                config: ::async_nats::jetstream::stream::Config {
369                    name: format!("{}_{}", #service_name.to_string().to_uppercase(), #stream_name.to_string()),
370                    subjects: vec![format!("{}.{}.{}", #service_name, #subject_prefix_expr, #stream_subject)],
371                    storage: #storage_expr,
372                    ..Default::default()
373                },
374                handler: ::std::sync::Arc::new(#handler_name::<Self>(::std::marker::PhantomData)),
375                message_schema: #message_schema,
376            });
377        });
378    }
379
380    let client_name = syn::Ident::new(&format!("{}Client", trait_name), trait_name.span());
381
382    let client_param_fields: Vec<proc_macro2::TokenStream> =
383        param_idents.iter().map(|p| quote! { #p: String }).collect();
384
385    let client_new_params = if param_idents.is_empty() {
386        quote! { nats: ::async_nats::Client }
387    } else {
388        quote! { nats: ::async_nats::Client, params: (#(#param_types,)*) }
389    };
390
391    // build the tuple destructuring statement for the client new() body
392    let client_params_destructure = if param_idents.is_empty() {
393        quote! {}
394    } else {
395        quote! { let (#(#param_idents,)*) = params; }
396    };
397
398    let client_field_inits: Vec<proc_macro2::TokenStream> = param_idents
399        .iter()
400        .map(|p| quote! { #p: #p.to_string() })
401        .collect();
402
403    let client_methods: Vec<proc_macro2::TokenStream> = endpoint_methods
404        .iter()
405        .map(|(method_name, subject, has_body_param, request_type, response_type)| {
406            let subject_expr =
407                build_client_subject_expr(service_name, subject, &service_template_params);
408            let header_block = quote! {
409                let request_id = ::ulid::Ulid::new().to_string();
410                let mut headers = ::async_nats::HeaderMap::new();
411                headers.insert(::rofr::header::REQUEST_ID, request_id.as_str());
412                let subject = #subject_expr;
413            };
414            let status_check = quote! {
415                if let Some(status) = msg.status {
416                    if status.as_u16() != 200 {
417                        let err = msg.description
418                            .unwrap_or_else(|| String::from_utf8_lossy(&msg.payload).to_string());
419                        return Err(::rofr::ClientError::ServiceError(err));
420                    }
421                }
422                let result: #response_type = ::serde_json::from_slice(&msg.payload)
423                    .map_err(::rofr::ClientError::Deserialize)?;
424                Ok(result)
425            };
426            if *has_body_param {
427                quote! {
428                    pub async fn #method_name(&self, body: #request_type) -> Result<#response_type, ::rofr::ClientError> {
429                        #header_block
430                        let payload = ::serde_json::to_vec(&body)
431                            .map_err(::rofr::ClientError::Serialize)?;
432                        let msg = self.nats
433                            .request_with_headers(subject, headers, ::rofr::Bytes::from(payload))
434                            .await
435                            .map_err(|e| ::rofr::ClientError::Request(Box::new(e)))?;
436                        #status_check
437                    }
438                }
439            } else {
440                quote! {
441                    pub async fn #method_name(&self) -> Result<#response_type, ::rofr::ClientError> {
442                        #header_block
443                        let msg = self.nats
444                            .request_with_headers(subject, headers, ::rofr::Bytes::new())
445                            .await
446                            .map_err(|e| ::rofr::ClientError::Request(Box::new(e)))?;
447                        #status_check
448                    }
449                }
450            }
451        })
452        .collect();
453
454    let expanded = quote! {
455        #trait_item
456
457        // generate handler structs outside the impl block
458        #(#handler_structs)*
459
460        // generate Debug implementations for handlers
461        #(#handler_debug_impls)*
462
463        // generate handler implementations
464        #(#handler_impls)*
465
466        // generate stream handler structs
467        #(#stream_handler_structs)*
468
469        // generate Debug implementations for stream handlers
470        #(#stream_handler_debug_impls)*
471
472        // generate stream handler implementations
473        #(#stream_handler_impls)*
474
475        // extension trait for the service() method with default implementation
476        pub trait #ext_trait_name: #trait_name + Sized
477        where
478            Self: Send + Sync + 'static,
479            Self::Context: ::rofr::ServiceContext,
480        {
481            #service_fn_signature;
482        }
483
484        // blanket implementation of the extension trait
485        impl<T> #ext_trait_name for T
486        where
487            T: #trait_name + Send + Sync + 'static,
488            T::Context: ::rofr::ServiceContext,
489        {
490            #service_fn_signature {
491                #service_fn_body_prelude
492                let mut endpoints = Vec::new();
493                let mut streams = Vec::new();
494
495                #(#endpoint_registrations)*
496
497                #(#stream_registrations)*
498
499                ::rofr::Service {
500                    name: #service_name.to_string(),
501                    version: #service_version.to_string(),
502                    endpoints,
503                    streams,
504                    context,
505                }
506            }
507        }
508
509        /// Generated service client.
510        pub struct #client_name {
511            nats: ::async_nats::Client,
512            #(#client_param_fields,)*
513        }
514
515        impl #client_name {
516            pub fn new(#client_new_params) -> Self {
517                #client_params_destructure
518                Self {
519                    nats,
520                    #(#client_field_inits,)*
521                }
522            }
523
524            #(#client_methods)*
525        }
526    };
527
528    TokenStream::from(expanded)
529}
530
531#[proc_macro_attribute]
532pub fn endpoint(_args: TokenStream, input: TokenStream) -> TokenStream {
533    // this is handled by the service macro
534    input
535}
536
537#[proc_macro_attribute]
538pub fn stream(_args: TokenStream, input: TokenStream) -> TokenStream {
539    // this is handled by the service macro
540    input
541}
542
543/// Helper function to extract the response type T from Result<Response<T>, Error>
544fn extract_response_type(ty: &syn::Type) -> Option<syn::Type> {
545    if let syn::Type::Path(type_path) = ty {
546        // look for Result<Response<T>, Error>
547        if let Some(segment) = type_path.path.segments.last()
548            && segment.ident == "Result"
549            && let syn::PathArguments::AngleBracketed(args) = &segment.arguments
550        {
551            // get the first type argument (Response<T>)
552            if let Some(syn::GenericArgument::Type(syn::Type::Path(response_path))) =
553                args.args.first()
554                && let Some(response_segment) = response_path.path.segments.last()
555                && response_segment.ident == "Response"
556                && let syn::PathArguments::AngleBracketed(response_args) =
557                    &response_segment.arguments
558            {
559                // get T from Response<T>
560                if let Some(syn::GenericArgument::Type(inner_ty)) = response_args.args.first() {
561                    return Some(inner_ty.clone());
562                }
563            }
564        }
565    }
566
567    None
568}
569
570fn snake_to_pascal(s: &str) -> String {
571    s.split('_')
572        .filter(|word| !word.is_empty())
573        .map(|word| {
574            let mut c = word.chars();
575            match c.next() {
576                None => String::new(),
577                Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
578            }
579        })
580        .collect()
581}
582
583/// Extract template parameters from a template string
584/// e.g., "weather.{id}.{zone}" -> ["id", "zone"]
585fn extract_template_params(template: &str) -> Vec<String> {
586    let mut params = Vec::new();
587    let mut chars = template.chars().peekable();
588
589    while let Some(ch) = chars.next() {
590        if ch == '{' {
591            let mut param = String::new();
592            for ch in chars.by_ref() {
593                if ch == '}' {
594                    break;
595                }
596                param.push(ch);
597            }
598            if !param.is_empty() {
599                params.push(param);
600            }
601        }
602    }
603
604    params
605}
606
607/// Build an expression that constructs the subject by replacing template parameters
608/// with the corresponding parameters from the service name
609/// e.g., subject "weather.{id}.wind_speed" with service params ["id"] -> format!("{}.wind_speed", id)
610fn build_subject_expr(subject: &str, service_params: &[String]) -> proc_macro2::TokenStream {
611    if service_params.is_empty() {
612        // no parameters, just return the subject as a string
613        return quote! { #subject.to_string() };
614    }
615
616    // replace {param} with {} for format! macro
617    let mut format_str = String::new();
618    for _ in service_params {
619        format_str.push_str("{}.");
620    }
621    format_str.push_str(subject);
622
623    // generate parameter identifiers in the order they appear in this subject
624    let param_idents: Vec<proc_macro2::TokenStream> = service_params
625        .iter()
626        .map(|p| {
627            let ident = syn::Ident::new(p, proc_macro2::Span::call_site());
628            quote! { #ident }
629        })
630        .collect();
631
632    quote! {
633        format!(#format_str, #(#param_idents),*)
634    }
635}
636
637/// Build an expression that construct the subject prefix by replacing the
638/// template parameters with the corresponding parameters from the service name
639fn build_subject_prefix_expr(service_params: &[String]) -> proc_macro2::TokenStream {
640    let format_str = service_params
641        .iter()
642        .map(|_| "{}")
643        .collect::<Vec<_>>()
644        .join(".");
645
646    // generate parameter identifiers in the order they appear in this subject
647    let param_idents: Vec<proc_macro2::TokenStream> = service_params
648        .iter()
649        .map(|p| {
650            let ident = syn::Ident::new(p, proc_macro2::Span::call_site());
651            quote! { #ident }
652        })
653        .collect();
654
655    quote! {
656        format!(#format_str, #(#param_idents),*)
657    }
658}
659
660/// Build an expression that constructs the full client subject (including the service
661/// base name) by replacing template parameters with `self.param` references.
662/// e.g., service_name = "weather", subject = "wind_speed", params = ["location", "id"]
663/// -> format!("weather.{}.{}.wind_speed", &self.location, &self.id)
664fn build_client_subject_expr(
665    service_name: &str,
666    subject: &str,
667    service_params: &[String],
668) -> proc_macro2::TokenStream {
669    if service_params.is_empty() {
670        let full = format!("{}.{}", service_name, subject);
671        return quote! { #full.to_string() };
672    }
673
674    let mut fmt = format!("{}.", service_name);
675    for _ in service_params {
676        fmt.push_str("{}.");
677    }
678    fmt.push_str(subject);
679
680    let param_exprs: Vec<proc_macro2::TokenStream> = service_params
681        .iter()
682        .map(|p| {
683            let ident = syn::Ident::new(p, proc_macro2::Span::call_site());
684            quote! { &self.#ident }
685        })
686        .collect();
687
688    quote! { format!(#fmt, #(#param_exprs),*) }
689}
690
691#[cfg(test)]
692mod tests {
693    use super::*;
694    use quote::quote;
695
696    #[test]
697    fn test_snake_to_pascal_simple() {
698        assert_eq!(snake_to_pascal("hello_world"), "HelloWorld");
699    }
700
701    #[test]
702    fn test_snake_to_pascal_single_word() {
703        assert_eq!(snake_to_pascal("hello"), "Hello");
704    }
705
706    #[test]
707    fn test_snake_to_pascal_empty_string() {
708        assert_eq!(snake_to_pascal(""), "");
709    }
710
711    #[test]
712    fn test_snake_to_pascal_multiple_underscores() {
713        assert_eq!(snake_to_pascal("hello__world"), "HelloWorld");
714    }
715
716    #[test]
717    fn test_snake_to_pascal_leading_underscore() {
718        assert_eq!(snake_to_pascal("_hello_world"), "HelloWorld");
719    }
720
721    #[test]
722    fn test_snake_to_pascal_trailing_underscore() {
723        assert_eq!(snake_to_pascal("hello_world_"), "HelloWorld");
724    }
725
726    #[test]
727    fn test_snake_to_pascal_many_words() {
728        assert_eq!(snake_to_pascal("this_is_a_long_name"), "ThisIsALongName");
729    }
730
731    #[test]
732    fn test_snake_to_pascal_single_char_words() {
733        assert_eq!(snake_to_pascal("a_b_c"), "ABC");
734    }
735
736    #[test]
737    fn test_snake_to_pascal_already_capitalized() {
738        assert_eq!(snake_to_pascal("Hello_World"), "HelloWorld");
739    }
740
741    #[test]
742    fn test_extract_template_params_none() {
743        assert_eq!(extract_template_params("wind_speed"), Vec::<String>::new());
744    }
745
746    #[test]
747    fn test_extract_template_params_single() {
748        assert_eq!(extract_template_params("weather.{id}"), vec!["id"]);
749    }
750
751    #[test]
752    fn test_extract_template_params_multiple() {
753        assert_eq!(
754            extract_template_params("weather.{id}.{zone}"),
755            vec!["id", "zone"]
756        );
757    }
758
759    #[test]
760    fn test_extract_template_params_empty_braces() {
761        assert_eq!(extract_template_params("weather.{}"), Vec::<String>::new());
762    }
763
764    #[test]
765    fn test_extract_template_params_mixed() {
766        assert_eq!(
767            extract_template_params("prefix.{param1}.middle.{param2}.suffix"),
768            vec!["param1", "param2"]
769        );
770    }
771
772    #[test]
773    fn test_build_subject_expr_no_params() {
774        let subject = "wind_speed";
775        let service_params: Vec<String> = vec![];
776
777        let result = build_subject_expr(subject, &service_params);
778        let expected = quote! { "wind_speed".to_string() };
779
780        assert_eq!(result.to_string(), expected.to_string());
781    }
782
783    #[test]
784    fn test_build_subject_expr_single_param() {
785        let subject = "sensor_data";
786        let service_params = vec!["id".to_string()];
787
788        let result = build_subject_expr(subject, &service_params);
789        let expected = quote! {
790            format!("{}.sensor_data", id)
791        };
792
793        assert_eq!(result.to_string(), expected.to_string());
794    }
795
796    #[test]
797    fn test_build_subject_expr_multiple_params() {
798        let subject = "wind_speed";
799        let service_params = vec!["region".to_string(), "id".to_string()];
800
801        let result = build_subject_expr(subject, &service_params);
802        let expected = quote! {
803            format!("{}.{}.wind_speed", region, id)
804        };
805
806        assert_eq!(result.to_string(), expected.to_string());
807    }
808
809    #[test]
810    fn test_build_subject_expr_three_params() {
811        let subject = "data";
812        let service_params = vec![
813            "namespace".to_string(),
814            "service".to_string(),
815            "id".to_string(),
816        ];
817
818        let result = build_subject_expr(subject, &service_params);
819        let expected = quote! {
820            format!("{}.{}.{}.data", namespace, service, id)
821        };
822
823        assert_eq!(result.to_string(), expected.to_string());
824    }
825
826    #[test]
827    fn test_build_subject_expr_subject_with_special_chars() {
828        let subject = "sensor.temperature_reading";
829        let service_params = vec!["id".to_string()];
830
831        let result = build_subject_expr(subject, &service_params);
832        let expected = quote! {
833            format!("{}.sensor.temperature_reading", id)
834        };
835
836        assert_eq!(result.to_string(), expected.to_string());
837    }
838
839    #[test]
840    fn test_build_subject_expr_empty_subject() {
841        let subject = "";
842        let service_params = vec!["id".to_string()];
843
844        let result = build_subject_expr(subject, &service_params);
845        let expected = quote! {
846            format!("{}.", id)
847        };
848
849        assert_eq!(result.to_string(), expected.to_string());
850    }
851}