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 metrics_code = 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    let write_batch_with_metrics = if cfg!(feature = "metrics") {
263        quote! {
264            if !batch.is_empty() {
265                #[cfg(not(target_arch = "wasm32"))]
266                let start = std::time::Instant::now();
267                self.context().store().write_batch(batch).await?;
268                #[cfg(not(target_arch = "wasm32"))]
269                {
270                    let latency_ms = start.elapsed().as_secs_f64() * 1000.0;
271                    linera_views::metrics::SAVE_VIEW_LATENCY
272                        .with_label_values(&[stringify!(#struct_name)])
273                        .observe(latency_ms);
274                }
275            }
276        }
277    } else {
278        quote! {
279            if !batch.is_empty() {
280                self.context().store().write_batch(batch).await?;
281            }
282        }
283    };
284
285    quote! {
286        impl #impl_generics linera_views::views::RootView for #struct_name #type_generics
287        where
288            #(#input_constraints,)*
289            Self: linera_views::views::View,
290        {
291            async fn save(&mut self) -> Result<(), linera_views::ViewError> {
292                use linera_views::{context::Context as _, batch::Batch, store::WritableKeyValueStore as _, views::View as _};
293                #metrics_code
294                let mut batch = Batch::new();
295                self.pre_save(&mut batch)?;
296                #write_batch_with_metrics
297                self.post_save();
298                Ok(())
299            }
300        }
301    }
302}
303
304fn generate_hash_view_code(input: ItemStruct) -> Result<TokenStream2, Error> {
305    // Validate that all fields are named
306    for field in &input.fields {
307        if field.ident.is_none() {
308            return Err(Error::new_spanned(field, "All fields must be named."));
309        }
310    }
311
312    let Constraints {
313        input_constraints,
314        impl_generics,
315        type_generics,
316    } = Constraints::get(&input);
317    let struct_name = &input.ident;
318
319    let field_types = input.fields.iter().map(|field| &field.ty);
320    let mut field_hashes_mut = Vec::new();
321    let mut field_hashes = Vec::new();
322    for e in &input.fields {
323        let name = e.ident.as_ref().unwrap();
324        field_hashes_mut.push(quote! { hasher.write_all(self.#name.hash_mut().await?.as_ref())?; });
325        field_hashes.push(quote! { hasher.write_all(self.#name.hash().await?.as_ref())?; });
326    }
327
328    Ok(quote! {
329        impl #impl_generics linera_views::views::HashableView for #struct_name #type_generics
330        where
331            #(#field_types: linera_views::views::HashableView,)*
332            #(#input_constraints,)*
333            Self: linera_views::views::View,
334        {
335            type Hasher = linera_views::sha3::Sha3_256;
336
337            async fn hash_mut(&mut self) -> Result<<Self::Hasher as linera_views::views::Hasher>::Output, linera_views::ViewError> {
338                use linera_views::views::Hasher as _;
339                use std::io::Write as _;
340                let mut hasher = Self::Hasher::default();
341                #(#field_hashes_mut)*
342                Ok(hasher.finalize())
343            }
344
345            async fn hash(&self) -> Result<<Self::Hasher as linera_views::views::Hasher>::Output, linera_views::ViewError> {
346                use linera_views::views::Hasher as _;
347                use std::io::Write as _;
348                let mut hasher = Self::Hasher::default();
349                #(#field_hashes)*
350                Ok(hasher.finalize())
351            }
352        }
353    })
354}
355
356fn generate_crypto_hash_code(input: ItemStruct) -> TokenStream2 {
357    let Constraints {
358        input_constraints,
359        impl_generics,
360        type_generics,
361    } = Constraints::get(&input);
362    let field_types = input.fields.iter().map(|field| &field.ty);
363    let struct_name = &input.ident;
364    let hash_type = syn::Ident::new(&format!("{struct_name}Hash"), Span::call_site());
365    quote! {
366        impl #impl_generics linera_views::views::CryptoHashView
367        for #struct_name #type_generics
368        where
369            #(#field_types: linera_views::views::HashableView,)*
370            #(#input_constraints,)*
371            Self: linera_views::views::View,
372        {
373            async fn crypto_hash(&self) -> Result<linera_base::crypto::CryptoHash, linera_views::ViewError> {
374                use linera_base::crypto::{BcsHashable, CryptoHash};
375                use linera_views::{
376                    generic_array::GenericArray,
377                    sha3::{digest::OutputSizeUser, Sha3_256},
378                    views::HashableView as _,
379                };
380                #[derive(serde::Serialize, serde::Deserialize)]
381                struct #hash_type(GenericArray<u8, <Sha3_256 as OutputSizeUser>::OutputSize>);
382                impl<'de> BcsHashable<'de> for #hash_type {}
383                let hash = self.hash().await?;
384                Ok(CryptoHash::new(&#hash_type(hash)))
385            }
386
387            async fn crypto_hash_mut(&mut self) -> Result<linera_base::crypto::CryptoHash, linera_views::ViewError> {
388                use linera_base::crypto::{BcsHashable, CryptoHash};
389                use linera_views::{
390                    generic_array::GenericArray,
391                    sha3::{digest::OutputSizeUser, Sha3_256},
392                    views::HashableView as _,
393                };
394                #[derive(serde::Serialize, serde::Deserialize)]
395                struct #hash_type(GenericArray<u8, <Sha3_256 as OutputSizeUser>::OutputSize>);
396                impl<'de> BcsHashable<'de> for #hash_type {}
397                let hash = self.hash_mut().await?;
398                Ok(CryptoHash::new(&#hash_type(hash)))
399            }
400        }
401    }
402}
403
404fn generate_clonable_view_code(input: ItemStruct) -> Result<TokenStream2, Error> {
405    // Validate that all fields are named
406    for field in &input.fields {
407        if field.ident.is_none() {
408            return Err(Error::new_spanned(field, "All fields must be named."));
409        }
410    }
411
412    let Constraints {
413        input_constraints,
414        impl_generics,
415        type_generics,
416    } = Constraints::get(&input);
417    let struct_name = &input.ident;
418
419    let mut clone_constraints = vec![];
420    let mut clone_fields = vec![];
421
422    for field in &input.fields {
423        let name = &field.ident;
424        let ty = &field.ty;
425        clone_constraints.push(quote! { #ty: ClonableView });
426        clone_fields.push(quote! { #name: self.#name.clone_unchecked()? });
427    }
428
429    Ok(quote! {
430        impl #impl_generics linera_views::views::ClonableView for #struct_name #type_generics
431        where
432            #(#input_constraints,)*
433            #(#clone_constraints,)*
434            Self: linera_views::views::View,
435        {
436            fn clone_unchecked(&mut self) -> Result<Self, linera_views::ViewError> {
437                Ok(Self {
438                    #(#clone_fields,)*
439                })
440            }
441        }
442    })
443}
444
445fn to_token_stream(input: Result<TokenStream2, Error>) -> TokenStream {
446    match input {
447        Ok(tokens) => tokens.into(),
448        Err(err) => err.to_compile_error().into(),
449    }
450}
451
452#[proc_macro_derive(View, attributes(view))]
453pub fn derive_view(input: TokenStream) -> TokenStream {
454    let input = parse_macro_input!(input as ItemStruct);
455    let input = generate_view_code(input, false);
456    to_token_stream(input)
457}
458
459fn derive_hash_view_token_stream2(input: ItemStruct) -> Result<TokenStream2, Error> {
460    let mut stream = generate_view_code(input.clone(), false)?;
461    stream.extend(generate_hash_view_code(input)?);
462    Ok(stream)
463}
464
465#[proc_macro_derive(HashableView, attributes(view))]
466pub fn derive_hash_view(input: TokenStream) -> TokenStream {
467    let input = parse_macro_input!(input as ItemStruct);
468
469    let stream = derive_hash_view_token_stream2(input);
470    to_token_stream(stream)
471}
472
473fn derive_root_view_token_stream2(input: ItemStruct) -> Result<TokenStream2, Error> {
474    let mut stream = generate_view_code(input.clone(), true)?;
475    stream.extend(generate_root_view_code(input));
476    Ok(stream)
477}
478
479#[proc_macro_derive(RootView, attributes(view))]
480pub fn derive_root_view(input: TokenStream) -> TokenStream {
481    let input = parse_macro_input!(input as ItemStruct);
482
483    let stream = derive_root_view_token_stream2(input);
484    to_token_stream(stream)
485}
486
487fn derive_crypto_hash_view_token_stream2(input: ItemStruct) -> Result<TokenStream2, Error> {
488    let mut stream = generate_view_code(input.clone(), false)?;
489    stream.extend(generate_hash_view_code(input.clone())?);
490    stream.extend(generate_crypto_hash_code(input));
491    Ok(stream)
492}
493
494#[proc_macro_derive(CryptoHashView, attributes(view))]
495pub fn derive_crypto_hash_view(input: TokenStream) -> TokenStream {
496    let input = parse_macro_input!(input as ItemStruct);
497
498    let stream = derive_crypto_hash_view_token_stream2(input);
499    to_token_stream(stream)
500}
501
502fn derive_crypto_hash_root_view_token_stream2(input: ItemStruct) -> Result<TokenStream2, Error> {
503    let mut stream = generate_view_code(input.clone(), true)?;
504    stream.extend(generate_root_view_code(input.clone()));
505    stream.extend(generate_hash_view_code(input.clone())?);
506    stream.extend(generate_crypto_hash_code(input));
507    Ok(stream)
508}
509
510#[proc_macro_derive(CryptoHashRootView, attributes(view))]
511pub fn derive_crypto_hash_root_view(input: TokenStream) -> TokenStream {
512    let input = parse_macro_input!(input as ItemStruct);
513
514    let stream = derive_crypto_hash_root_view_token_stream2(input);
515    to_token_stream(stream)
516}
517
518#[cfg(test)]
519fn derive_hashable_root_view_token_stream2(input: ItemStruct) -> Result<TokenStream2, Error> {
520    let mut stream = generate_view_code(input.clone(), true)?;
521    stream.extend(generate_root_view_code(input.clone()));
522    stream.extend(generate_hash_view_code(input)?);
523    Ok(stream)
524}
525
526#[proc_macro_derive(HashableRootView, attributes(view))]
527#[cfg(test)]
528pub fn derive_hashable_root_view(input: TokenStream) -> TokenStream {
529    let input = parse_macro_input!(input as ItemStruct);
530
531    let stream = derive_hashable_root_view_token_stream2(input);
532    to_token_stream(stream)
533}
534
535#[proc_macro_derive(ClonableView, attributes(view))]
536pub fn derive_clonable_view(input: TokenStream) -> TokenStream {
537    let input = parse_macro_input!(input as ItemStruct);
538    match generate_clonable_view_code(input) {
539        Ok(tokens) => tokens.into(),
540        Err(err) => err.to_compile_error().into(),
541    }
542}
543
544#[cfg(test)]
545pub mod tests {
546
547    use quote::quote;
548    use syn::{parse_quote, AngleBracketedGenericArguments};
549
550    use crate::*;
551
552    fn pretty(tokens: TokenStream2) -> String {
553        prettyplease::unparse(
554            &syn::parse2::<syn::File>(tokens).expect("failed to parse test output"),
555        )
556    }
557
558    #[test]
559    fn test_generate_view_code() {
560        for context in SpecificContextInfo::test_cases() {
561            let input = context.test_view_input();
562            insta::assert_snapshot!(
563                format!(
564                    "test_generate_view_code{}_{}",
565                    if cfg!(feature = "metrics") {
566                        "_metrics"
567                    } else {
568                        ""
569                    },
570                    context.name,
571                ),
572                pretty(generate_view_code(input, true).unwrap())
573            );
574        }
575    }
576
577    #[test]
578    fn test_generate_hash_view_code() {
579        for context in SpecificContextInfo::test_cases() {
580            let input = context.test_view_input();
581            insta::assert_snapshot!(
582                format!("test_generate_hash_view_code_{}", context.name),
583                pretty(generate_hash_view_code(input).unwrap())
584            );
585        }
586    }
587
588    #[test]
589    fn test_generate_root_view_code() {
590        for context in SpecificContextInfo::test_cases() {
591            let input = context.test_view_input();
592            insta::assert_snapshot!(
593                format!(
594                    "test_generate_root_view_code{}_{}",
595                    if cfg!(feature = "metrics") {
596                        "_metrics"
597                    } else {
598                        ""
599                    },
600                    context.name,
601                ),
602                pretty(generate_root_view_code(input))
603            );
604        }
605    }
606
607    #[test]
608    fn test_generate_crypto_hash_code() {
609        for context in SpecificContextInfo::test_cases() {
610            let input = context.test_view_input();
611            insta::assert_snapshot!(pretty(generate_crypto_hash_code(input)));
612        }
613    }
614
615    #[test]
616    fn test_generate_clonable_view_code() {
617        for context in SpecificContextInfo::test_cases() {
618            let input = context.test_view_input();
619            insta::assert_snapshot!(pretty(generate_clonable_view_code(input).unwrap()));
620        }
621    }
622
623    #[derive(Clone)]
624    pub struct SpecificContextInfo {
625        name: String,
626        attribute: Option<TokenStream2>,
627        context: Type,
628        generics: AngleBracketedGenericArguments,
629        where_clause: Option<TokenStream2>,
630    }
631
632    impl SpecificContextInfo {
633        pub fn empty() -> Self {
634            SpecificContextInfo {
635                name: "C".to_string(),
636                attribute: None,
637                context: syn::parse_quote! { C },
638                generics: syn::parse_quote! { <C> },
639                where_clause: None,
640            }
641        }
642
643        pub fn new(context: syn::Type) -> Self {
644            let name = quote! { #context };
645            SpecificContextInfo {
646                name: format!("{name}")
647                    .replace(' ', "")
648                    .replace([':', '<', '>'], "_"),
649                attribute: Some(quote! { #[view(context = #context)] }),
650                context,
651                generics: parse_quote! { <> },
652                where_clause: None,
653            }
654        }
655
656        /// Sets the `where_clause` to a dummy value for test cases with a where clause.
657        ///
658        /// Also adds a `MyParam` generic type parameter to the `generics` field, which is the type
659        /// constrained by the dummy predicate in the `where_clause`.
660        pub fn with_dummy_where_clause(mut self) -> Self {
661            self.generics.args.push(parse_quote! { MyParam });
662            self.where_clause = Some(quote! {
663                where MyParam: Send + Sync + 'static,
664            });
665            self.name.push_str("_with_where");
666
667            self
668        }
669
670        pub fn test_cases() -> impl Iterator<Item = Self> {
671            Some(Self::empty())
672                .into_iter()
673                .chain(
674                    [
675                        syn::parse_quote! { CustomContext },
676                        syn::parse_quote! { custom::path::to::ContextType },
677                        syn::parse_quote! { custom::GenericContext<T> },
678                    ]
679                    .into_iter()
680                    .map(Self::new),
681                )
682                .flat_map(|case| [case.clone(), case.with_dummy_where_clause()])
683        }
684
685        pub fn test_view_input(&self) -> ItemStruct {
686            let SpecificContextInfo {
687                attribute,
688                context,
689                generics,
690                where_clause,
691                ..
692            } = self;
693
694            parse_quote! {
695                #attribute
696                struct TestView #generics
697                #where_clause
698                {
699                    register: RegisterView<#context, usize>,
700                    collection: CollectionView<#context, usize, RegisterView<#context, usize>>,
701                }
702            }
703        }
704    }
705
706    // Failure scenario tests
707    #[test]
708    fn test_tuple_struct_failure() {
709        let input: ItemStruct = parse_quote! {
710            struct TestView<C>(RegisterView<C, u64>);
711        };
712        let result = generate_view_code(input, false);
713        assert!(result.is_err());
714        let error_msg = result.unwrap_err().to_string();
715        assert!(error_msg.contains("All fields must be named"));
716    }
717
718    #[test]
719    fn test_empty_struct_failure() {
720        let input: ItemStruct = parse_quote! {
721            struct TestView<C> {}
722        };
723        let result = generate_view_code(input, false);
724        assert!(result.is_err());
725        let error_msg = result.unwrap_err().to_string();
726        assert!(error_msg.contains("Struct must have at least one field"));
727    }
728
729    #[test]
730    fn test_missing_context_no_generics_failure() {
731        let input: ItemStruct = parse_quote! {
732            struct TestView {
733                register: RegisterView<CustomContext, u64>,
734            }
735        };
736        let result = generate_view_code(input, false);
737        assert!(result.is_err());
738        let error_msg = result.unwrap_err().to_string();
739        assert!(error_msg.contains("Missing context"));
740    }
741
742    #[test]
743    fn test_missing_context_empty_generics_failure() {
744        let input: ItemStruct = parse_quote! {
745            struct TestView<> {
746                register: RegisterView<CustomContext, u64>,
747            }
748        };
749        let result = generate_view_code(input, false);
750        assert!(result.is_err());
751        let error_msg = result.unwrap_err().to_string();
752        assert!(error_msg.contains("Missing context"));
753    }
754
755    #[test]
756    fn test_non_path_type_failure() {
757        let input: ItemStruct = parse_quote! {
758            struct TestView<C> {
759                field: fn() -> i32,
760            }
761        };
762        let result = generate_view_code(input, false);
763        assert!(result.is_err());
764        let error_msg = result.unwrap_err().to_string();
765        assert!(error_msg.contains("Expected a path type"));
766    }
767
768    #[test]
769    fn test_unnamed_field_in_hash_view_failure() {
770        let input: ItemStruct = parse_quote! {
771            struct TestView<C>(RegisterView<C, u64>);
772        };
773        let result = generate_hash_view_code(input);
774        assert!(result.is_err());
775        let error_msg = result.unwrap_err().to_string();
776        assert!(error_msg.contains("All fields must be named"));
777    }
778
779    #[test]
780    fn test_unnamed_field_in_clonable_view_failure() {
781        let input: ItemStruct = parse_quote! {
782            struct TestView<C>(RegisterView<C, u64>);
783        };
784        let result = generate_clonable_view_code(input);
785        assert!(result.is_err());
786        let error_msg = result.unwrap_err().to_string();
787        assert!(error_msg.contains("All fields must be named"));
788    }
789
790    #[test]
791    fn test_array_type_failure() {
792        let input: ItemStruct = parse_quote! {
793            struct TestView<C> {
794                field: [u8; 32],
795            }
796        };
797        let result = generate_view_code(input, false);
798        assert!(result.is_err());
799        let error_msg = result.unwrap_err().to_string();
800        assert!(error_msg.contains("Expected a path type"));
801    }
802
803    #[test]
804    fn test_reference_type_failure() {
805        let input: ItemStruct = parse_quote! {
806            struct TestView<C> {
807                field: &'static str,
808            }
809        };
810        let result = generate_view_code(input, false);
811        assert!(result.is_err());
812        let error_msg = result.unwrap_err().to_string();
813        assert!(error_msg.contains("Expected a path type"));
814    }
815
816    #[test]
817    fn test_pointer_type_failure() {
818        let input: ItemStruct = parse_quote! {
819            struct TestView<C> {
820                field: *const i32,
821            }
822        };
823        let result = generate_view_code(input, false);
824        assert!(result.is_err());
825        let error_msg = result.unwrap_err().to_string();
826        assert!(error_msg.contains("Expected a path type"));
827    }
828
829    #[test]
830    fn test_generate_root_view_code_with_empty_struct() {
831        let input: ItemStruct = parse_quote! {
832            struct TestView<C> {}
833        };
834        // Root view generation depends on view generation, so this should fail at the view level
835        let result = generate_view_code(input.clone(), true);
836        assert!(result.is_err());
837        let error_msg = result.unwrap_err().to_string();
838        assert!(error_msg.contains("Struct must have at least one field"));
839    }
840
841    #[test]
842    fn test_generate_functions_behavior_differences() {
843        // Some generation functions validate field types while others don't
844        let input: ItemStruct = parse_quote! {
845            struct TestView<C> {
846                field: fn() -> i32,
847            }
848        };
849
850        // View code generation validates field types and should fail
851        let view_result = generate_view_code(input.clone(), false);
852        assert!(view_result.is_err());
853        let error_msg = view_result.unwrap_err().to_string();
854        assert!(error_msg.contains("Expected a path type"));
855
856        // Hash view generation doesn't validate field types in the same way
857        let hash_result = generate_hash_view_code(input.clone());
858        assert!(hash_result.is_ok());
859
860        // Crypto hash code generation also succeeds
861        let _result = generate_crypto_hash_code(input);
862    }
863
864    #[test]
865    fn test_crypto_hash_code_generation_failure() {
866        // Crypto hash code generation should succeed as it doesn't validate field types directly
867        let input: ItemStruct = parse_quote! {
868            struct TestView<C> {
869                register: RegisterView<C, usize>,
870            }
871        };
872        let _result = generate_crypto_hash_code(input);
873    }
874}