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