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 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 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 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 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 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 } 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 } else {
107 true }
109 });
110
111 if let (Some(name), Some(subject)) = (stream_name, stream_subject) {
112 let method_name = method.sig.ident.clone();
113
114 if method.sig.asyncness.is_some()
116 && let ReturnType::Type(_, ref mut ty) = method.sig.output
117 {
118 let original_ty = (**ty).clone();
120 **ty = syn::parse_quote!(
121 impl ::std::future::Future<Output = #original_ty> + Send
122 );
123 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 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 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 if method.sig.asyncness.is_some()
157 && let ReturnType::Type(_, ref mut ty) = method.sig.output
158 {
159 let original_ty = (**ty).clone();
161 **ty = syn::parse_quote!(
162 impl ::std::future::Future<Output = #original_ty> + Send
163 );
164 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 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 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 let handler_name = syn::Ident::new(
193 &format!("{}Handler", snake_to_pascal(&method_name.to_string())),
194 method_name.span(),
195 );
196
197 handler_structs.push(quote! {
199 struct #handler_name<T>(::std::marker::PhantomData<T>);
200 });
201
202 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 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 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 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 let service_fn_body_prelude = if param_idents.is_empty() {
299 quote! {}
300 } else {
301 quote! { let (#(#param_idents,)*) = params; }
302 };
303
304 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 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 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 #(#handler_structs)*
459
460 #(#handler_debug_impls)*
462
463 #(#handler_impls)*
465
466 #(#stream_handler_structs)*
468
469 #(#stream_handler_debug_impls)*
471
472 #(#stream_handler_impls)*
474
475 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 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 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 input
535}
536
537#[proc_macro_attribute]
538pub fn stream(_args: TokenStream, input: TokenStream) -> TokenStream {
539 input
541}
542
543fn extract_response_type(ty: &syn::Type) -> Option<syn::Type> {
545 if let syn::Type::Path(type_path) = ty {
546 if let Some(segment) = type_path.path.segments.last()
548 && segment.ident == "Result"
549 && let syn::PathArguments::AngleBracketed(args) = &segment.arguments
550 {
551 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 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
583fn 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
607fn build_subject_expr(subject: &str, service_params: &[String]) -> proc_macro2::TokenStream {
611 if service_params.is_empty() {
612 return quote! { #subject.to_string() };
614 }
615
616 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 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
637fn 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 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
660fn 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}