linera_views_derive/
lib.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! The procedural macros for the crate `linera-views`.
5
6use proc_macro::TokenStream;
7use proc_macro2::{Span, TokenStream as TokenStream2};
8use quote::{format_ident, quote};
9use syn::{parse_macro_input, parse_quote, Error, ItemStruct, Type};
10
11#[derive(Debug, deluxe::ParseAttributes)]
12#[deluxe(attributes(view))]
13struct StructAttrs {
14    context: Option<syn::Type>,
15}
16
17struct Constraints<'a> {
18    input_constraints: Vec<&'a syn::WherePredicate>,
19    impl_generics: syn::ImplGenerics<'a>,
20    type_generics: syn::TypeGenerics<'a>,
21}
22
23impl<'a> Constraints<'a> {
24    fn get(item: &'a syn::ItemStruct) -> Self {
25        let (impl_generics, type_generics, maybe_where_clause) = item.generics.split_for_impl();
26        let input_constraints = maybe_where_clause
27            .map(|w| w.predicates.iter())
28            .into_iter()
29            .flatten()
30            .collect();
31
32        Self {
33            input_constraints,
34            impl_generics,
35            type_generics,
36        }
37    }
38}
39
40fn get_extended_entry(e: Type) -> Result<TokenStream2, Error> {
41    let syn::Type::Path(typepath) = &e else {
42        return Err(Error::new_spanned(e, "Expected a path type"));
43    };
44    let Some(path_segment) = typepath.path.segments.first() else {
45        return Err(Error::new_spanned(&typepath.path, "Path has no segments"));
46    };
47    let ident = &path_segment.ident;
48    let arguments = &path_segment.arguments;
49    Ok(quote! { #ident :: #arguments })
50}
51
52fn generate_view_code(input: ItemStruct, root: bool) -> Result<TokenStream2, Error> {
53    // Validate that all fields are named
54    for field in &input.fields {
55        if field.ident.is_none() {
56            return Err(Error::new_spanned(field, "All fields must be named."));
57        }
58    }
59
60    let Constraints {
61        input_constraints,
62        impl_generics,
63        type_generics,
64    } = Constraints::get(&input);
65
66    let attrs: StructAttrs = deluxe::parse_attributes(&input)
67        .map_err(|e| Error::new_spanned(&input, format!("Failed to parse attributes: {e}")))?;
68    let context = attrs.context.or_else(|| {
69        input.generics.type_params().next().map(|param| {
70            let ident = &param.ident;
71            parse_quote! { #ident }
72        })
73    }).ok_or_else(|| {
74        Error::new_spanned(
75            &input,
76            "Missing context: either add a generic type parameter or specify the context with #[view(context = YourContextType)]"
77        )
78    })?;
79
80    let struct_name = &input.ident;
81    let field_types: Vec<_> = input.fields.iter().map(|field| &field.ty).collect();
82
83    let mut name_quotes = Vec::new();
84    let mut rollback_quotes = Vec::new();
85    let mut pre_save_quotes = Vec::new();
86    let mut delete_view_quotes = Vec::new();
87    let mut clear_quotes = Vec::new();
88    let mut has_pending_changes_quotes = Vec::new();
89    let mut num_init_keys_quotes = Vec::new();
90    let mut pre_load_keys_quotes = Vec::new();
91    let mut post_load_keys_quotes = Vec::new();
92    let num_fields = input.fields.len();
93    for (idx, e) in input.fields.iter().enumerate() {
94        let name = e.ident.clone().unwrap();
95        let delete_view_ident = format_ident!("deleted{}", idx);
96        let g = get_extended_entry(e.ty.clone())?;
97        name_quotes.push(quote! { #name });
98        rollback_quotes.push(quote! { self.#name.rollback(); });
99        pre_save_quotes.push(quote! { let #delete_view_ident = self.#name.pre_save(batch)?; });
100        delete_view_quotes.push(quote! { #delete_view_ident });
101        clear_quotes.push(quote! { self.#name.clear(); });
102        has_pending_changes_quotes.push(quote! {
103            if self.#name.has_pending_changes().await {
104                return true;
105            }
106        });
107        num_init_keys_quotes.push(quote! { #g :: NUM_INIT_KEYS });
108
109        let derive_key_logic = if num_fields < 256 {
110            let idx_u8 = idx as u8;
111            quote! {
112                let __linera_reserved_index = #idx_u8;
113                let __linera_reserved_base_key = context.base_key().derive_tag_key(linera_views::views::MIN_VIEW_TAG, &__linera_reserved_index)?;
114            }
115        } else {
116            assert!(num_fields < 65536);
117            let idx_u16 = idx as u16;
118            quote! {
119                let __linera_reserved_index = #idx_u16;
120                let __linera_reserved_base_key = context.base_key().derive_tag_key(linera_views::views::MIN_VIEW_TAG, &__linera_reserved_index)?;
121            }
122        };
123
124        pre_load_keys_quotes.push(quote! {
125            #derive_key_logic
126            keys.extend(#g :: pre_load(&context.clone_with_base_key(__linera_reserved_base_key))?);
127        });
128        post_load_keys_quotes.push(quote! {
129            #derive_key_logic
130            let __linera_reserved_pos_next = __linera_reserved_pos + #g :: NUM_INIT_KEYS;
131            let #name = #g :: post_load(context.clone_with_base_key(__linera_reserved_base_key), &values[__linera_reserved_pos..__linera_reserved_pos_next])?;
132            __linera_reserved_pos = __linera_reserved_pos_next;
133        });
134    }
135
136    // derive_key_logic above adds one byte to the key as a tag, and then either one or two more
137    // bytes for field indices, depending on how many fields there are. Thus, we need to trim 2
138    // bytes if there are less than 256 child fields (then the field index fits within one byte),
139    // or 3 bytes if there are more.
140    let trim_key_logic = if num_fields < 256 {
141        quote! {
142            let __bytes_to_trim = 2;
143        }
144    } else {
145        quote! {
146            let __bytes_to_trim = 3;
147        }
148    };
149
150    let first_name_quote = name_quotes.first().ok_or(Error::new_spanned(
151        &input,
152        "Struct must have at least one field",
153    ))?;
154
155    let load_metrics = if root && cfg!(feature = "metrics") {
156        quote! {
157            #[cfg(not(target_arch = "wasm32"))]
158            linera_views::metrics::increment_counter(
159                &linera_views::metrics::LOAD_VIEW_COUNTER,
160                stringify!(#struct_name),
161                &context.base_key().bytes,
162            );
163            #[cfg(not(target_arch = "wasm32"))]
164            use linera_views::metrics::prometheus_util::MeasureLatency as _;
165            let _latency = linera_views::metrics::LOAD_VIEW_LATENCY.measure_latency();
166        }
167    } else {
168        quote! {}
169    };
170
171    Ok(quote! {
172        impl #impl_generics linera_views::views::View for #struct_name #type_generics
173        where
174            #context: linera_views::context::Context,
175            #(#input_constraints,)*
176            #(#field_types: linera_views::views::View<Context = #context>,)*
177        {
178            const NUM_INIT_KEYS: usize = #(<#field_types as linera_views::views::View>::NUM_INIT_KEYS)+*;
179
180            type Context = #context;
181
182            fn context(&self) -> #context {
183                use linera_views::{context::Context as _};
184                #trim_key_logic
185                let context = self.#first_name_quote.context();
186                context.clone_with_trimmed_key(__bytes_to_trim)
187            }
188
189            fn pre_load(context: &#context) -> Result<Vec<Vec<u8>>, linera_views::ViewError> {
190                use linera_views::context::Context as _;
191                let mut keys = Vec::new();
192                #(#pre_load_keys_quotes)*
193                Ok(keys)
194            }
195
196            fn post_load(context: #context, values: &[Option<Vec<u8>>]) -> Result<Self, linera_views::ViewError> {
197                use linera_views::context::Context as _;
198                let mut __linera_reserved_pos = 0;
199                #(#post_load_keys_quotes)*
200                Ok(Self {#(#name_quotes),*})
201            }
202
203            async fn load(context: #context) -> Result<Self, linera_views::ViewError> {
204                use linera_views::{context::Context as _, store::ReadableKeyValueStore as _};
205                #load_metrics
206                if Self::NUM_INIT_KEYS == 0 {
207                    Self::post_load(context, &[])
208                } else {
209                    let keys = Self::pre_load(&context)?;
210                    let values = context.store().read_multi_values_bytes(&keys).await?;
211                    Self::post_load(context, &values)
212                }
213            }
214
215
216            fn rollback(&mut self) {
217                #(#rollback_quotes)*
218            }
219
220            async fn has_pending_changes(&self) -> bool {
221                #(#has_pending_changes_quotes)*
222                false
223            }
224
225            fn pre_save(&self, batch: &mut linera_views::batch::Batch) -> Result<bool, linera_views::ViewError> {
226                #(#pre_save_quotes)*
227                Ok( #(#delete_view_quotes)&&* )
228            }
229
230            fn post_save(&mut self) {
231                #(self.#name_quotes.post_save();)*
232            }
233
234            fn clear(&mut self) {
235                #(#clear_quotes)*
236            }
237        }
238    })
239}
240
241fn generate_root_view_code(input: ItemStruct) -> TokenStream2 {
242    let Constraints {
243        input_constraints,
244        impl_generics,
245        type_generics,
246    } = Constraints::get(&input);
247    let struct_name = &input.ident;
248
249    let increment_counter = if cfg!(feature = "metrics") {
250        quote! {
251            #[cfg(not(target_arch = "wasm32"))]
252            linera_views::metrics::increment_counter(
253                &linera_views::metrics::SAVE_VIEW_COUNTER,
254                stringify!(#struct_name),
255                &self.context().base_key().bytes,
256            );
257        }
258    } else {
259        quote! {}
260    };
261
262    quote! {
263        impl #impl_generics linera_views::views::RootView for #struct_name #type_generics
264        where
265            #(#input_constraints,)*
266            Self: linera_views::views::View,
267        {
268            async fn save(&mut self) -> Result<(), linera_views::ViewError> {
269                use linera_views::{context::Context as _, batch::Batch, store::WritableKeyValueStore as _, views::View as _};
270                #increment_counter
271                let mut batch = Batch::new();
272                self.pre_save(&mut batch)?;
273                if !batch.is_empty() {
274                    self.context().store().write_batch(batch).await?;
275                }
276                self.post_save();
277                Ok(())
278            }
279        }
280    }
281}
282
283fn generate_hash_view_code(input: ItemStruct) -> Result<TokenStream2, Error> {
284    // Validate that all fields are named
285    for field in &input.fields {
286        if field.ident.is_none() {
287            return Err(Error::new_spanned(field, "All fields must be named."));
288        }
289    }
290
291    let Constraints {
292        input_constraints,
293        impl_generics,
294        type_generics,
295    } = Constraints::get(&input);
296    let struct_name = &input.ident;
297
298    let field_types = input.fields.iter().map(|field| &field.ty);
299    let mut field_hashes_mut = Vec::new();
300    let mut field_hashes = Vec::new();
301    for e in &input.fields {
302        let name = e.ident.as_ref().unwrap();
303        field_hashes_mut.push(quote! { hasher.write_all(self.#name.hash_mut().await?.as_ref())?; });
304        field_hashes.push(quote! { hasher.write_all(self.#name.hash().await?.as_ref())?; });
305    }
306
307    Ok(quote! {
308        impl #impl_generics linera_views::views::HashableView for #struct_name #type_generics
309        where
310            #(#field_types: linera_views::views::HashableView,)*
311            #(#input_constraints,)*
312            Self: linera_views::views::View,
313        {
314            type Hasher = linera_views::sha3::Sha3_256;
315
316            async fn hash_mut(&mut self) -> Result<<Self::Hasher as linera_views::views::Hasher>::Output, linera_views::ViewError> {
317                use linera_views::views::Hasher as _;
318                use std::io::Write as _;
319                let mut hasher = Self::Hasher::default();
320                #(#field_hashes_mut)*
321                Ok(hasher.finalize())
322            }
323
324            async fn hash(&self) -> Result<<Self::Hasher as linera_views::views::Hasher>::Output, linera_views::ViewError> {
325                use linera_views::views::Hasher as _;
326                use std::io::Write as _;
327                let mut hasher = Self::Hasher::default();
328                #(#field_hashes)*
329                Ok(hasher.finalize())
330            }
331        }
332    })
333}
334
335fn generate_crypto_hash_code(input: ItemStruct) -> TokenStream2 {
336    let Constraints {
337        input_constraints,
338        impl_generics,
339        type_generics,
340    } = Constraints::get(&input);
341    let field_types = input.fields.iter().map(|field| &field.ty);
342    let struct_name = &input.ident;
343    let hash_type = syn::Ident::new(&format!("{struct_name}Hash"), Span::call_site());
344    quote! {
345        impl #impl_generics linera_views::views::CryptoHashView
346        for #struct_name #type_generics
347        where
348            #(#field_types: linera_views::views::HashableView,)*
349            #(#input_constraints,)*
350            Self: linera_views::views::View,
351        {
352            async fn crypto_hash(&self) -> Result<linera_base::crypto::CryptoHash, linera_views::ViewError> {
353                use linera_base::crypto::{BcsHashable, CryptoHash};
354                use linera_views::{
355                    generic_array::GenericArray,
356                    sha3::{digest::OutputSizeUser, Sha3_256},
357                    views::HashableView as _,
358                };
359                #[derive(serde::Serialize, serde::Deserialize)]
360                struct #hash_type(GenericArray<u8, <Sha3_256 as OutputSizeUser>::OutputSize>);
361                impl<'de> BcsHashable<'de> for #hash_type {}
362                let hash = self.hash().await?;
363                Ok(CryptoHash::new(&#hash_type(hash)))
364            }
365
366            async fn crypto_hash_mut(&mut self) -> Result<linera_base::crypto::CryptoHash, linera_views::ViewError> {
367                use linera_base::crypto::{BcsHashable, CryptoHash};
368                use linera_views::{
369                    generic_array::GenericArray,
370                    sha3::{digest::OutputSizeUser, Sha3_256},
371                    views::HashableView as _,
372                };
373                #[derive(serde::Serialize, serde::Deserialize)]
374                struct #hash_type(GenericArray<u8, <Sha3_256 as OutputSizeUser>::OutputSize>);
375                impl<'de> BcsHashable<'de> for #hash_type {}
376                let hash = self.hash_mut().await?;
377                Ok(CryptoHash::new(&#hash_type(hash)))
378            }
379        }
380    }
381}
382
383fn generate_clonable_view_code(input: ItemStruct) -> Result<TokenStream2, Error> {
384    // Validate that all fields are named
385    for field in &input.fields {
386        if field.ident.is_none() {
387            return Err(Error::new_spanned(field, "All fields must be named."));
388        }
389    }
390
391    let Constraints {
392        input_constraints,
393        impl_generics,
394        type_generics,
395    } = Constraints::get(&input);
396    let struct_name = &input.ident;
397
398    let mut clone_constraints = vec![];
399    let mut clone_fields = vec![];
400
401    for field in &input.fields {
402        let name = &field.ident;
403        let ty = &field.ty;
404        clone_constraints.push(quote! { #ty: ClonableView });
405        clone_fields.push(quote! { #name: self.#name.clone_unchecked()? });
406    }
407
408    Ok(quote! {
409        impl #impl_generics linera_views::views::ClonableView for #struct_name #type_generics
410        where
411            #(#input_constraints,)*
412            #(#clone_constraints,)*
413            Self: linera_views::views::View,
414        {
415            fn clone_unchecked(&mut self) -> Result<Self, linera_views::ViewError> {
416                Ok(Self {
417                    #(#clone_fields,)*
418                })
419            }
420        }
421    })
422}
423
424fn to_token_stream(input: Result<TokenStream2, Error>) -> TokenStream {
425    match input {
426        Ok(tokens) => tokens.into(),
427        Err(err) => err.to_compile_error().into(),
428    }
429}
430
431#[proc_macro_derive(View, attributes(view))]
432pub fn derive_view(input: TokenStream) -> TokenStream {
433    let input = parse_macro_input!(input as ItemStruct);
434    let input = generate_view_code(input, false);
435    to_token_stream(input)
436}
437
438fn derive_hash_view_token_stream2(input: ItemStruct) -> Result<TokenStream2, Error> {
439    let mut stream = generate_view_code(input.clone(), false)?;
440    stream.extend(generate_hash_view_code(input)?);
441    Ok(stream)
442}
443
444#[proc_macro_derive(HashableView, attributes(view))]
445pub fn derive_hash_view(input: TokenStream) -> TokenStream {
446    let input = parse_macro_input!(input as ItemStruct);
447
448    let stream = derive_hash_view_token_stream2(input);
449    to_token_stream(stream)
450}
451
452fn derive_root_view_token_stream2(input: ItemStruct) -> Result<TokenStream2, Error> {
453    let mut stream = generate_view_code(input.clone(), true)?;
454    stream.extend(generate_root_view_code(input));
455    Ok(stream)
456}
457
458#[proc_macro_derive(RootView, attributes(view))]
459pub fn derive_root_view(input: TokenStream) -> TokenStream {
460    let input = parse_macro_input!(input as ItemStruct);
461
462    let stream = derive_root_view_token_stream2(input);
463    to_token_stream(stream)
464}
465
466fn derive_crypto_hash_view_token_stream2(input: ItemStruct) -> Result<TokenStream2, Error> {
467    let mut stream = generate_view_code(input.clone(), false)?;
468    stream.extend(generate_hash_view_code(input.clone())?);
469    stream.extend(generate_crypto_hash_code(input));
470    Ok(stream)
471}
472
473#[proc_macro_derive(CryptoHashView, attributes(view))]
474pub fn derive_crypto_hash_view(input: TokenStream) -> TokenStream {
475    let input = parse_macro_input!(input as ItemStruct);
476
477    let stream = derive_crypto_hash_view_token_stream2(input);
478    to_token_stream(stream)
479}
480
481fn derive_crypto_hash_root_view_token_stream2(input: ItemStruct) -> Result<TokenStream2, Error> {
482    let mut stream = generate_view_code(input.clone(), true)?;
483    stream.extend(generate_root_view_code(input.clone()));
484    stream.extend(generate_hash_view_code(input.clone())?);
485    stream.extend(generate_crypto_hash_code(input));
486    Ok(stream)
487}
488
489#[proc_macro_derive(CryptoHashRootView, attributes(view))]
490pub fn derive_crypto_hash_root_view(input: TokenStream) -> TokenStream {
491    let input = parse_macro_input!(input as ItemStruct);
492
493    let stream = derive_crypto_hash_root_view_token_stream2(input);
494    to_token_stream(stream)
495}
496
497#[cfg(test)]
498fn derive_hashable_root_view_token_stream2(input: ItemStruct) -> Result<TokenStream2, Error> {
499    let mut stream = generate_view_code(input.clone(), true)?;
500    stream.extend(generate_root_view_code(input.clone()));
501    stream.extend(generate_hash_view_code(input)?);
502    Ok(stream)
503}
504
505#[proc_macro_derive(HashableRootView, attributes(view))]
506#[cfg(test)]
507pub fn derive_hashable_root_view(input: TokenStream) -> TokenStream {
508    let input = parse_macro_input!(input as ItemStruct);
509
510    let stream = derive_hashable_root_view_token_stream2(input);
511    to_token_stream(stream)
512}
513
514#[proc_macro_derive(ClonableView, attributes(view))]
515pub fn derive_clonable_view(input: TokenStream) -> TokenStream {
516    let input = parse_macro_input!(input as ItemStruct);
517    match generate_clonable_view_code(input) {
518        Ok(tokens) => tokens.into(),
519        Err(err) => err.to_compile_error().into(),
520    }
521}
522
523#[cfg(test)]
524pub mod tests {
525
526    use quote::quote;
527    use syn::{parse_quote, AngleBracketedGenericArguments};
528
529    use crate::*;
530
531    fn pretty(tokens: TokenStream2) -> String {
532        prettyplease::unparse(
533            &syn::parse2::<syn::File>(tokens).expect("failed to parse test output"),
534        )
535    }
536
537    #[test]
538    fn test_generate_view_code() {
539        for context in SpecificContextInfo::test_cases() {
540            let input = context.test_view_input();
541            insta::assert_snapshot!(
542                format!(
543                    "test_generate_view_code{}_{}",
544                    if cfg!(feature = "metrics") {
545                        "_metrics"
546                    } else {
547                        ""
548                    },
549                    context.name,
550                ),
551                pretty(generate_view_code(input, true).unwrap())
552            );
553        }
554    }
555
556    #[test]
557    fn test_generate_hash_view_code() {
558        for context in SpecificContextInfo::test_cases() {
559            let input = context.test_view_input();
560            insta::assert_snapshot!(
561                format!("test_generate_hash_view_code_{}", context.name),
562                pretty(generate_hash_view_code(input).unwrap())
563            );
564        }
565    }
566
567    #[test]
568    fn test_generate_root_view_code() {
569        for context in SpecificContextInfo::test_cases() {
570            let input = context.test_view_input();
571            insta::assert_snapshot!(
572                format!(
573                    "test_generate_root_view_code{}_{}",
574                    if cfg!(feature = "metrics") {
575                        "_metrics"
576                    } else {
577                        ""
578                    },
579                    context.name,
580                ),
581                pretty(generate_root_view_code(input))
582            );
583        }
584    }
585
586    #[test]
587    fn test_generate_crypto_hash_code() {
588        for context in SpecificContextInfo::test_cases() {
589            let input = context.test_view_input();
590            insta::assert_snapshot!(pretty(generate_crypto_hash_code(input)));
591        }
592    }
593
594    #[test]
595    fn test_generate_clonable_view_code() {
596        for context in SpecificContextInfo::test_cases() {
597            let input = context.test_view_input();
598            insta::assert_snapshot!(pretty(generate_clonable_view_code(input).unwrap()));
599        }
600    }
601
602    #[derive(Clone)]
603    pub struct SpecificContextInfo {
604        name: String,
605        attribute: Option<TokenStream2>,
606        context: Type,
607        generics: AngleBracketedGenericArguments,
608        where_clause: Option<TokenStream2>,
609    }
610
611    impl SpecificContextInfo {
612        pub fn empty() -> Self {
613            SpecificContextInfo {
614                name: "C".to_string(),
615                attribute: None,
616                context: syn::parse_quote! { C },
617                generics: syn::parse_quote! { <C> },
618                where_clause: None,
619            }
620        }
621
622        pub fn new(context: syn::Type) -> Self {
623            let name = quote! { #context };
624            SpecificContextInfo {
625                name: format!("{name}")
626                    .replace(' ', "")
627                    .replace([':', '<', '>'], "_"),
628                attribute: Some(quote! { #[view(context = #context)] }),
629                context,
630                generics: parse_quote! { <> },
631                where_clause: None,
632            }
633        }
634
635        /// Sets the `where_clause` to a dummy value for test cases with a where clause.
636        ///
637        /// Also adds a `MyParam` generic type parameter to the `generics` field, which is the type
638        /// constrained by the dummy predicate in the `where_clause`.
639        pub fn with_dummy_where_clause(mut self) -> Self {
640            self.generics.args.push(parse_quote! { MyParam });
641            self.where_clause = Some(quote! {
642                where MyParam: Send + Sync + 'static,
643            });
644            self.name.push_str("_with_where");
645
646            self
647        }
648
649        pub fn test_cases() -> impl Iterator<Item = Self> {
650            Some(Self::empty())
651                .into_iter()
652                .chain(
653                    [
654                        syn::parse_quote! { CustomContext },
655                        syn::parse_quote! { custom::path::to::ContextType },
656                        syn::parse_quote! { custom::GenericContext<T> },
657                    ]
658                    .into_iter()
659                    .map(Self::new),
660                )
661                .flat_map(|case| [case.clone(), case.with_dummy_where_clause()])
662        }
663
664        pub fn test_view_input(&self) -> ItemStruct {
665            let SpecificContextInfo {
666                attribute,
667                context,
668                generics,
669                where_clause,
670                ..
671            } = self;
672
673            parse_quote! {
674                #attribute
675                struct TestView #generics
676                #where_clause
677                {
678                    register: RegisterView<#context, usize>,
679                    collection: CollectionView<#context, usize, RegisterView<#context, usize>>,
680                }
681            }
682        }
683    }
684
685    // Failure scenario tests
686    #[test]
687    fn test_tuple_struct_failure() {
688        let input: ItemStruct = parse_quote! {
689            struct TestView<C>(RegisterView<C, u64>);
690        };
691        let result = generate_view_code(input, false);
692        assert!(result.is_err());
693        let error_msg = result.unwrap_err().to_string();
694        assert!(error_msg.contains("All fields must be named"));
695    }
696
697    #[test]
698    fn test_empty_struct_failure() {
699        let input: ItemStruct = parse_quote! {
700            struct TestView<C> {}
701        };
702        let result = generate_view_code(input, false);
703        assert!(result.is_err());
704        let error_msg = result.unwrap_err().to_string();
705        assert!(error_msg.contains("Struct must have at least one field"));
706    }
707
708    #[test]
709    fn test_missing_context_no_generics_failure() {
710        let input: ItemStruct = parse_quote! {
711            struct TestView {
712                register: RegisterView<CustomContext, u64>,
713            }
714        };
715        let result = generate_view_code(input, false);
716        assert!(result.is_err());
717        let error_msg = result.unwrap_err().to_string();
718        assert!(error_msg.contains("Missing context"));
719    }
720
721    #[test]
722    fn test_missing_context_empty_generics_failure() {
723        let input: ItemStruct = parse_quote! {
724            struct TestView<> {
725                register: RegisterView<CustomContext, u64>,
726            }
727        };
728        let result = generate_view_code(input, false);
729        assert!(result.is_err());
730        let error_msg = result.unwrap_err().to_string();
731        assert!(error_msg.contains("Missing context"));
732    }
733
734    #[test]
735    fn test_non_path_type_failure() {
736        let input: ItemStruct = parse_quote! {
737            struct TestView<C> {
738                field: fn() -> i32,
739            }
740        };
741        let result = generate_view_code(input, false);
742        assert!(result.is_err());
743        let error_msg = result.unwrap_err().to_string();
744        assert!(error_msg.contains("Expected a path type"));
745    }
746
747    #[test]
748    fn test_unnamed_field_in_hash_view_failure() {
749        let input: ItemStruct = parse_quote! {
750            struct TestView<C>(RegisterView<C, u64>);
751        };
752        let result = generate_hash_view_code(input);
753        assert!(result.is_err());
754        let error_msg = result.unwrap_err().to_string();
755        assert!(error_msg.contains("All fields must be named"));
756    }
757
758    #[test]
759    fn test_unnamed_field_in_clonable_view_failure() {
760        let input: ItemStruct = parse_quote! {
761            struct TestView<C>(RegisterView<C, u64>);
762        };
763        let result = generate_clonable_view_code(input);
764        assert!(result.is_err());
765        let error_msg = result.unwrap_err().to_string();
766        assert!(error_msg.contains("All fields must be named"));
767    }
768
769    #[test]
770    fn test_array_type_failure() {
771        let input: ItemStruct = parse_quote! {
772            struct TestView<C> {
773                field: [u8; 32],
774            }
775        };
776        let result = generate_view_code(input, false);
777        assert!(result.is_err());
778        let error_msg = result.unwrap_err().to_string();
779        assert!(error_msg.contains("Expected a path type"));
780    }
781
782    #[test]
783    fn test_reference_type_failure() {
784        let input: ItemStruct = parse_quote! {
785            struct TestView<C> {
786                field: &'static str,
787            }
788        };
789        let result = generate_view_code(input, false);
790        assert!(result.is_err());
791        let error_msg = result.unwrap_err().to_string();
792        assert!(error_msg.contains("Expected a path type"));
793    }
794
795    #[test]
796    fn test_pointer_type_failure() {
797        let input: ItemStruct = parse_quote! {
798            struct TestView<C> {
799                field: *const i32,
800            }
801        };
802        let result = generate_view_code(input, false);
803        assert!(result.is_err());
804        let error_msg = result.unwrap_err().to_string();
805        assert!(error_msg.contains("Expected a path type"));
806    }
807
808    #[test]
809    fn test_generate_root_view_code_with_empty_struct() {
810        let input: ItemStruct = parse_quote! {
811            struct TestView<C> {}
812        };
813        // Root view generation depends on view generation, so this should fail at the view level
814        let result = generate_view_code(input.clone(), true);
815        assert!(result.is_err());
816        let error_msg = result.unwrap_err().to_string();
817        assert!(error_msg.contains("Struct must have at least one field"));
818    }
819
820    #[test]
821    fn test_generate_functions_behavior_differences() {
822        // Some generation functions validate field types while others don't
823        let input: ItemStruct = parse_quote! {
824            struct TestView<C> {
825                field: fn() -> i32,
826            }
827        };
828
829        // View code generation validates field types and should fail
830        let view_result = generate_view_code(input.clone(), false);
831        assert!(view_result.is_err());
832        let error_msg = view_result.unwrap_err().to_string();
833        assert!(error_msg.contains("Expected a path type"));
834
835        // Hash view generation doesn't validate field types in the same way
836        let hash_result = generate_hash_view_code(input.clone());
837        assert!(hash_result.is_ok());
838
839        // Crypto hash code generation also succeeds
840        let _result = generate_crypto_hash_code(input);
841    }
842
843    #[test]
844    fn test_crypto_hash_code_generation_failure() {
845        // Crypto hash code generation should succeed as it doesn't validate field types directly
846        let input: ItemStruct = parse_quote! {
847            struct TestView<C> {
848                register: RegisterView<C, usize>,
849            }
850        };
851        let _result = generate_crypto_hash_code(input);
852    }
853}