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 metrics_code = 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 let write_batch_with_metrics = if cfg!(feature = "metrics") {
263 quote! {
264 if !batch.is_empty() {
265 #[cfg(not(target_arch = "wasm32"))]
266 let start = std::time::Instant::now();
267 self.context().store().write_batch(batch).await?;
268 #[cfg(not(target_arch = "wasm32"))]
269 {
270 let latency_ms = start.elapsed().as_secs_f64() * 1000.0;
271 linera_views::metrics::SAVE_VIEW_LATENCY
272 .with_label_values(&[stringify!(#struct_name)])
273 .observe(latency_ms);
274 }
275 }
276 }
277 } else {
278 quote! {
279 if !batch.is_empty() {
280 self.context().store().write_batch(batch).await?;
281 }
282 }
283 };
284
285 quote! {
286 impl #impl_generics linera_views::views::RootView for #struct_name #type_generics
287 where
288 #(#input_constraints,)*
289 Self: linera_views::views::View,
290 {
291 async fn save(&mut self) -> Result<(), linera_views::ViewError> {
292 use linera_views::{context::Context as _, batch::Batch, store::WritableKeyValueStore as _, views::View as _};
293 #metrics_code
294 let mut batch = Batch::new();
295 self.pre_save(&mut batch)?;
296 #write_batch_with_metrics
297 self.post_save();
298 Ok(())
299 }
300 }
301 }
302}
303
304fn generate_hash_view_code(input: ItemStruct) -> Result<TokenStream2, Error> {
305 for field in &input.fields {
307 if field.ident.is_none() {
308 return Err(Error::new_spanned(field, "All fields must be named."));
309 }
310 }
311
312 let Constraints {
313 input_constraints,
314 impl_generics,
315 type_generics,
316 } = Constraints::get(&input);
317 let struct_name = &input.ident;
318
319 let field_types = input.fields.iter().map(|field| &field.ty);
320 let mut field_hashes_mut = Vec::new();
321 let mut field_hashes = Vec::new();
322 for e in &input.fields {
323 let name = e.ident.as_ref().unwrap();
324 field_hashes_mut.push(quote! { hasher.write_all(self.#name.hash_mut().await?.as_ref())?; });
325 field_hashes.push(quote! { hasher.write_all(self.#name.hash().await?.as_ref())?; });
326 }
327
328 Ok(quote! {
329 impl #impl_generics linera_views::views::HashableView for #struct_name #type_generics
330 where
331 #(#field_types: linera_views::views::HashableView,)*
332 #(#input_constraints,)*
333 Self: linera_views::views::View,
334 {
335 type Hasher = linera_views::sha3::Sha3_256;
336
337 async fn hash_mut(&mut self) -> Result<<Self::Hasher as linera_views::views::Hasher>::Output, linera_views::ViewError> {
338 use linera_views::views::Hasher as _;
339 use std::io::Write as _;
340 let mut hasher = Self::Hasher::default();
341 #(#field_hashes_mut)*
342 Ok(hasher.finalize())
343 }
344
345 async fn hash(&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)*
350 Ok(hasher.finalize())
351 }
352 }
353 })
354}
355
356fn generate_crypto_hash_code(input: ItemStruct) -> TokenStream2 {
357 let Constraints {
358 input_constraints,
359 impl_generics,
360 type_generics,
361 } = Constraints::get(&input);
362 let field_types = input.fields.iter().map(|field| &field.ty);
363 let struct_name = &input.ident;
364 let hash_type = syn::Ident::new(&format!("{struct_name}Hash"), Span::call_site());
365 quote! {
366 impl #impl_generics linera_views::views::CryptoHashView
367 for #struct_name #type_generics
368 where
369 #(#field_types: linera_views::views::HashableView,)*
370 #(#input_constraints,)*
371 Self: linera_views::views::View,
372 {
373 async fn crypto_hash(&self) -> Result<linera_base::crypto::CryptoHash, linera_views::ViewError> {
374 use linera_base::crypto::{BcsHashable, CryptoHash};
375 use linera_views::{
376 generic_array::GenericArray,
377 sha3::{digest::OutputSizeUser, Sha3_256},
378 views::HashableView as _,
379 };
380 #[derive(serde::Serialize, serde::Deserialize)]
381 struct #hash_type(GenericArray<u8, <Sha3_256 as OutputSizeUser>::OutputSize>);
382 impl<'de> BcsHashable<'de> for #hash_type {}
383 let hash = self.hash().await?;
384 Ok(CryptoHash::new(&#hash_type(hash)))
385 }
386
387 async fn crypto_hash_mut(&mut self) -> Result<linera_base::crypto::CryptoHash, linera_views::ViewError> {
388 use linera_base::crypto::{BcsHashable, CryptoHash};
389 use linera_views::{
390 generic_array::GenericArray,
391 sha3::{digest::OutputSizeUser, Sha3_256},
392 views::HashableView as _,
393 };
394 #[derive(serde::Serialize, serde::Deserialize)]
395 struct #hash_type(GenericArray<u8, <Sha3_256 as OutputSizeUser>::OutputSize>);
396 impl<'de> BcsHashable<'de> for #hash_type {}
397 let hash = self.hash_mut().await?;
398 Ok(CryptoHash::new(&#hash_type(hash)))
399 }
400 }
401 }
402}
403
404fn generate_clonable_view_code(input: ItemStruct) -> Result<TokenStream2, Error> {
405 for field in &input.fields {
407 if field.ident.is_none() {
408 return Err(Error::new_spanned(field, "All fields must be named."));
409 }
410 }
411
412 let Constraints {
413 input_constraints,
414 impl_generics,
415 type_generics,
416 } = Constraints::get(&input);
417 let struct_name = &input.ident;
418
419 let mut clone_constraints = vec![];
420 let mut clone_fields = vec![];
421
422 for field in &input.fields {
423 let name = &field.ident;
424 let ty = &field.ty;
425 clone_constraints.push(quote! { #ty: ClonableView });
426 clone_fields.push(quote! { #name: self.#name.clone_unchecked()? });
427 }
428
429 Ok(quote! {
430 impl #impl_generics linera_views::views::ClonableView for #struct_name #type_generics
431 where
432 #(#input_constraints,)*
433 #(#clone_constraints,)*
434 Self: linera_views::views::View,
435 {
436 fn clone_unchecked(&mut self) -> Result<Self, linera_views::ViewError> {
437 Ok(Self {
438 #(#clone_fields,)*
439 })
440 }
441 }
442 })
443}
444
445fn to_token_stream(input: Result<TokenStream2, Error>) -> TokenStream {
446 match input {
447 Ok(tokens) => tokens.into(),
448 Err(err) => err.to_compile_error().into(),
449 }
450}
451
452#[proc_macro_derive(View, attributes(view))]
453pub fn derive_view(input: TokenStream) -> TokenStream {
454 let input = parse_macro_input!(input as ItemStruct);
455 let input = generate_view_code(input, false);
456 to_token_stream(input)
457}
458
459fn derive_hash_view_token_stream2(input: ItemStruct) -> Result<TokenStream2, Error> {
460 let mut stream = generate_view_code(input.clone(), false)?;
461 stream.extend(generate_hash_view_code(input)?);
462 Ok(stream)
463}
464
465#[proc_macro_derive(HashableView, attributes(view))]
466pub fn derive_hash_view(input: TokenStream) -> TokenStream {
467 let input = parse_macro_input!(input as ItemStruct);
468
469 let stream = derive_hash_view_token_stream2(input);
470 to_token_stream(stream)
471}
472
473fn derive_root_view_token_stream2(input: ItemStruct) -> Result<TokenStream2, Error> {
474 let mut stream = generate_view_code(input.clone(), true)?;
475 stream.extend(generate_root_view_code(input));
476 Ok(stream)
477}
478
479#[proc_macro_derive(RootView, attributes(view))]
480pub fn derive_root_view(input: TokenStream) -> TokenStream {
481 let input = parse_macro_input!(input as ItemStruct);
482
483 let stream = derive_root_view_token_stream2(input);
484 to_token_stream(stream)
485}
486
487fn derive_crypto_hash_view_token_stream2(input: ItemStruct) -> Result<TokenStream2, Error> {
488 let mut stream = generate_view_code(input.clone(), false)?;
489 stream.extend(generate_hash_view_code(input.clone())?);
490 stream.extend(generate_crypto_hash_code(input));
491 Ok(stream)
492}
493
494#[proc_macro_derive(CryptoHashView, attributes(view))]
495pub fn derive_crypto_hash_view(input: TokenStream) -> TokenStream {
496 let input = parse_macro_input!(input as ItemStruct);
497
498 let stream = derive_crypto_hash_view_token_stream2(input);
499 to_token_stream(stream)
500}
501
502fn derive_crypto_hash_root_view_token_stream2(input: ItemStruct) -> Result<TokenStream2, Error> {
503 let mut stream = generate_view_code(input.clone(), true)?;
504 stream.extend(generate_root_view_code(input.clone()));
505 stream.extend(generate_hash_view_code(input.clone())?);
506 stream.extend(generate_crypto_hash_code(input));
507 Ok(stream)
508}
509
510#[proc_macro_derive(CryptoHashRootView, attributes(view))]
511pub fn derive_crypto_hash_root_view(input: TokenStream) -> TokenStream {
512 let input = parse_macro_input!(input as ItemStruct);
513
514 let stream = derive_crypto_hash_root_view_token_stream2(input);
515 to_token_stream(stream)
516}
517
518#[cfg(test)]
519fn derive_hashable_root_view_token_stream2(input: ItemStruct) -> Result<TokenStream2, Error> {
520 let mut stream = generate_view_code(input.clone(), true)?;
521 stream.extend(generate_root_view_code(input.clone()));
522 stream.extend(generate_hash_view_code(input)?);
523 Ok(stream)
524}
525
526#[proc_macro_derive(HashableRootView, attributes(view))]
527#[cfg(test)]
528pub fn derive_hashable_root_view(input: TokenStream) -> TokenStream {
529 let input = parse_macro_input!(input as ItemStruct);
530
531 let stream = derive_hashable_root_view_token_stream2(input);
532 to_token_stream(stream)
533}
534
535#[proc_macro_derive(ClonableView, attributes(view))]
536pub fn derive_clonable_view(input: TokenStream) -> TokenStream {
537 let input = parse_macro_input!(input as ItemStruct);
538 match generate_clonable_view_code(input) {
539 Ok(tokens) => tokens.into(),
540 Err(err) => err.to_compile_error().into(),
541 }
542}
543
544#[cfg(test)]
545pub mod tests {
546
547 use quote::quote;
548 use syn::{parse_quote, AngleBracketedGenericArguments};
549
550 use crate::*;
551
552 fn pretty(tokens: TokenStream2) -> String {
553 prettyplease::unparse(
554 &syn::parse2::<syn::File>(tokens).expect("failed to parse test output"),
555 )
556 }
557
558 #[test]
559 fn test_generate_view_code() {
560 for context in SpecificContextInfo::test_cases() {
561 let input = context.test_view_input();
562 insta::assert_snapshot!(
563 format!(
564 "test_generate_view_code{}_{}",
565 if cfg!(feature = "metrics") {
566 "_metrics"
567 } else {
568 ""
569 },
570 context.name,
571 ),
572 pretty(generate_view_code(input, true).unwrap())
573 );
574 }
575 }
576
577 #[test]
578 fn test_generate_hash_view_code() {
579 for context in SpecificContextInfo::test_cases() {
580 let input = context.test_view_input();
581 insta::assert_snapshot!(
582 format!("test_generate_hash_view_code_{}", context.name),
583 pretty(generate_hash_view_code(input).unwrap())
584 );
585 }
586 }
587
588 #[test]
589 fn test_generate_root_view_code() {
590 for context in SpecificContextInfo::test_cases() {
591 let input = context.test_view_input();
592 insta::assert_snapshot!(
593 format!(
594 "test_generate_root_view_code{}_{}",
595 if cfg!(feature = "metrics") {
596 "_metrics"
597 } else {
598 ""
599 },
600 context.name,
601 ),
602 pretty(generate_root_view_code(input))
603 );
604 }
605 }
606
607 #[test]
608 fn test_generate_crypto_hash_code() {
609 for context in SpecificContextInfo::test_cases() {
610 let input = context.test_view_input();
611 insta::assert_snapshot!(pretty(generate_crypto_hash_code(input)));
612 }
613 }
614
615 #[test]
616 fn test_generate_clonable_view_code() {
617 for context in SpecificContextInfo::test_cases() {
618 let input = context.test_view_input();
619 insta::assert_snapshot!(pretty(generate_clonable_view_code(input).unwrap()));
620 }
621 }
622
623 #[derive(Clone)]
624 pub struct SpecificContextInfo {
625 name: String,
626 attribute: Option<TokenStream2>,
627 context: Type,
628 generics: AngleBracketedGenericArguments,
629 where_clause: Option<TokenStream2>,
630 }
631
632 impl SpecificContextInfo {
633 pub fn empty() -> Self {
634 SpecificContextInfo {
635 name: "C".to_string(),
636 attribute: None,
637 context: syn::parse_quote! { C },
638 generics: syn::parse_quote! { <C> },
639 where_clause: None,
640 }
641 }
642
643 pub fn new(context: syn::Type) -> Self {
644 let name = quote! { #context };
645 SpecificContextInfo {
646 name: format!("{name}")
647 .replace(' ', "")
648 .replace([':', '<', '>'], "_"),
649 attribute: Some(quote! { #[view(context = #context)] }),
650 context,
651 generics: parse_quote! { <> },
652 where_clause: None,
653 }
654 }
655
656 pub fn with_dummy_where_clause(mut self) -> Self {
661 self.generics.args.push(parse_quote! { MyParam });
662 self.where_clause = Some(quote! {
663 where MyParam: Send + Sync + 'static,
664 });
665 self.name.push_str("_with_where");
666
667 self
668 }
669
670 pub fn test_cases() -> impl Iterator<Item = Self> {
671 Some(Self::empty())
672 .into_iter()
673 .chain(
674 [
675 syn::parse_quote! { CustomContext },
676 syn::parse_quote! { custom::path::to::ContextType },
677 syn::parse_quote! { custom::GenericContext<T> },
678 ]
679 .into_iter()
680 .map(Self::new),
681 )
682 .flat_map(|case| [case.clone(), case.with_dummy_where_clause()])
683 }
684
685 pub fn test_view_input(&self) -> ItemStruct {
686 let SpecificContextInfo {
687 attribute,
688 context,
689 generics,
690 where_clause,
691 ..
692 } = self;
693
694 parse_quote! {
695 #attribute
696 struct TestView #generics
697 #where_clause
698 {
699 register: RegisterView<#context, usize>,
700 collection: CollectionView<#context, usize, RegisterView<#context, usize>>,
701 }
702 }
703 }
704 }
705
706 #[test]
708 fn test_tuple_struct_failure() {
709 let input: ItemStruct = parse_quote! {
710 struct TestView<C>(RegisterView<C, u64>);
711 };
712 let result = generate_view_code(input, false);
713 assert!(result.is_err());
714 let error_msg = result.unwrap_err().to_string();
715 assert!(error_msg.contains("All fields must be named"));
716 }
717
718 #[test]
719 fn test_empty_struct_failure() {
720 let input: ItemStruct = parse_quote! {
721 struct TestView<C> {}
722 };
723 let result = generate_view_code(input, false);
724 assert!(result.is_err());
725 let error_msg = result.unwrap_err().to_string();
726 assert!(error_msg.contains("Struct must have at least one field"));
727 }
728
729 #[test]
730 fn test_missing_context_no_generics_failure() {
731 let input: ItemStruct = parse_quote! {
732 struct TestView {
733 register: RegisterView<CustomContext, u64>,
734 }
735 };
736 let result = generate_view_code(input, false);
737 assert!(result.is_err());
738 let error_msg = result.unwrap_err().to_string();
739 assert!(error_msg.contains("Missing context"));
740 }
741
742 #[test]
743 fn test_missing_context_empty_generics_failure() {
744 let input: ItemStruct = parse_quote! {
745 struct TestView<> {
746 register: RegisterView<CustomContext, u64>,
747 }
748 };
749 let result = generate_view_code(input, false);
750 assert!(result.is_err());
751 let error_msg = result.unwrap_err().to_string();
752 assert!(error_msg.contains("Missing context"));
753 }
754
755 #[test]
756 fn test_non_path_type_failure() {
757 let input: ItemStruct = parse_quote! {
758 struct TestView<C> {
759 field: fn() -> i32,
760 }
761 };
762 let result = generate_view_code(input, false);
763 assert!(result.is_err());
764 let error_msg = result.unwrap_err().to_string();
765 assert!(error_msg.contains("Expected a path type"));
766 }
767
768 #[test]
769 fn test_unnamed_field_in_hash_view_failure() {
770 let input: ItemStruct = parse_quote! {
771 struct TestView<C>(RegisterView<C, u64>);
772 };
773 let result = generate_hash_view_code(input);
774 assert!(result.is_err());
775 let error_msg = result.unwrap_err().to_string();
776 assert!(error_msg.contains("All fields must be named"));
777 }
778
779 #[test]
780 fn test_unnamed_field_in_clonable_view_failure() {
781 let input: ItemStruct = parse_quote! {
782 struct TestView<C>(RegisterView<C, u64>);
783 };
784 let result = generate_clonable_view_code(input);
785 assert!(result.is_err());
786 let error_msg = result.unwrap_err().to_string();
787 assert!(error_msg.contains("All fields must be named"));
788 }
789
790 #[test]
791 fn test_array_type_failure() {
792 let input: ItemStruct = parse_quote! {
793 struct TestView<C> {
794 field: [u8; 32],
795 }
796 };
797 let result = generate_view_code(input, false);
798 assert!(result.is_err());
799 let error_msg = result.unwrap_err().to_string();
800 assert!(error_msg.contains("Expected a path type"));
801 }
802
803 #[test]
804 fn test_reference_type_failure() {
805 let input: ItemStruct = parse_quote! {
806 struct TestView<C> {
807 field: &'static str,
808 }
809 };
810 let result = generate_view_code(input, false);
811 assert!(result.is_err());
812 let error_msg = result.unwrap_err().to_string();
813 assert!(error_msg.contains("Expected a path type"));
814 }
815
816 #[test]
817 fn test_pointer_type_failure() {
818 let input: ItemStruct = parse_quote! {
819 struct TestView<C> {
820 field: *const i32,
821 }
822 };
823 let result = generate_view_code(input, false);
824 assert!(result.is_err());
825 let error_msg = result.unwrap_err().to_string();
826 assert!(error_msg.contains("Expected a path type"));
827 }
828
829 #[test]
830 fn test_generate_root_view_code_with_empty_struct() {
831 let input: ItemStruct = parse_quote! {
832 struct TestView<C> {}
833 };
834 let result = generate_view_code(input.clone(), true);
836 assert!(result.is_err());
837 let error_msg = result.unwrap_err().to_string();
838 assert!(error_msg.contains("Struct must have at least one field"));
839 }
840
841 #[test]
842 fn test_generate_functions_behavior_differences() {
843 let input: ItemStruct = parse_quote! {
845 struct TestView<C> {
846 field: fn() -> i32,
847 }
848 };
849
850 let view_result = generate_view_code(input.clone(), false);
852 assert!(view_result.is_err());
853 let error_msg = view_result.unwrap_err().to_string();
854 assert!(error_msg.contains("Expected a path type"));
855
856 let hash_result = generate_hash_view_code(input.clone());
858 assert!(hash_result.is_ok());
859
860 let _result = generate_crypto_hash_code(input);
862 }
863
864 #[test]
865 fn test_crypto_hash_code_generation_failure() {
866 let input: ItemStruct = parse_quote! {
868 struct TestView<C> {
869 register: RegisterView<C, usize>,
870 }
871 };
872 let _result = generate_crypto_hash_code(input);
873 }
874}