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 flush_quotes = Vec::new();
86    let mut test_flush_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 test_flush_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        flush_quotes.push(quote! { let #test_flush_ident = self.#name.flush(batch)?; });
100        test_flush_quotes.push(quote! { #test_flush_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    let first_name_quote = name_quotes.first().ok_or(Error::new_spanned(
137        &input,
138        "Struct must have at least one field",
139    ))?;
140
141    let load_metrics = if root && cfg!(feature = "metrics") {
142        quote! {
143            #[cfg(not(target_arch = "wasm32"))]
144            linera_views::metrics::increment_counter(
145                &linera_views::metrics::LOAD_VIEW_COUNTER,
146                stringify!(#struct_name),
147                &context.base_key().bytes,
148            );
149            #[cfg(not(target_arch = "wasm32"))]
150            use linera_views::metrics::prometheus_util::MeasureLatency as _;
151            let _latency = linera_views::metrics::LOAD_VIEW_LATENCY.measure_latency();
152        }
153    } else {
154        quote! {}
155    };
156
157    Ok(quote! {
158        impl #impl_generics linera_views::views::View for #struct_name #type_generics
159        where
160            #context: linera_views::context::Context,
161            #(#input_constraints,)*
162            #(#field_types: linera_views::views::View<Context = #context>,)*
163        {
164            const NUM_INIT_KEYS: usize = #(<#field_types as linera_views::views::View>::NUM_INIT_KEYS)+*;
165
166            type Context = #context;
167
168            fn context(&self) -> &#context {
169                self.#first_name_quote.context()
170            }
171
172            fn pre_load(context: &#context) -> Result<Vec<Vec<u8>>, linera_views::ViewError> {
173                use linera_views::context::Context as _;
174                let mut keys = Vec::new();
175                #(#pre_load_keys_quotes)*
176                Ok(keys)
177            }
178
179            fn post_load(context: #context, values: &[Option<Vec<u8>>]) -> Result<Self, linera_views::ViewError> {
180                use linera_views::context::Context as _;
181                let mut __linera_reserved_pos = 0;
182                #(#post_load_keys_quotes)*
183                Ok(Self {#(#name_quotes),*})
184            }
185
186            async fn load(context: #context) -> Result<Self, linera_views::ViewError> {
187                use linera_views::{context::Context as _, store::ReadableKeyValueStore as _};
188                #load_metrics
189                if Self::NUM_INIT_KEYS == 0 {
190                    Self::post_load(context, &[])
191                } else {
192                    let keys = Self::pre_load(&context)?;
193                    let values = context.store().read_multi_values_bytes(keys).await?;
194                    Self::post_load(context, &values)
195                }
196            }
197
198
199            fn rollback(&mut self) {
200                #(#rollback_quotes)*
201            }
202
203            async fn has_pending_changes(&self) -> bool {
204                #(#has_pending_changes_quotes)*
205                false
206            }
207
208            fn flush(&mut self, batch: &mut linera_views::batch::Batch) -> Result<bool, linera_views::ViewError> {
209                #(#flush_quotes)*
210                Ok( #(#test_flush_quotes)&&* )
211            }
212
213            fn clear(&mut self) {
214                #(#clear_quotes)*
215            }
216        }
217    })
218}
219
220fn generate_root_view_code(input: ItemStruct) -> TokenStream2 {
221    let Constraints {
222        input_constraints,
223        impl_generics,
224        type_generics,
225    } = Constraints::get(&input);
226    let struct_name = &input.ident;
227
228    let increment_counter = if cfg!(feature = "metrics") {
229        quote! {
230            #[cfg(not(target_arch = "wasm32"))]
231            linera_views::metrics::increment_counter(
232                &linera_views::metrics::SAVE_VIEW_COUNTER,
233                stringify!(#struct_name),
234                &self.context().base_key().bytes,
235            );
236        }
237    } else {
238        quote! {}
239    };
240
241    quote! {
242        impl #impl_generics linera_views::views::RootView for #struct_name #type_generics
243        where
244            #(#input_constraints,)*
245            Self: linera_views::views::View,
246        {
247            async fn save(&mut self) -> Result<(), linera_views::ViewError> {
248                use linera_views::{context::Context as _, batch::Batch, store::WritableKeyValueStore as _, views::View as _};
249                #increment_counter
250                let mut batch = Batch::new();
251                self.flush(&mut batch)?;
252                if !batch.is_empty() {
253                    self.context().store().write_batch(batch).await?;
254                }
255                Ok(())
256            }
257        }
258    }
259}
260
261fn generate_hash_view_code(input: ItemStruct) -> Result<TokenStream2, Error> {
262    // Validate that all fields are named
263    for field in &input.fields {
264        if field.ident.is_none() {
265            return Err(Error::new_spanned(field, "All fields must be named."));
266        }
267    }
268
269    let Constraints {
270        input_constraints,
271        impl_generics,
272        type_generics,
273    } = Constraints::get(&input);
274    let struct_name = &input.ident;
275
276    let field_types = input.fields.iter().map(|field| &field.ty);
277    let mut field_hashes_mut = Vec::new();
278    let mut field_hashes = Vec::new();
279    for e in &input.fields {
280        let name = e.ident.as_ref().unwrap();
281        field_hashes_mut.push(quote! { hasher.write_all(self.#name.hash_mut().await?.as_ref())?; });
282        field_hashes.push(quote! { hasher.write_all(self.#name.hash().await?.as_ref())?; });
283    }
284
285    Ok(quote! {
286        impl #impl_generics linera_views::views::HashableView for #struct_name #type_generics
287        where
288            #(#field_types: linera_views::views::HashableView,)*
289            #(#input_constraints,)*
290            Self: linera_views::views::View,
291        {
292            type Hasher = linera_views::sha3::Sha3_256;
293
294            async fn hash_mut(&mut self) -> Result<<Self::Hasher as linera_views::views::Hasher>::Output, linera_views::ViewError> {
295                use linera_views::views::Hasher as _;
296                use std::io::Write as _;
297                let mut hasher = Self::Hasher::default();
298                #(#field_hashes_mut)*
299                Ok(hasher.finalize())
300            }
301
302            async fn hash(&self) -> Result<<Self::Hasher as linera_views::views::Hasher>::Output, linera_views::ViewError> {
303                use linera_views::views::Hasher as _;
304                use std::io::Write as _;
305                let mut hasher = Self::Hasher::default();
306                #(#field_hashes)*
307                Ok(hasher.finalize())
308            }
309        }
310    })
311}
312
313fn generate_crypto_hash_code(input: ItemStruct) -> TokenStream2 {
314    let Constraints {
315        input_constraints,
316        impl_generics,
317        type_generics,
318    } = Constraints::get(&input);
319    let field_types = input.fields.iter().map(|field| &field.ty);
320    let struct_name = &input.ident;
321    let hash_type = syn::Ident::new(&format!("{struct_name}Hash"), Span::call_site());
322    quote! {
323        impl #impl_generics linera_views::views::CryptoHashView
324        for #struct_name #type_generics
325        where
326            #(#field_types: linera_views::views::HashableView,)*
327            #(#input_constraints,)*
328            Self: linera_views::views::View,
329        {
330            async fn crypto_hash(&self) -> Result<linera_base::crypto::CryptoHash, linera_views::ViewError> {
331                use linera_base::crypto::{BcsHashable, CryptoHash};
332                use linera_views::{
333                    generic_array::GenericArray,
334                    sha3::{digest::OutputSizeUser, Sha3_256},
335                    views::HashableView as _,
336                };
337                #[derive(serde::Serialize, serde::Deserialize)]
338                struct #hash_type(GenericArray<u8, <Sha3_256 as OutputSizeUser>::OutputSize>);
339                impl<'de> BcsHashable<'de> for #hash_type {}
340                let hash = self.hash().await?;
341                Ok(CryptoHash::new(&#hash_type(hash)))
342            }
343
344            async fn crypto_hash_mut(&mut self) -> Result<linera_base::crypto::CryptoHash, linera_views::ViewError> {
345                use linera_base::crypto::{BcsHashable, CryptoHash};
346                use linera_views::{
347                    generic_array::GenericArray,
348                    sha3::{digest::OutputSizeUser, Sha3_256},
349                    views::HashableView as _,
350                };
351                #[derive(serde::Serialize, serde::Deserialize)]
352                struct #hash_type(GenericArray<u8, <Sha3_256 as OutputSizeUser>::OutputSize>);
353                impl<'de> BcsHashable<'de> for #hash_type {}
354                let hash = self.hash_mut().await?;
355                Ok(CryptoHash::new(&#hash_type(hash)))
356            }
357        }
358    }
359}
360
361fn generate_clonable_view_code(input: ItemStruct) -> Result<TokenStream2, Error> {
362    // Validate that all fields are named
363    for field in &input.fields {
364        if field.ident.is_none() {
365            return Err(Error::new_spanned(field, "All fields must be named."));
366        }
367    }
368
369    let Constraints {
370        input_constraints,
371        impl_generics,
372        type_generics,
373    } = Constraints::get(&input);
374    let struct_name = &input.ident;
375
376    let mut clone_constraints = vec![];
377    let mut clone_fields = vec![];
378
379    for field in &input.fields {
380        let name = &field.ident;
381        let ty = &field.ty;
382        clone_constraints.push(quote! { #ty: ClonableView });
383        clone_fields.push(quote! { #name: self.#name.clone_unchecked()? });
384    }
385
386    Ok(quote! {
387        impl #impl_generics linera_views::views::ClonableView for #struct_name #type_generics
388        where
389            #(#input_constraints,)*
390            #(#clone_constraints,)*
391            Self: linera_views::views::View,
392        {
393            fn clone_unchecked(&mut self) -> Result<Self, linera_views::ViewError> {
394                Ok(Self {
395                    #(#clone_fields,)*
396                })
397            }
398        }
399    })
400}
401
402fn to_token_stream(input: Result<TokenStream2, Error>) -> TokenStream {
403    match input {
404        Ok(tokens) => tokens.into(),
405        Err(err) => err.to_compile_error().into(),
406    }
407}
408
409#[proc_macro_derive(View, attributes(view))]
410pub fn derive_view(input: TokenStream) -> TokenStream {
411    let input = parse_macro_input!(input as ItemStruct);
412    let input = generate_view_code(input, false);
413    to_token_stream(input)
414}
415
416fn derive_hash_view_token_stream2(input: ItemStruct) -> Result<TokenStream2, Error> {
417    let mut stream = generate_view_code(input.clone(), false)?;
418    stream.extend(generate_hash_view_code(input)?);
419    Ok(stream)
420}
421
422#[proc_macro_derive(HashableView, attributes(view))]
423pub fn derive_hash_view(input: TokenStream) -> TokenStream {
424    let input = parse_macro_input!(input as ItemStruct);
425
426    let stream = derive_hash_view_token_stream2(input);
427    to_token_stream(stream)
428}
429
430fn derive_root_view_token_stream2(input: ItemStruct) -> Result<TokenStream2, Error> {
431    let mut stream = generate_view_code(input.clone(), true)?;
432    stream.extend(generate_root_view_code(input));
433    Ok(stream)
434}
435
436#[proc_macro_derive(RootView, attributes(view))]
437pub fn derive_root_view(input: TokenStream) -> TokenStream {
438    let input = parse_macro_input!(input as ItemStruct);
439
440    let stream = derive_root_view_token_stream2(input);
441    to_token_stream(stream)
442}
443
444fn derive_crypto_hash_view_token_stream2(input: ItemStruct) -> Result<TokenStream2, Error> {
445    let mut stream = generate_view_code(input.clone(), false)?;
446    stream.extend(generate_hash_view_code(input.clone())?);
447    stream.extend(generate_crypto_hash_code(input));
448    Ok(stream)
449}
450
451#[proc_macro_derive(CryptoHashView, attributes(view))]
452pub fn derive_crypto_hash_view(input: TokenStream) -> TokenStream {
453    let input = parse_macro_input!(input as ItemStruct);
454
455    let stream = derive_crypto_hash_view_token_stream2(input);
456    to_token_stream(stream)
457}
458
459fn derive_crypto_hash_root_view_token_stream2(input: ItemStruct) -> Result<TokenStream2, Error> {
460    let mut stream = generate_view_code(input.clone(), true)?;
461    stream.extend(generate_root_view_code(input.clone()));
462    stream.extend(generate_hash_view_code(input.clone())?);
463    stream.extend(generate_crypto_hash_code(input));
464    Ok(stream)
465}
466
467#[proc_macro_derive(CryptoHashRootView, attributes(view))]
468pub fn derive_crypto_hash_root_view(input: TokenStream) -> TokenStream {
469    let input = parse_macro_input!(input as ItemStruct);
470
471    let stream = derive_crypto_hash_root_view_token_stream2(input);
472    to_token_stream(stream)
473}
474
475#[cfg(test)]
476fn derive_hashable_root_view_token_stream2(input: ItemStruct) -> Result<TokenStream2, Error> {
477    let mut stream = generate_view_code(input.clone(), true)?;
478    stream.extend(generate_root_view_code(input.clone()));
479    stream.extend(generate_hash_view_code(input)?);
480    Ok(stream)
481}
482
483#[proc_macro_derive(HashableRootView, attributes(view))]
484#[cfg(test)]
485pub fn derive_hashable_root_view(input: TokenStream) -> TokenStream {
486    let input = parse_macro_input!(input as ItemStruct);
487
488    let stream = derive_hashable_root_view_token_stream2(input);
489    to_token_stream(stream)
490}
491
492#[proc_macro_derive(ClonableView, attributes(view))]
493pub fn derive_clonable_view(input: TokenStream) -> TokenStream {
494    let input = parse_macro_input!(input as ItemStruct);
495    match generate_clonable_view_code(input) {
496        Ok(tokens) => tokens.into(),
497        Err(err) => err.to_compile_error().into(),
498    }
499}
500
501#[cfg(test)]
502pub mod tests {
503
504    use quote::quote;
505    use syn::{parse_quote, AngleBracketedGenericArguments};
506
507    use crate::*;
508
509    fn pretty(tokens: TokenStream2) -> String {
510        prettyplease::unparse(
511            &syn::parse2::<syn::File>(tokens).expect("failed to parse test output"),
512        )
513    }
514
515    #[test]
516    fn test_generate_view_code() {
517        for context in SpecificContextInfo::test_cases() {
518            let input = context.test_view_input();
519            insta::assert_snapshot!(
520                format!(
521                    "test_generate_view_code{}_{}",
522                    if cfg!(feature = "metrics") {
523                        "_metrics"
524                    } else {
525                        ""
526                    },
527                    context.name,
528                ),
529                pretty(generate_view_code(input, true).unwrap())
530            );
531        }
532    }
533
534    #[test]
535    fn test_generate_hash_view_code() {
536        for context in SpecificContextInfo::test_cases() {
537            let input = context.test_view_input();
538            insta::assert_snapshot!(
539                format!("test_generate_hash_view_code_{}", context.name),
540                pretty(generate_hash_view_code(input).unwrap())
541            );
542        }
543    }
544
545    #[test]
546    fn test_generate_root_view_code() {
547        for context in SpecificContextInfo::test_cases() {
548            let input = context.test_view_input();
549            insta::assert_snapshot!(
550                format!(
551                    "test_generate_root_view_code{}_{}",
552                    if cfg!(feature = "metrics") {
553                        "_metrics"
554                    } else {
555                        ""
556                    },
557                    context.name,
558                ),
559                pretty(generate_root_view_code(input))
560            );
561        }
562    }
563
564    #[test]
565    fn test_generate_crypto_hash_code() {
566        for context in SpecificContextInfo::test_cases() {
567            let input = context.test_view_input();
568            insta::assert_snapshot!(pretty(generate_crypto_hash_code(input)));
569        }
570    }
571
572    #[test]
573    fn test_generate_clonable_view_code() {
574        for context in SpecificContextInfo::test_cases() {
575            let input = context.test_view_input();
576            insta::assert_snapshot!(pretty(generate_clonable_view_code(input).unwrap()));
577        }
578    }
579
580    #[derive(Clone)]
581    pub struct SpecificContextInfo {
582        name: String,
583        attribute: Option<TokenStream2>,
584        context: Type,
585        generics: AngleBracketedGenericArguments,
586        where_clause: Option<TokenStream2>,
587    }
588
589    impl SpecificContextInfo {
590        pub fn empty() -> Self {
591            SpecificContextInfo {
592                name: "C".to_string(),
593                attribute: None,
594                context: syn::parse_quote! { C },
595                generics: syn::parse_quote! { <C> },
596                where_clause: None,
597            }
598        }
599
600        pub fn new(context: syn::Type) -> Self {
601            let name = quote! { #context };
602            SpecificContextInfo {
603                name: format!("{name}")
604                    .replace(' ', "")
605                    .replace([':', '<', '>'], "_"),
606                attribute: Some(quote! { #[view(context = #context)] }),
607                context,
608                generics: parse_quote! { <> },
609                where_clause: None,
610            }
611        }
612
613        /// Sets the `where_clause` to a dummy value for test cases with a where clause.
614        ///
615        /// Also adds a `MyParam` generic type parameter to the `generics` field, which is the type
616        /// constrained by the dummy predicate in the `where_clause`.
617        pub fn with_dummy_where_clause(mut self) -> Self {
618            self.generics.args.push(parse_quote! { MyParam });
619            self.where_clause = Some(quote! {
620                where MyParam: Send + Sync + 'static,
621            });
622            self.name.push_str("_with_where");
623
624            self
625        }
626
627        pub fn test_cases() -> impl Iterator<Item = Self> {
628            Some(Self::empty())
629                .into_iter()
630                .chain(
631                    [
632                        syn::parse_quote! { CustomContext },
633                        syn::parse_quote! { custom::path::to::ContextType },
634                        syn::parse_quote! { custom::GenericContext<T> },
635                    ]
636                    .into_iter()
637                    .map(Self::new),
638                )
639                .flat_map(|case| [case.clone(), case.with_dummy_where_clause()])
640        }
641
642        pub fn test_view_input(&self) -> ItemStruct {
643            let SpecificContextInfo {
644                attribute,
645                context,
646                generics,
647                where_clause,
648                ..
649            } = self;
650
651            parse_quote! {
652                #attribute
653                struct TestView #generics
654                #where_clause
655                {
656                    register: RegisterView<#context, usize>,
657                    collection: CollectionView<#context, usize, RegisterView<#context, usize>>,
658                }
659            }
660        }
661    }
662
663    // Failure scenario tests
664    #[test]
665    fn test_tuple_struct_failure() {
666        let input: ItemStruct = parse_quote! {
667            struct TestView<C>(RegisterView<C, u64>);
668        };
669        let result = generate_view_code(input, false);
670        assert!(result.is_err());
671        let error_msg = result.unwrap_err().to_string();
672        assert!(error_msg.contains("All fields must be named"));
673    }
674
675    #[test]
676    fn test_empty_struct_failure() {
677        let input: ItemStruct = parse_quote! {
678            struct TestView<C> {}
679        };
680        let result = generate_view_code(input, false);
681        assert!(result.is_err());
682        let error_msg = result.unwrap_err().to_string();
683        assert!(error_msg.contains("Struct must have at least one field"));
684    }
685
686    #[test]
687    fn test_missing_context_no_generics_failure() {
688        let input: ItemStruct = parse_quote! {
689            struct TestView {
690                register: RegisterView<CustomContext, u64>,
691            }
692        };
693        let result = generate_view_code(input, false);
694        assert!(result.is_err());
695        let error_msg = result.unwrap_err().to_string();
696        assert!(error_msg.contains("Missing context"));
697    }
698
699    #[test]
700    fn test_missing_context_empty_generics_failure() {
701        let input: ItemStruct = parse_quote! {
702            struct TestView<> {
703                register: RegisterView<CustomContext, u64>,
704            }
705        };
706        let result = generate_view_code(input, false);
707        assert!(result.is_err());
708        let error_msg = result.unwrap_err().to_string();
709        assert!(error_msg.contains("Missing context"));
710    }
711
712    #[test]
713    fn test_non_path_type_failure() {
714        let input: ItemStruct = parse_quote! {
715            struct TestView<C> {
716                field: fn() -> i32,
717            }
718        };
719        let result = generate_view_code(input, false);
720        assert!(result.is_err());
721        let error_msg = result.unwrap_err().to_string();
722        assert!(error_msg.contains("Expected a path type"));
723    }
724
725    #[test]
726    fn test_unnamed_field_in_hash_view_failure() {
727        let input: ItemStruct = parse_quote! {
728            struct TestView<C>(RegisterView<C, u64>);
729        };
730        let result = generate_hash_view_code(input);
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_unnamed_field_in_clonable_view_failure() {
738        let input: ItemStruct = parse_quote! {
739            struct TestView<C>(RegisterView<C, u64>);
740        };
741        let result = generate_clonable_view_code(input);
742        assert!(result.is_err());
743        let error_msg = result.unwrap_err().to_string();
744        assert!(error_msg.contains("All fields must be named"));
745    }
746
747    #[test]
748    fn test_array_type_failure() {
749        let input: ItemStruct = parse_quote! {
750            struct TestView<C> {
751                field: [u8; 32],
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("Expected a path type"));
758    }
759
760    #[test]
761    fn test_reference_type_failure() {
762        let input: ItemStruct = parse_quote! {
763            struct TestView<C> {
764                field: &'static str,
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("Expected a path type"));
771    }
772
773    #[test]
774    fn test_pointer_type_failure() {
775        let input: ItemStruct = parse_quote! {
776            struct TestView<C> {
777                field: *const 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_generate_root_view_code_with_empty_struct() {
788        let input: ItemStruct = parse_quote! {
789            struct TestView<C> {}
790        };
791        // Root view generation depends on view generation, so this should fail at the view level
792        let result = generate_view_code(input.clone(), true);
793        assert!(result.is_err());
794        let error_msg = result.unwrap_err().to_string();
795        assert!(error_msg.contains("Struct must have at least one field"));
796    }
797
798    #[test]
799    fn test_generate_functions_behavior_differences() {
800        // Some generation functions validate field types while others don't
801        let input: ItemStruct = parse_quote! {
802            struct TestView<C> {
803                field: fn() -> i32,
804            }
805        };
806
807        // View code generation validates field types and should fail
808        let view_result = generate_view_code(input.clone(), false);
809        assert!(view_result.is_err());
810        let error_msg = view_result.unwrap_err().to_string();
811        assert!(error_msg.contains("Expected a path type"));
812
813        // Hash view generation doesn't validate field types in the same way
814        let hash_result = generate_hash_view_code(input.clone());
815        assert!(hash_result.is_ok());
816
817        // Crypto hash code generation also succeeds
818        let _result = generate_crypto_hash_code(input);
819    }
820
821    #[test]
822    fn test_crypto_hash_code_generation_failure() {
823        // Crypto hash code generation should succeed as it doesn't validate field types directly
824        let input: ItemStruct = parse_quote! {
825            struct TestView<C> {
826                register: RegisterView<C, usize>,
827            }
828        };
829        let _result = generate_crypto_hash_code(input);
830    }
831}