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