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