Skip to main content

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