1use proc_macro::TokenStream;
23use quote::{format_ident, quote};
24use syn::{Data, DeriveInput, Fields, Lit, parse_macro_input};
25
26#[proc_macro_derive(PlushieEnum, attributes(plushie_type, plushie))]
58pub fn derive_plushie_enum(input: TokenStream) -> TokenStream {
59 let input = parse_macro_input!(input as DeriveInput);
60
61 match derive_enum_impl(&input) {
62 Ok(tokens) => tokens.into(),
63 Err(err) => err.to_compile_error().into(),
64 }
65}
66
67struct VariantMeta {
69 ident: syn::Ident,
70 wire_name: String,
71 aliases: Vec<String>,
72}
73
74fn derive_enum_impl(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
75 let type_name = extract_plushie_type_name(input)?;
76
77 let variants = match &input.data {
78 Data::Enum(data) => &data.variants,
79 _ => {
80 return Err(syn::Error::new_spanned(
81 &input.ident,
82 "PlushieEnum can only be derived for enums",
83 ));
84 }
85 };
86
87 for v in variants {
89 if !matches!(v.fields, Fields::Unit) {
90 return Err(syn::Error::new_spanned(
91 v,
92 "PlushieEnum requires all variants to be unit variants (no fields)",
93 ));
94 }
95 }
96
97 let metas: Vec<VariantMeta> = variants
98 .iter()
99 .map(extract_variant_meta)
100 .collect::<syn::Result<_>>()?;
101
102 let enum_name = &input.ident;
103
104 let decode_arms = metas.iter().map(|m| {
106 let ident = &m.ident;
107 let wire = &m.wire_name;
108 let alias_pats = m.aliases.iter().map(|a| quote! { | #a });
109 quote! {
110 #wire #(#alias_pats)* => ::core::option::Option::Some(Self::#ident)
111 }
112 });
113
114 let encode_arms = metas.iter().map(|m| {
116 let ident = &m.ident;
117 let wire = &m.wire_name;
118 quote! {
119 Self::#ident => #wire
120 }
121 });
122
123 let extract_arms = metas.iter().map(|m| {
125 let ident = &m.ident;
126 let wire = &m.wire_name;
127 let alias_pats = m.aliases.iter().map(|a| quote! { | #a });
128 quote! {
129 #wire #(#alias_pats)* => ::core::option::Option::Some(Self::#ident)
130 }
131 });
132
133 Ok(quote! {
134 impl ::plushie_core::types::PlushieType for #enum_name {
135 fn wire_decode(value: &::serde_json::Value) -> ::core::option::Option<Self> {
136 match value.as_str()? {
137 #(#decode_arms,)*
138 _ => ::core::option::Option::None,
139 }
140 }
141
142 fn wire_encode(&self) -> ::plushie_core::protocol::PropValue {
143 ::plushie_core::protocol::PropValue::Str(
144 match self {
145 #(#encode_arms,)*
146 }
147 .into(),
148 )
149 }
150
151 fn extract(
152 props: &::plushie_core::protocol::Props,
153 key: &str,
154 ) -> ::core::option::Option<Self> {
155 match props.get_str(key)? {
156 #(#extract_arms,)*
157 _ => ::core::option::Option::None,
158 }
159 }
160
161 fn type_name() -> &'static str {
162 #type_name
163 }
164 }
165 })
166}
167
168fn extract_plushie_type_name(input: &DeriveInput) -> syn::Result<String> {
169 for attr in &input.attrs {
170 if attr.path().is_ident("plushie_type") {
171 let mut name = None;
172 attr.parse_nested_meta(|meta| {
173 if meta.path.is_ident("name") {
174 let value = meta.value()?;
175 let lit: Lit = value.parse()?;
176 if let Lit::Str(s) = lit {
177 name = Some(s.value());
178 Ok(())
179 } else {
180 Err(meta.error("expected string literal for plushie_type name"))
181 }
182 } else {
183 Err(meta.error("unknown plushie_type attribute, expected `name`"))
184 }
185 })?;
186 return name.ok_or_else(|| {
187 syn::Error::new_spanned(attr, "plushie_type attribute requires name = \"...\"")
188 });
189 }
190 }
191 Err(syn::Error::new_spanned(
192 &input.ident,
193 "PlushieEnum requires #[plushie_type(name = \"...\")] attribute",
194 ))
195}
196
197fn extract_variant_meta(variant: &syn::Variant) -> syn::Result<VariantMeta> {
198 let ident = variant.ident.clone();
199 let mut wire_name: Option<String> = None;
200 let mut aliases: Vec<String> = Vec::new();
201
202 for attr in &variant.attrs {
203 if attr.path().is_ident("plushie") {
204 attr.parse_nested_meta(|meta| {
205 if meta.path.is_ident("wire") {
206 let value = meta.value()?;
207 let lit: Lit = value.parse()?;
208 if let Lit::Str(s) = lit {
209 wire_name = Some(s.value());
210 Ok(())
211 } else {
212 Err(meta.error("expected string literal for wire name"))
213 }
214 } else if meta.path.is_ident("aliases") {
215 let value = meta.value()?;
216 let array: syn::ExprArray = value.parse()?;
217 for elem in &array.elems {
218 if let syn::Expr::Lit(syn::ExprLit {
219 lit: Lit::Str(s), ..
220 }) = elem
221 {
222 aliases.push(s.value());
223 } else {
224 return Err(syn::Error::new_spanned(
225 elem,
226 "expected string literal in aliases array",
227 ));
228 }
229 }
230 Ok(())
231 } else {
232 Err(meta.error("unknown plushie attribute, expected `wire` or `aliases`"))
233 }
234 })?;
235 }
236 }
237
238 let wire_name = wire_name.unwrap_or_else(|| pascal_to_snake(&ident.to_string()));
239
240 Ok(VariantMeta {
241 ident,
242 wire_name,
243 aliases,
244 })
245}
246
247fn pascal_to_snake(s: &str) -> String {
252 let mut result = String::with_capacity(s.len() + 4);
253 let chars: Vec<char> = s.chars().collect();
254
255 for (i, &c) in chars.iter().enumerate() {
256 if c == '_' {
257 if !result.ends_with('_') && !result.is_empty() {
258 result.push('_');
259 }
260 continue;
261 }
262
263 if i > 0 && should_insert_snake_boundary(chars[i - 1], c, chars.get(i + 1).copied()) {
264 result.push('_');
265 }
266
267 if c.is_uppercase() {
268 result.extend(c.to_lowercase());
269 } else {
270 result.push(c);
271 }
272 }
273
274 if result.ends_with('_') {
275 result.pop();
276 }
277
278 result
279}
280
281fn should_insert_snake_boundary(prev: char, current: char, next: Option<char>) -> bool {
282 if prev == '_' || current == '_' {
283 return false;
284 }
285
286 let lower_to_upper = prev.is_lowercase() && current.is_uppercase();
287 let acronym_to_word =
288 prev.is_uppercase() && current.is_uppercase() && next.is_some_and(char::is_lowercase);
289 let lower_to_digit =
290 prev.is_lowercase() && current.is_ascii_digit() && next.is_some_and(char::is_uppercase);
291 let digit_to_word =
292 prev.is_ascii_digit() && current.is_uppercase() && next.is_some_and(char::is_lowercase);
293
294 lower_to_upper || acronym_to_word || lower_to_digit || digit_to_word
295}
296
297#[proc_macro_derive(WidgetEvent)]
333pub fn derive_widget_event(input: TokenStream) -> TokenStream {
334 let input = parse_macro_input!(input as DeriveInput);
335
336 match derive_widget_event_impl(&input) {
337 Ok(tokens) => tokens.into(),
338 Err(err) => err.to_compile_error().into(),
339 }
340}
341
342fn derive_widget_event_impl(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
343 let enum_name = &input.ident;
344
345 let variants = match &input.data {
346 Data::Enum(data) => &data.variants,
347 _ => {
348 return Err(syn::Error::new_spanned(
349 enum_name,
350 "WidgetEvent can only be derived for enums",
351 ));
352 }
353 };
354
355 for v in variants {
357 if let Fields::Unnamed(fields) = &v.fields
358 && fields.unnamed.len() > 1
359 {
360 return Err(syn::Error::new_spanned(
361 v,
362 "WidgetEvent tuple variants must have exactly one field; \
363 use named fields for multiple values",
364 ));
365 }
366 }
367
368 let match_arms = variants.iter().map(|v| {
369 let ident = &v.ident;
370 let family = pascal_to_snake(&ident.to_string());
371
372 match &v.fields {
373 Fields::Unit => {
374 quote! {
375 Self::#ident => (#family, ::plushie_core::protocol::PropValue::Null)
376 }
377 }
378 Fields::Unnamed(_) => {
379 quote! {
381 Self::#ident(v) => (
382 #family,
383 ::plushie_core::types::PlushieType::wire_encode(v),
384 )
385 }
386 }
387 Fields::Named(fields) => {
388 let field_names: Vec<_> = fields
389 .named
390 .iter()
391 .map(|f| f.ident.as_ref().unwrap())
392 .collect();
393 let field_keys: Vec<_> = field_names.iter().map(|n| n.to_string()).collect();
394 let inserts = field_names
395 .iter()
396 .zip(field_keys.iter())
397 .map(|(name, key)| {
398 quote! {
399 map.insert(
400 #key,
401 ::plushie_core::types::PlushieType::wire_encode(#name),
402 );
403 }
404 });
405
406 quote! {
407 Self::#ident { #(#field_names),* } => {
408 let mut map = ::plushie_core::protocol::PropMap::new();
409 #(#inserts)*
410 (#family, ::plushie_core::protocol::PropValue::Object(map))
411 }
412 }
413 }
414 }
415 });
416
417 let spec_arms = generate_spec_arms(variants, "EventSpec", "WidgetEvent")?;
418
419 Ok(quote! {
420 impl ::plushie_core::types::WidgetEventEncode for #enum_name {
421 fn to_wire(&self) -> (&'static str, ::plushie_core::protocol::PropValue) {
422 match self {
423 #(#match_arms,)*
424 }
425 }
426 }
427
428 impl #enum_name {
429 pub fn event_specs() -> Vec<::plushie_core::spec::EventSpec> {
431 vec![#(#spec_arms,)*]
432 }
433 }
434 })
435}
436
437#[proc_macro_derive(WidgetCommand)]
471pub fn derive_widget_command(input: TokenStream) -> TokenStream {
472 let input = parse_macro_input!(input as DeriveInput);
473
474 match derive_widget_command_impl(&input) {
475 Ok(tokens) => tokens.into(),
476 Err(err) => err.to_compile_error().into(),
477 }
478}
479
480fn derive_widget_command_impl(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
481 let enum_name = &input.ident;
482
483 let variants = match &input.data {
484 Data::Enum(data) => &data.variants,
485 _ => {
486 return Err(syn::Error::new_spanned(
487 enum_name,
488 "WidgetCommand can only be derived for enums",
489 ));
490 }
491 };
492
493 for v in variants {
495 if let Fields::Unnamed(fields) = &v.fields
496 && fields.unnamed.len() > 1
497 {
498 return Err(syn::Error::new_spanned(
499 v,
500 "WidgetCommand tuple variants must have exactly one field; \
501 use named fields for multiple values",
502 ));
503 }
504 }
505
506 let match_arms = variants.iter().map(|v| {
508 let ident = &v.ident;
509 let op = pascal_to_snake(&ident.to_string());
510
511 match &v.fields {
512 Fields::Unit => {
513 quote! {
514 Self::#ident => (#op, ::plushie_core::protocol::PropValue::Null)
515 }
516 }
517 Fields::Unnamed(_) => {
518 quote! {
519 Self::#ident(v) => (
520 #op,
521 ::plushie_core::types::PlushieType::wire_encode(v),
522 )
523 }
524 }
525 Fields::Named(fields) => {
526 let field_names: Vec<_> = fields
527 .named
528 .iter()
529 .map(|f| f.ident.as_ref().unwrap())
530 .collect();
531 let field_keys: Vec<_> = field_names.iter().map(|n| n.to_string()).collect();
532 let inserts = field_names
533 .iter()
534 .zip(field_keys.iter())
535 .map(|(name, key)| {
536 quote! {
537 map.insert(
538 #key,
539 ::plushie_core::types::PlushieType::wire_encode(#name),
540 );
541 }
542 });
543
544 quote! {
545 Self::#ident { #(#field_names),* } => {
546 let mut map = ::plushie_core::protocol::PropMap::new();
547 #(#inserts)*
548 (#op, ::plushie_core::protocol::PropValue::Object(map))
549 }
550 }
551 }
552 }
553 });
554
555 let spec_arms = generate_spec_arms(variants, "CommandSpec", "WidgetCommand")?;
556
557 Ok(quote! {
558 impl ::plushie_core::spec::WidgetCommandEncode for #enum_name {
559 fn to_wire(&self) -> (&'static str, ::plushie_core::protocol::PropValue) {
560 match self {
561 #(#match_arms,)*
562 }
563 }
564
565 fn command_specs() -> Vec<::plushie_core::spec::CommandSpec> {
566 vec![#(#spec_arms,)*]
567 }
568 }
569 })
570}
571
572fn generate_spec_arms<'a>(
581 variants: impl IntoIterator<Item = &'a syn::Variant>,
582 spec_type: &str,
583 derive_name: &str,
584) -> syn::Result<Vec<proc_macro2::TokenStream>> {
585 let spec_ident = format_ident!("{}", spec_type);
586 let name_field = format_ident!("family");
587
588 variants
589 .into_iter()
590 .map(|v| {
591 let name = pascal_to_snake(&v.ident.to_string());
592
593 let payload = match &v.fields {
594 Fields::Unit => {
595 quote! { ::plushie_core::spec::PayloadSpec::None }
596 }
597 Fields::Unnamed(fields) => {
598 let ty = &fields.unnamed.first().unwrap().ty;
599 let vt = rust_type_to_value_type(ty, derive_name)?;
600 quote! { ::plushie_core::spec::PayloadSpec::Value(#vt) }
601 }
602 Fields::Named(fields) => {
603 let field_specs: Vec<_> = fields
604 .named
605 .iter()
606 .map(|f| {
607 let fname = f.ident.as_ref().unwrap().to_string();
608 let vt = rust_type_to_value_type(&f.ty, derive_name)?;
609 Ok(quote! { (#fname.to_string(), #vt) })
610 })
611 .collect::<syn::Result<_>>()?;
612 let required: Vec<_> = fields
613 .named
614 .iter()
615 .map(|f| f.ident.as_ref().unwrap().to_string())
616 .collect();
617 quote! {
618 ::plushie_core::spec::PayloadSpec::Fields {
619 fields: vec![#(#field_specs),*],
620 required: vec![#(#required.to_string()),*],
621 }
622 }
623 }
624 };
625
626 Ok(quote! {
627 ::plushie_core::spec::#spec_ident {
628 #name_field: #name.to_string(),
629 payload: #payload,
630 }
631 })
632 })
633 .collect()
634}
635
636fn rust_type_to_value_type(
638 ty: &syn::Type,
639 derive_name: &str,
640) -> syn::Result<proc_macro2::TokenStream> {
641 if path_matches(ty, &["f32"]) || path_matches(ty, &["f64"]) {
642 return Ok(quote! { ::plushie_core::spec::ValueType::Float });
643 }
644 if path_matches(ty, &["i32"])
645 || path_matches(ty, &["i64"])
646 || path_matches(ty, &["u32"])
647 || path_matches(ty, &["u64"])
648 {
649 return Ok(quote! { ::plushie_core::spec::ValueType::Integer });
650 }
651 if path_matches(ty, &["bool"]) {
652 return Ok(quote! { ::plushie_core::spec::ValueType::Bool });
653 }
654 if path_matches(ty, &["String"])
655 || path_matches(ty, &["std", "string", "String"])
656 || path_matches(ty, &["alloc", "string", "String"])
657 {
658 return Ok(quote! { ::plushie_core::spec::ValueType::String });
659 }
660 if path_matches(ty, &["PropValue"])
661 || path_matches(ty, &["plushie_core", "protocol", "PropValue"])
662 {
663 return Ok(quote! { ::plushie_core::spec::ValueType::Any });
664 }
665
666 Err(syn::Error::new_spanned(
667 ty,
668 format!(
669 "unsupported {derive_name} payload type `{}`; supported payload types are f32, f64, i32, i64, u32, u64, bool, String, std::string::String, alloc::string::String, and plushie_core::protocol::PropValue",
670 quote!(#ty)
671 ),
672 ))
673}
674
675fn path_matches(ty: &syn::Type, expected: &[&str]) -> bool {
676 let syn::Type::Path(type_path) = ty else {
677 return false;
678 };
679 if type_path.qself.is_some() {
680 return false;
681 }
682
683 let mut segments = type_path.path.segments.iter();
684 for expected_ident in expected {
685 let Some(segment) = segments.next() else {
686 return false;
687 };
688 if segment.ident != expected_ident {
689 return false;
690 }
691 if !matches!(segment.arguments, syn::PathArguments::None) {
692 return false;
693 }
694 }
695 segments.next().is_none()
696}
697
698#[proc_macro_derive(WidgetProps, attributes(widget, field, widget_props))]
729pub fn derive_plushie_widget(input: TokenStream) -> TokenStream {
730 let input = parse_macro_input!(input as DeriveInput);
731
732 match derive_widget_impl(&input) {
733 Ok(tokens) => tokens.into(),
734 Err(err) => err.to_compile_error().into(),
735 }
736}
737
738fn derive_widget_impl(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
739 let widget_name = extract_widget_name(input)?;
740 let is_container = has_widget_props_container_attr(input);
741
742 let fields = match &input.data {
743 Data::Struct(data) => match &data.fields {
744 Fields::Named(fields) => &fields.named,
745 _ => {
746 return Err(syn::Error::new_spanned(
747 &input.ident,
748 "WidgetProps requires named fields",
749 ));
750 }
751 },
752 _ => {
753 return Err(syn::Error::new_spanned(
754 &input.ident,
755 "WidgetProps can only be derived for structs",
756 ));
757 }
758 };
759
760 let struct_name = &input.ident;
761 let props_name = format_ident!("{}Props", struct_name);
762
763 let prop_fields = fields.iter().map(|f| {
765 let name = &f.ident;
766 let ty = &f.ty;
767 let docs = f
768 .attrs
769 .iter()
770 .filter(|a| a.path().is_ident("doc"))
771 .collect::<Vec<_>>();
772 quote! {
773 #(#docs)*
774 pub #name: Option<#ty>
775 }
776 });
777
778 let extractions = fields.iter().map(|f| {
780 let name = &f.ident;
781 let ty = &f.ty;
782 let key = name.as_ref().unwrap().to_string();
783 quote! {
784 #name: <#ty as ::plushie_core::types::PlushieType>::extract(p, #key)
785 }
786 });
787
788 let field_names: Vec<_> = fields.iter().map(|f| &f.ident).collect();
789
790 let debug_fields = field_names.iter().map(|name| {
792 let name_str = name.as_ref().unwrap().to_string();
793 quote! {
794 .field(#name_str, &self.#name)
795 }
796 });
797
798 let field_list: String = fields
800 .iter()
801 .map(|f| {
802 let name = f.ident.as_ref().unwrap().to_string();
803 let ty = &f.ty;
804 let ty_str = quote!(#ty).to_string();
805 let doc = f
807 .attrs
808 .iter()
809 .filter(|a| a.path().is_ident("doc"))
810 .filter_map(|a| {
811 if let syn::Meta::NameValue(nv) = &a.meta
812 && let syn::Expr::Lit(lit) = &nv.value
813 && let syn::Lit::Str(s) = &lit.lit
814 {
815 return Some(s.value().trim().to_string());
816 }
817 None
818 })
819 .next();
820 match doc {
821 Some(d) => format!("- **`{}`** (`{}`): {}", name, ty_str, d),
822 None => format!("- **`{}`** (`{}`)", name, ty_str),
823 }
824 })
825 .collect::<Vec<_>>()
826 .join("\n");
827
828 let props_doc = format!(
829 "Typed properties for the `{}` widget.\n\n## Fields\n\n{}",
830 widget_name, field_list
831 );
832 let from_node_doc = format!("Extract properties from a `{}` tree node.", widget_name);
833 let type_name_doc = format!("The widget type name: `\"{}\"`.", widget_name);
834
835 let extractions_for_trait = fields.iter().map(|f| {
838 let name = &f.ident;
839 let ty = &f.ty;
840 let key = name.as_ref().unwrap().to_string();
841 quote! {
842 #name: <#ty as ::plushie_core::types::PlushieType>::extract(p, #key)
843 }
844 });
845
846 let builder_name = format_ident!("{}Builder", struct_name);
849
850 let builder_setters = fields.iter().map(|f| {
851 let name = f.ident.as_ref().unwrap();
852 let ty = &f.ty;
853 let key = name.to_string();
854 let doc = f
855 .attrs
856 .iter()
857 .filter(|a| a.path().is_ident("doc"))
858 .filter_map(|a| {
859 if let syn::Meta::NameValue(nv) = &a.meta
860 && let syn::Expr::Lit(lit) = &nv.value
861 && let syn::Lit::Str(s) = &lit.lit
862 {
863 return Some(s.value().trim().to_string());
864 }
865 None
866 })
867 .next();
868 let setter_doc = match doc {
869 Some(d) => d,
870 None => format!("The `{}` property.", key),
871 };
872
873 quote! {
874 #[doc = #setter_doc]
875 pub fn #name(mut self, v: #ty) -> Self {
876 self.0.props.insert(
877 #key,
878 ::plushie_core::types::PlushieType::wire_encode(&v),
879 );
880 self
881 }
882 }
883 });
884
885 let builder_doc = format!(
886 "Builder for the `{}` widget.\n\n\
887 ## Properties\n\n{}",
888 widget_name, field_list
889 );
890 let builder_new_doc = format!(
891 "Create a new `{}` widget builder with the given ID.",
892 widget_name
893 );
894 let builder_fn_doc = format!(
895 "Create a `{}` widget builder with the given ID.",
896 widget_name
897 );
898
899 let container_methods = if is_container {
900 quote! {
901 pub fn child(mut self, child: ::plushie_core::protocol::TreeNode) -> Self {
903 self.0.children.push(child);
904 self
905 }
906
907 pub fn children(mut self, children: ::std::vec::Vec<::plushie_core::protocol::TreeNode>) -> Self {
909 self.0.children = children;
910 self
911 }
912 }
913 } else {
914 quote! {}
915 };
916
917 Ok(quote! {
918 #[doc = #props_doc]
919 pub struct #props_name {
920 #(#prop_fields,)*
921 }
922
923 impl #props_name {
924 #[doc = #from_node_doc]
925 pub fn from_node(node: &::plushie_core::protocol::TreeNode) -> Self {
926 let p = &node.props;
927 Self {
928 #(#extractions,)*
929 }
930 }
931 }
932
933 impl ::plushie_core::types::FromNode for #props_name {
934 fn from_node(node: &::plushie_core::protocol::TreeNode) -> Self {
935 let p = &node.props;
936 Self {
937 #(#extractions_for_trait,)*
938 }
939 }
940 }
941
942 impl ::core::fmt::Debug for #props_name {
943 fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
944 f.debug_struct(stringify!(#props_name))
945 #(#debug_fields)*
946 .finish()
947 }
948 }
949
950 impl #struct_name {
951 #[doc = #type_name_doc]
952 pub fn type_name() -> &'static str {
953 #widget_name
954 }
955
956 #[doc = #builder_fn_doc]
957 pub fn builder(id: &str) -> #builder_name {
958 #builder_name::new(id)
959 }
960 }
961
962 #[doc = #builder_doc]
963 pub struct #builder_name(pub ::plushie_core::WidgetBuilder);
964
965 impl #builder_name {
966 #[doc = #builder_new_doc]
967 pub fn new(id: &str) -> Self {
968 Self(::plushie_core::WidgetBuilder::new(#widget_name, id))
969 }
970
971 #(#builder_setters)*
972
973 pub fn prop(mut self, key: &str, value: impl Into<::plushie_core::protocol::PropValue>) -> Self {
975 self.0.props.insert(key, value.into());
976 self
977 }
978
979 #container_methods
980 }
981 })
982}
983
984#[proc_macro_derive(PlushieWidget, attributes(plushie_widget))]
1026pub fn derive_plushie_widget_trait(input: TokenStream) -> TokenStream {
1027 let input = parse_macro_input!(input as DeriveInput);
1028
1029 match derive_plushie_widget_trait_impl(&input) {
1030 Ok(tokens) => tokens.into(),
1031 Err(err) => err.to_compile_error().into(),
1032 }
1033}
1034
1035fn derive_plushie_widget_trait_impl(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
1036 let type_name = extract_plushie_widget_type_name(input)?;
1037 let struct_name = &input.ident;
1038
1039 let is_unit = matches!(
1040 &input.data,
1041 Data::Struct(data) if matches!(&data.fields, Fields::Unit)
1042 );
1043 let fresh_expr = if is_unit {
1044 quote! { ::std::boxed::Box::new(Self) }
1045 } else {
1046 quote! { ::std::boxed::Box::new(<Self as ::core::default::Default>::default()) }
1047 };
1048
1049 Ok(quote! {
1050 impl<__R: ::plushie_widget_sdk::PlushieRenderer>
1051 ::plushie_widget_sdk::registry::PlushieWidget<__R> for #struct_name
1052 where
1053 Self: ::plushie_widget_sdk::registry::PlushieWidgetRender<__R>,
1054 {
1055 fn type_names(&self) -> &[&str] {
1056 &[#type_name]
1057 }
1058
1059 fn render<'a>(
1060 &'a self,
1061 node: &'a ::plushie_widget_sdk::protocol::TreeNode,
1062 ctx: &::plushie_widget_sdk::render_ctx::RenderCtx<'a, __R>,
1063 ) -> ::plushie_widget_sdk::PlushieElement<'a, __R> {
1064 <Self as ::plushie_widget_sdk::registry::PlushieWidgetRender<__R>>::render(
1065 self, node, ctx,
1066 )
1067 }
1068
1069 fn fresh_for_session(&self)
1070 -> ::std::boxed::Box<dyn ::plushie_widget_sdk::registry::PlushieWidget<__R>>
1071 {
1072 #fresh_expr
1073 }
1074 }
1075 })
1076}
1077
1078fn extract_plushie_widget_type_name(input: &DeriveInput) -> syn::Result<String> {
1079 for attr in &input.attrs {
1080 if attr.path().is_ident("plushie_widget") {
1081 let mut name = None;
1082 attr.parse_nested_meta(|meta| {
1083 if meta.path.is_ident("type_name") {
1084 let value = meta.value()?;
1085 let lit: Lit = value.parse()?;
1086 if let Lit::Str(s) = lit {
1087 name = Some(s.value());
1088 Ok(())
1089 } else {
1090 Err(meta.error("expected string literal for type_name"))
1091 }
1092 } else {
1093 Err(meta.error("unknown plushie_widget attribute, expected `type_name`"))
1094 }
1095 })?;
1096 return name.ok_or_else(|| {
1097 syn::Error::new_spanned(
1098 attr,
1099 "plushie_widget attribute requires type_name = \"...\"",
1100 )
1101 });
1102 }
1103 }
1104 Err(syn::Error::new_spanned(
1105 &input.ident,
1106 "PlushieWidget derive requires #[plushie_widget(type_name = \"...\")] attribute",
1107 ))
1108}
1109
1110fn has_widget_props_container_attr(input: &DeriveInput) -> bool {
1111 for attr in &input.attrs {
1112 if attr.path().is_ident("widget_props") {
1113 let mut is_container = false;
1114 let _ = attr.parse_nested_meta(|meta| {
1115 if meta.path.is_ident("container") {
1116 is_container = true;
1117 }
1118 Ok(())
1119 });
1120 if is_container {
1121 return true;
1122 }
1123 }
1124 }
1125 false
1126}
1127
1128fn extract_widget_name(input: &DeriveInput) -> syn::Result<String> {
1129 for attr in &input.attrs {
1130 if attr.path().is_ident("widget") {
1131 let mut name = None;
1132 attr.parse_nested_meta(|meta| {
1133 if meta.path.is_ident("name") {
1134 let value = meta.value()?;
1135 let lit: Lit = value.parse()?;
1136 if let Lit::Str(s) = lit {
1137 name = Some(s.value());
1138 Ok(())
1139 } else {
1140 Err(meta.error("expected string literal for widget name"))
1141 }
1142 } else {
1143 Err(meta.error("unknown widget attribute, expected `name`"))
1144 }
1145 })?;
1146 return name.ok_or_else(|| {
1147 syn::Error::new_spanned(attr, "widget attribute requires name = \"...\"")
1148 });
1149 }
1150 }
1151 Err(syn::Error::new_spanned(
1152 &input.ident,
1153 "WidgetProps requires #[widget(name = \"...\")] attribute",
1154 ))
1155}
1156
1157#[proc_macro]
1222pub fn widget(input: TokenStream) -> TokenStream {
1223 let input2: proc_macro2::TokenStream = input.into();
1224 match widget_impl(input2) {
1225 Ok(tokens) => tokens.into(),
1226 Err(err) => err.to_compile_error().into(),
1227 }
1228}
1229
1230struct WidgetInput {
1232 attrs: Vec<syn::Attribute>,
1233 meta: WidgetMeta,
1234 vis: syn::Visibility,
1235 ident: syn::Ident,
1236 fields: syn::FieldsNamed,
1237 events: Option<WidgetEventsBlock>,
1238}
1239
1240struct WidgetMeta {
1242 type_name: String,
1243 crate_name: Option<String>,
1244}
1245
1246struct WidgetEventsBlock {
1248 ident: syn::Ident,
1249 variants: syn::punctuated::Punctuated<syn::Variant, syn::Token![,]>,
1250}
1251
1252impl syn::parse::Parse for WidgetInput {
1253 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
1254 let attrs = input.call(syn::Attribute::parse_outer)?;
1257 let vis: syn::Visibility = input.parse()?;
1258 let _struct_token: syn::Token![struct] = input.parse()?;
1259 let ident: syn::Ident = input.parse()?;
1260 let fields: syn::FieldsNamed = input.parse()?;
1261
1262 let events = if input.peek(syn::Ident) {
1264 let lookahead: syn::Ident = input.fork().parse()?;
1265 if lookahead == "events" {
1266 let _events_kw: syn::Ident = input.parse()?;
1267 let content;
1268 syn::braced!(content in input);
1269 let variants = content.parse_terminated(syn::Variant::parse, syn::Token![,])?;
1270 Some(WidgetEventsBlock {
1271 ident: format_ident!("{}Event", ident),
1272 variants,
1273 })
1274 } else {
1275 None
1276 }
1277 } else {
1278 None
1279 };
1280
1281 let meta = parse_widget_meta(&attrs, &ident)?;
1282
1283 Ok(WidgetInput {
1284 attrs,
1285 meta,
1286 vis,
1287 ident,
1288 fields,
1289 events,
1290 })
1291 }
1292}
1293
1294fn parse_widget_meta(attrs: &[syn::Attribute], ident: &syn::Ident) -> syn::Result<WidgetMeta> {
1295 let mut type_name: Option<String> = None;
1296 let mut crate_name: Option<String> = None;
1297
1298 for attr in attrs {
1299 if !attr.path().is_ident("widget") {
1300 continue;
1301 }
1302 attr.parse_nested_meta(|meta| {
1303 if meta.path.is_ident("type_name") {
1304 let value = meta.value()?;
1305 let lit: Lit = value.parse()?;
1306 if let Lit::Str(s) = lit {
1307 type_name = Some(s.value());
1308 Ok(())
1309 } else {
1310 Err(meta.error("type_name must be a string literal"))
1311 }
1312 } else if meta.path.is_ident("crate") {
1313 let value = meta.value()?;
1314 let lit: Lit = value.parse()?;
1315 if let Lit::Str(s) = lit {
1316 crate_name = Some(s.value());
1317 Ok(())
1318 } else {
1319 Err(meta.error("crate must be a string literal"))
1320 }
1321 } else if meta.path.is_ident("constructor") {
1322 Err(meta.error(
1327 "`constructor` is no longer accepted in `#[widget(...)]`; \
1328 declare it once in `[package.metadata.plushie.widget]` in \
1329 the crate's Cargo.toml",
1330 ))
1331 } else {
1332 Err(meta.error("unknown widget attribute (expected `type_name` or `crate`)"))
1333 }
1334 })?;
1335 }
1336
1337 let type_name = type_name.ok_or_else(|| {
1338 syn::Error::new_spanned(
1339 ident,
1340 "widget! requires #[widget(type_name = \"...\")] above the struct",
1341 )
1342 })?;
1343
1344 Ok(WidgetMeta {
1345 type_name,
1346 crate_name,
1347 })
1348}
1349
1350fn widget_impl(input: proc_macro2::TokenStream) -> syn::Result<proc_macro2::TokenStream> {
1351 let parsed: WidgetInput = syn::parse2(input)?;
1352
1353 let WidgetInput {
1354 attrs,
1355 meta,
1356 vis,
1357 ident,
1358 fields,
1359 events,
1360 } = parsed;
1361
1362 let pass_attrs: Vec<&syn::Attribute> = attrs
1365 .iter()
1366 .filter(|a| !a.path().is_ident("widget"))
1367 .collect();
1368
1369 let type_name = &meta.type_name;
1370 let struct_fields: Vec<(&syn::Ident, &syn::Type, Vec<&syn::Attribute>)> = fields
1371 .named
1372 .iter()
1373 .map(|f| {
1374 let fname = f.ident.as_ref().expect("named field");
1375 let ty = &f.ty;
1376 let docs: Vec<&syn::Attribute> = f
1377 .attrs
1378 .iter()
1379 .filter(|a| a.path().is_ident("doc"))
1380 .collect();
1381 (fname, ty, docs)
1382 })
1383 .collect();
1384
1385 let decl_fields = struct_fields.iter().map(|(fname, ty, docs)| {
1388 quote! {
1389 #(#docs)*
1390 pub #fname: ::core::option::Option<#ty>
1391 }
1392 });
1393
1394 let default_inits = struct_fields.iter().map(|(fname, _, _)| {
1396 quote! { #fname: ::core::option::Option::None }
1397 });
1398
1399 let builder_methods = struct_fields.iter().map(|(fname, ty, docs)| {
1403 quote! {
1404 #(#docs)*
1405 pub fn #fname(mut self, value: #ty) -> Self {
1406 self.#fname = ::core::option::Option::Some(value);
1407 self
1408 }
1409 }
1410 });
1411
1412 let to_props_inserts = struct_fields.iter().map(|(fname, _, _)| {
1414 let key = fname.to_string();
1415 quote! {
1416 if let ::core::option::Option::Some(v) = widget.#fname {
1417 props.insert(
1418 #key,
1419 ::plushie_core::types::PlushieType::wire_encode(&v),
1420 );
1421 }
1422 }
1423 });
1424
1425 let crate_name_json = match &meta.crate_name {
1431 Some(c) => format!(",\"crate\":\"{}\"", escape_json(c)),
1432 None => String::new(),
1433 };
1434 let metadata_str = format!(
1435 "{{\"type_name\":\"{}\",\"struct\":\"{}\"{}}}",
1436 escape_json(type_name),
1437 ident,
1438 crate_name_json,
1439 );
1440
1441 let events_decl = events.as_ref().map(|e| {
1444 let ename = &e.ident;
1445 let variants = e.variants.iter();
1446 quote! {
1447 #[derive(::core::fmt::Debug, ::core::clone::Clone, ::plushie_core::WidgetEvent)]
1448 pub enum #ename {
1449 #(#variants),*
1450 }
1451 }
1452 });
1453
1454 let new_doc = format!("Create a new `{}` widget builder with the given ID.", ident);
1456 let struct_doc = format!(
1457 "`{}` widget. Type name: `\"{}\"`. Built by the `widget!` macro.",
1458 ident, type_name
1459 );
1460 let metadata_doc = format!(
1461 "Build-time metadata for the `{}` widget (consumed by `cargo plushie build`).",
1462 ident
1463 );
1464
1465 Ok(quote! {
1466 #(#pass_attrs)*
1467 #[doc = #struct_doc]
1468 #vis struct #ident {
1469 pub id: ::std::string::String,
1471 #(#decl_fields,)*
1472 }
1473
1474 impl #ident {
1475 #[doc = #new_doc]
1476 pub fn new(id: impl ::core::convert::Into<::std::string::String>) -> Self {
1477 Self {
1478 id: id.into(),
1479 #(#default_inits,)*
1480 }
1481 }
1482
1483 pub const fn type_name() -> &'static str {
1485 #type_name
1486 }
1487
1488 #(#builder_methods)*
1489 }
1490
1491 impl ::core::convert::From<#ident> for ::plushie_core::protocol::TreeNode {
1492 fn from(widget: #ident) -> Self {
1493 let mut props = ::plushie_core::protocol::PropMap::new();
1494 #(#to_props_inserts)*
1495 ::plushie_core::protocol::TreeNode {
1496 id: widget.id,
1497 type_name: #type_name.to_string(),
1498 props: ::plushie_core::protocol::Props::from(props),
1499 children: ::std::vec::Vec::new(),
1500 }
1501 }
1502 }
1503
1504 #[doc = #metadata_doc]
1505 pub const PLUSHIE_WIDGET_METADATA: &::core::primitive::str = #metadata_str;
1506
1507 #events_decl
1508 })
1509}
1510
1511fn escape_json(s: &str) -> String {
1513 let mut out = String::with_capacity(s.len());
1514 for c in s.chars() {
1515 match c {
1516 '"' => out.push_str("\\\""),
1517 '\\' => out.push_str("\\\\"),
1518 '\n' => out.push_str("\\n"),
1519 '\r' => out.push_str("\\r"),
1520 '\t' => out.push_str("\\t"),
1521 c if (c as u32) < 0x20 => {
1522 out.push_str(&format!("\\u{:04x}", c as u32));
1523 }
1524 c => out.push(c),
1525 }
1526 }
1527 out
1528}
1529
1530#[cfg(test)]
1535mod tests {
1536 use super::*;
1537 use syn::{DeriveInput, parse_quote};
1538
1539 #[test]
1542 fn pascal_to_snake_simple() {
1543 assert_eq!(pascal_to_snake("None"), "none");
1544 assert_eq!(pascal_to_snake("Word"), "word");
1545 assert_eq!(pascal_to_snake("WordOrGlyph"), "word_or_glyph");
1546 assert_eq!(pascal_to_snake("AlwaysOnTop"), "always_on_top");
1547 assert_eq!(pascal_to_snake("ScaleDown"), "scale_down");
1548 }
1549
1550 #[test]
1551 fn pascal_to_snake_consecutive_upper() {
1552 assert_eq!(pascal_to_snake("URL"), "url");
1553 assert_eq!(pascal_to_snake("HTMLParser"), "html_parser");
1554 assert_eq!(
1555 pascal_to_snake("ResizingDiagonallyUp"),
1556 "resizing_diagonally_up"
1557 );
1558 }
1559
1560 #[test]
1561 fn pascal_to_snake_digits_and_existing_underscores() {
1562 assert_eq!(pascal_to_snake("GL11Version"), "gl11_version");
1563 assert_eq!(pascal_to_snake("Version2D"), "version_2d");
1564 assert_eq!(pascal_to_snake("HTTP2Connection"), "http2_connection");
1565 assert_eq!(pascal_to_snake("XML_HTTP_Request"), "xml_http_request");
1566 }
1567
1568 #[test]
1569 fn pascal_to_snake_single_char() {
1570 assert_eq!(pascal_to_snake("X"), "x");
1571 assert_eq!(pascal_to_snake("Y"), "y");
1572 }
1573
1574 #[test]
1575 fn extract_plushie_type_name_works() {
1576 let input: DeriveInput = parse_quote! {
1577 #[plushie_type(name = "direction")]
1578 enum Direction {
1579 Horizontal,
1580 Vertical,
1581 }
1582 };
1583 assert_eq!(extract_plushie_type_name(&input).unwrap(), "direction");
1584 }
1585
1586 #[test]
1587 fn rejects_missing_plushie_type() {
1588 let input: DeriveInput = parse_quote! {
1589 enum NoAttr {
1590 A,
1591 }
1592 };
1593 assert!(extract_plushie_type_name(&input).is_err());
1594 }
1595
1596 #[test]
1597 fn variant_meta_default_wire_name() {
1598 let input: DeriveInput = parse_quote! {
1599 #[plushie_type(name = "test")]
1600 enum Test {
1601 WordOrGlyph,
1602 }
1603 };
1604 if let Data::Enum(data) = &input.data {
1605 let meta = extract_variant_meta(&data.variants[0]).unwrap();
1606 assert_eq!(meta.wire_name, "word_or_glyph");
1607 assert!(meta.aliases.is_empty());
1608 }
1609 }
1610
1611 #[test]
1612 fn variant_meta_custom_wire_and_aliases() {
1613 let input: DeriveInput = parse_quote! {
1614 #[plushie_type(name = "test")]
1615 enum Test {
1616 #[plushie(wire = "table_row", aliases = ["row"])]
1617 Row,
1618 }
1619 };
1620 if let Data::Enum(data) = &input.data {
1621 let meta = extract_variant_meta(&data.variants[0]).unwrap();
1622 assert_eq!(meta.wire_name, "table_row");
1623 assert_eq!(meta.aliases, vec!["row"]);
1624 }
1625 }
1626
1627 #[test]
1628 fn derive_enum_impl_produces_output() {
1629 let input: DeriveInput = parse_quote! {
1630 #[plushie_type(name = "direction")]
1631 enum Direction {
1632 Horizontal,
1633 Vertical,
1634 Both,
1635 }
1636 };
1637 let output = derive_enum_impl(&input).unwrap();
1638 let output_str = output.to_string();
1639
1640 assert!(output_str.contains("PlushieType"));
1641 assert!(output_str.contains("wire_decode"));
1642 assert!(output_str.contains("wire_encode"));
1643 assert!(output_str.contains("\"horizontal\""));
1644 assert!(output_str.contains("\"direction\""));
1645 }
1646
1647 #[test]
1648 fn rejects_struct_for_enum_derive() {
1649 let input: DeriveInput = parse_quote! {
1650 #[plushie_type(name = "bad")]
1651 struct NotAnEnum {
1652 x: f32,
1653 }
1654 };
1655 assert!(derive_enum_impl(&input).is_err());
1656 }
1657
1658 #[test]
1659 fn rejects_tuple_variant() {
1660 let input: DeriveInput = parse_quote! {
1661 #[plushie_type(name = "bad")]
1662 enum HasData {
1663 A(i32),
1664 }
1665 };
1666 assert!(derive_enum_impl(&input).is_err());
1667 }
1668
1669 #[test]
1672 fn widget_event_unit_variant() {
1673 let input: DeriveInput = parse_quote! {
1674 enum TestEvent {
1675 Cleared,
1676 }
1677 };
1678 let output = derive_widget_event_impl(&input).unwrap();
1679 let output_str = output.to_string();
1680
1681 assert!(output_str.contains("WidgetEventEncode"));
1682 assert!(output_str.contains("to_wire"));
1683 assert!(output_str.contains("\"cleared\""));
1684 assert!(output_str.contains("PropValue :: Null"));
1685 }
1686
1687 #[test]
1688 fn widget_event_tuple_variant() {
1689 let input: DeriveInput = parse_quote! {
1690 enum TestEvent {
1691 Select(u64),
1692 HoverChanged(bool),
1693 }
1694 };
1695 let output = derive_widget_event_impl(&input).unwrap();
1696 let output_str = output.to_string();
1697
1698 assert!(output_str.contains("\"select\""));
1699 assert!(output_str.contains("\"hover_changed\""));
1700 assert!(output_str.contains("wire_encode"));
1701 }
1702
1703 #[test]
1704 fn widget_event_struct_variant() {
1705 let input: DeriveInput = parse_quote! {
1706 enum TestEvent {
1707 Change { x: f32, y: f32 },
1708 }
1709 };
1710 let output = derive_widget_event_impl(&input).unwrap();
1711 let output_str = output.to_string();
1712
1713 assert!(output_str.contains("\"change\""));
1714 assert!(output_str.contains("PropMap"));
1715 assert!(output_str.contains("\"x\""));
1716 assert!(output_str.contains("\"y\""));
1717 }
1718
1719 #[test]
1720 fn widget_event_mixed_variants() {
1721 let input: DeriveInput = parse_quote! {
1722 enum TestEvent {
1723 Select(u64),
1724 Change { x: f32, y: f32 },
1725 Cleared,
1726 }
1727 };
1728 let output = derive_widget_event_impl(&input).unwrap();
1729 let output_str = output.to_string();
1730
1731 assert!(output_str.contains("\"select\""));
1732 assert!(output_str.contains("\"change\""));
1733 assert!(output_str.contains("\"cleared\""));
1734 }
1735
1736 #[test]
1737 fn widget_event_rejects_struct() {
1738 let input: DeriveInput = parse_quote! {
1739 struct NotAnEnum {
1740 x: f32,
1741 }
1742 };
1743 assert!(derive_widget_event_impl(&input).is_err());
1744 }
1745
1746 #[test]
1747 fn widget_event_rejects_multi_field_tuple() {
1748 let input: DeriveInput = parse_quote! {
1749 enum BadEvent {
1750 Change(f32, f32),
1751 }
1752 };
1753 assert!(derive_widget_event_impl(&input).is_err());
1754 }
1755
1756 #[test]
1757 fn widget_event_specs_map_qualified_string_types() {
1758 let input: DeriveInput = parse_quote! {
1759 enum TestEvent {
1760 Owned(String),
1761 Std(std::string::String),
1762 Alloc(alloc::string::String),
1763 }
1764 };
1765 let output = derive_widget_event_impl(&input).unwrap();
1766 let output_str = output.to_string();
1767
1768 assert!(output_str.contains("\"owned\""));
1769 assert!(output_str.contains("\"std\""));
1770 assert!(output_str.contains("\"alloc\""));
1771 assert_eq!(output_str.matches("ValueType :: String").count(), 3);
1772 assert!(!output_str.contains("ValueType :: Any"));
1773 }
1774
1775 #[test]
1776 fn widget_event_specs_reject_unsupported_payload_type() {
1777 let input: DeriveInput = parse_quote! {
1778 enum BadEvent {
1779 Count(u8),
1780 }
1781 };
1782 let err = derive_widget_event_impl(&input).unwrap_err();
1783 assert!(
1784 err.to_string()
1785 .contains("unsupported WidgetEvent payload type")
1786 );
1787 }
1788
1789 #[test]
1790 fn widget_command_specs_reject_unsupported_field_type() {
1791 let input: DeriveInput = parse_quote! {
1792 enum BadCommand {
1793 Set { count: usize },
1794 }
1795 };
1796 let err = derive_widget_command_impl(&input).unwrap_err();
1797 assert!(
1798 err.to_string()
1799 .contains("unsupported WidgetCommand payload type")
1800 );
1801 }
1802
1803 #[test]
1806 fn extracts_widget_name() {
1807 let input: DeriveInput = parse_quote! {
1808 #[widget(name = "my_widget")]
1809 struct MyWidget {
1810 label: String,
1811 }
1812 };
1813 assert_eq!(extract_widget_name(&input).unwrap(), "my_widget");
1814 }
1815
1816 #[test]
1817 fn rejects_missing_widget_attr() {
1818 let input: DeriveInput = parse_quote! {
1819 struct NoAttr {
1820 label: String,
1821 }
1822 };
1823 assert!(extract_widget_name(&input).is_err());
1824 }
1825
1826 #[test]
1827 fn rejects_widget_attr_without_name() {
1828 let input: DeriveInput = parse_quote! {
1829 #[widget()]
1830 struct EmptyAttr {
1831 label: String,
1832 }
1833 };
1834 assert!(extract_widget_name(&input).is_err());
1835 }
1836
1837 #[test]
1838 fn derive_widget_impl_produces_output() {
1839 let input: DeriveInput = parse_quote! {
1840 #[widget(name = "gauge")]
1841 struct Gauge {
1842 value: f32,
1844 label: String,
1845 enabled: bool,
1846 }
1847 };
1848 let output = derive_widget_impl(&input).unwrap();
1849 let output_str = output.to_string();
1850
1851 assert!(output_str.contains("GaugeProps"));
1853 assert!(output_str.contains("from_node"));
1855 assert!(output_str.contains("FromNode"));
1857 assert!(output_str.contains("\"gauge\""));
1859 assert!(output_str.contains("PlushieType"));
1861
1862 assert!(output_str.contains("GaugeBuilder"));
1864 assert!(output_str.contains("WidgetBuilder"));
1866 assert!(output_str.contains("fn value"));
1868 assert!(output_str.contains("fn label"));
1869 assert!(output_str.contains("fn enabled"));
1870 assert!(output_str.contains("fn builder"));
1872 assert!(output_str.contains("fn prop"));
1874 }
1875
1876 #[test]
1877 fn derive_widget_builder_uses_wire_encode() {
1878 let input: DeriveInput = parse_quote! {
1879 #[widget(name = "slider")]
1880 struct Slider {
1881 min: f32,
1882 max: f32,
1883 }
1884 };
1885 let output = derive_widget_impl(&input).unwrap();
1886 let output_str = output.to_string();
1887
1888 assert!(output_str.contains("wire_encode"));
1890 assert!(output_str.contains("\"min\""));
1892 assert!(output_str.contains("\"max\""));
1893 }
1894
1895 #[test]
1896 fn derive_widget_builder_new_uses_widget_name() {
1897 let input: DeriveInput = parse_quote! {
1898 #[widget(name = "progress_bar")]
1899 struct ProgressBar {
1900 value: f32,
1901 }
1902 };
1903 let output = derive_widget_impl(&input).unwrap();
1904 let output_str = output.to_string();
1905
1906 assert!(output_str.contains("\"progress_bar\""));
1908 assert!(output_str.contains("ProgressBarBuilder"));
1909 }
1910
1911 #[test]
1912 fn rejects_enum_for_widget() {
1913 let input: DeriveInput = parse_quote! {
1914 #[widget(name = "bad")]
1915 enum NotAStruct {
1916 A,
1917 B,
1918 }
1919 };
1920 assert!(derive_widget_impl(&input).is_err());
1921 }
1922
1923 #[test]
1924 fn rejects_tuple_struct_for_widget() {
1925 let input: DeriveInput = parse_quote! {
1926 #[widget(name = "bad")]
1927 struct TupleStruct(String, f32);
1928 };
1929 assert!(derive_widget_impl(&input).is_err());
1930 }
1931
1932 #[test]
1935 fn widget_macro_expands() {
1936 let input: proc_macro2::TokenStream = quote! {
1937 #[widget(type_name = "my_gauge", crate = "my-gauge")]
1938 pub struct Gauge {
1939 pub value: f32,
1940 pub max: f32,
1941 }
1942 };
1943 let output = widget_impl(input).expect("widget! should expand");
1944 let s = output.to_string();
1945
1946 assert!(s.contains("pub struct Gauge"));
1948 assert!(s.contains("pub id :"));
1949 assert!(s.contains("fn value"));
1951 assert!(s.contains("fn max"));
1952 assert!(s.contains("TreeNode"));
1954 assert!(s.contains("\"my_gauge\""));
1955 assert!(s.contains("PLUSHIE_WIDGET_METADATA"));
1957 assert!(s.contains("\\\"type_name\\\""));
1958 assert!(s.contains("\\\"my_gauge\\\""));
1959 assert!(s.contains("\\\"crate\\\""));
1960 assert!(!s.contains("\\\"constructor\\\""));
1963 }
1964
1965 #[test]
1966 fn widget_macro_metadata_is_valid_json() {
1967 let type_name = "my_gauge";
1971 let crate_name_json = format!(",\"crate\":\"{}\"", escape_json("my-gauge"));
1972 let metadata_str = format!(
1973 "{{\"type_name\":\"{}\",\"struct\":\"{}\"{}}}",
1974 escape_json(type_name),
1975 "Gauge",
1976 crate_name_json,
1977 );
1978
1979 let value: serde_json::Value =
1980 serde_json::from_str(&metadata_str).expect("metadata parses as JSON");
1981 assert_eq!(value["type_name"], "my_gauge");
1982 assert_eq!(value["crate"], "my-gauge");
1983 assert_eq!(value["struct"], "Gauge");
1984 assert!(value.get("constructor").is_none());
1985 }
1986
1987 #[test]
1988 fn widget_macro_metadata_without_optional_fields() {
1989 let type_name = "bare_widget";
1991 let metadata_str = format!(
1992 "{{\"type_name\":\"{}\",\"struct\":\"{}\"{}}}",
1993 escape_json(type_name),
1994 "Bare",
1995 String::new(),
1996 );
1997 let value: serde_json::Value =
1998 serde_json::from_str(&metadata_str).expect("minimal metadata parses as JSON");
1999 assert_eq!(value["type_name"], "bare_widget");
2000 assert_eq!(value["struct"], "Bare");
2001 assert!(value.get("crate").is_none());
2002 assert!(value.get("constructor").is_none());
2003 }
2004
2005 #[test]
2006 fn widget_macro_rejects_constructor_attribute() {
2007 let input: proc_macro2::TokenStream = quote! {
2008 #[widget(type_name = "my_gauge", constructor = "x::y::new()")]
2009 pub struct Gauge {
2010 pub value: f32,
2011 }
2012 };
2013 let err = widget_impl(input).expect_err("constructor attribute should be rejected");
2014 assert!(
2015 err.to_string().contains("Cargo.toml"),
2016 "error should point at Cargo.toml: {err}",
2017 );
2018 }
2019
2020 #[test]
2021 fn escape_json_handles_specials() {
2022 assert_eq!(escape_json("a\"b"), "a\\\"b");
2023 assert_eq!(escape_json("a\\b"), "a\\\\b");
2024 assert_eq!(escape_json("a\nb"), "a\\nb");
2025 assert_eq!(escape_json("a\tb"), "a\\tb");
2026 assert_eq!(escape_json("normal_text"), "normal_text");
2027 }
2028
2029 #[test]
2030 fn widget_macro_requires_type_name() {
2031 let input: proc_macro2::TokenStream = quote! {
2032 pub struct NoAttr {
2033 pub value: f32,
2034 }
2035 };
2036 assert!(widget_impl(input).is_err());
2037 }
2038
2039 #[test]
2040 fn widget_macro_with_events_block() {
2041 let input: proc_macro2::TokenStream = quote! {
2042 #[widget(type_name = "my_gauge")]
2043 pub struct Gauge {
2044 pub value: f32,
2045 }
2046
2047 events {
2048 ValueChanged(f32),
2049 Cleared,
2050 }
2051 };
2052 let output = widget_impl(input).unwrap().to_string();
2053 assert!(output.contains("GaugeEvent"));
2054 assert!(output.contains("WidgetEvent"));
2055 assert!(output.contains("ValueChanged"));
2056 assert!(output.contains("Cleared"));
2057 }
2058}