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
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 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 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 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 #[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 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 let input: ItemStruct = parse_quote! {
853 struct TestView<C> {
854 field: fn() -> i32,
855 }
856 };
857
858 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 let hash_result = generate_hash_view_code(&input);
866 assert!(hash_result.is_ok());
867
868 let _result = generate_crypto_hash_code(&input);
870 }
871
872 #[test]
873 fn test_crypto_hash_code_generation_failure() {
874 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}