Skip to main content

linera_sdk_derive/
lib.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! The procedural macros for the crate `linera-sdk`.
5
6#![deny(missing_docs)]
7
8mod stable_enum;
9mod utils;
10
11use proc_macro::TokenStream;
12use proc_macro2::{Ident, Span};
13use syn::{
14    __private::{quote::quote, TokenStream2},
15    parse_macro_input, Fields, ItemEnum,
16};
17
18use crate::utils::{concat, snakify};
19
20/// Derives `GraphQLMutationRoot` for an operation enum, generating a GraphQL mutation root
21/// whose mutations each schedule the corresponding operation. SDK paths in the generated code
22/// are resolved against the `linera_sdk` crate.
23#[proc_macro_derive(GraphQLMutationRoot)]
24pub fn derive_mutation_root(input: TokenStream) -> TokenStream {
25    let input = parse_macro_input!(input as ItemEnum);
26    generate_mutation_root_code(input, "linera_sdk").into()
27}
28
29/// Like the `GraphQLMutationRoot` derive, but resolves SDK paths against `crate` instead of
30/// `linera_sdk`. Used within the `linera-sdk` crate itself.
31#[proc_macro_derive(GraphQLMutationRootInCrate)]
32pub fn derive_mutation_root_in_crate(input: TokenStream) -> TokenStream {
33    let input = parse_macro_input!(input as ItemEnum);
34    generate_mutation_root_code(input, "crate").into()
35}
36
37/// Derive `linera_sdk::formats::StableEnum` for an `enum`. Expands to:
38///
39/// * `serde::Serialize` / `serde::Deserialize` impls in which the variant tag
40///   is the first 4 bytes of `Keccak-256(variant_name)` (read big-endian as
41///   `u32`), with the top 5 bits masked to `00001` so the ULEB128 encoding is
42///   always exactly 4 bytes; and
43/// * a `linera_sdk::formats::StableEnumTrace` impl exposing the per-variant
44///   tags and a `trace_all_variants` method that drives
45///   `serde_reflection::Tracer` without caller-supplied samples.
46#[proc_macro_derive(StableEnum)]
47pub fn derive_stable_enum(input: TokenStream) -> TokenStream {
48    let input = parse_macro_input!(input as ItemEnum);
49    stable_enum::generate_all(&input, stable_enum::CrateRoot::LineraSdk)
50        .unwrap_or_else(|err| err.to_compile_error())
51        .into()
52}
53
54/// Same as [`StableEnum`] but referring to the trait through `crate::...`
55/// instead of `::linera_sdk::...`. Used inside the `linera-sdk` crate itself
56/// (and only there).
57#[proc_macro_derive(StableEnumInCrate)]
58pub fn derive_stable_enum_in_crate(input: TokenStream) -> TokenStream {
59    let input = parse_macro_input!(input as ItemEnum);
60    stable_enum::generate_all(&input, stable_enum::CrateRoot::Crate)
61        .unwrap_or_else(|err| err.to_compile_error())
62        .into()
63}
64
65fn generate_mutation_root_code(input: ItemEnum, crate_root: &str) -> TokenStream2 {
66    let crate_root = Ident::new(crate_root, Span::call_site());
67    let enum_name = input.ident;
68    let mutation_root_name = concat(&enum_name, "MutationRoot");
69    let mut methods = vec![];
70
71    for variant in input.variants {
72        let variant_name = &variant.ident;
73        let function_name = snakify(variant_name);
74        match variant.fields {
75            Fields::Named(named) => {
76                let mut fields = vec![];
77                let mut field_names = vec![];
78                for field in named.named {
79                    let name = field.ident.expect("named fields always have names");
80                    let ty = field.ty;
81                    fields.push(quote! {#name: #ty});
82                    field_names.push(name);
83                }
84                methods.push(quote! {
85                    async fn #function_name(&self, #(#fields,)*) -> [u8; 0] {
86                        let operation = #enum_name::#variant_name {
87                            #(#field_names,)*
88                        };
89
90                        self.runtime.schedule_operation(&operation);
91
92                        []
93                    }
94                });
95            }
96            Fields::Unnamed(unnamed) => {
97                let mut fields = vec![];
98                let mut field_names = vec![];
99                for (i, field) in unnamed.unnamed.iter().enumerate() {
100                    let name = concat(&syn::parse_str::<Ident>("field").unwrap(), &i.to_string());
101                    let ty = &field.ty;
102                    fields.push(quote! {#name: #ty});
103                    field_names.push(name);
104                }
105                methods.push(quote! {
106                    async fn #function_name(&self, #(#fields,)*) -> [u8; 0] {
107                        let operation = #enum_name::#variant_name(
108                            #(#field_names,)*
109                        );
110
111                        self.runtime.schedule_operation(&operation);
112
113                        []
114                    }
115                });
116            }
117            Fields::Unit => {
118                methods.push(quote! {
119                    async fn #function_name(&self) -> [u8; 0] {
120                        let operation = #enum_name::#variant_name;
121
122                        self.runtime.schedule_operation(&operation);
123
124                        []
125                    }
126                });
127            }
128        };
129    }
130
131    quote! {
132        /// Mutation root
133        pub struct #mutation_root_name<Application>
134        where
135            Application: #crate_root::Service,
136            Application::Abi: #crate_root::abi::ContractAbi<Operation = #enum_name>,
137            #crate_root::ServiceRuntime<Application>: Send + Sync,
138        {
139            runtime: ::std::sync::Arc<#crate_root::ServiceRuntime<Application>>,
140        }
141
142        #[async_graphql::Object]
143        impl<Application> #mutation_root_name<Application>
144        where
145            Application: #crate_root::Service,
146            Application::Abi: #crate_root::abi::ContractAbi<Operation = #enum_name>,
147            #crate_root::ServiceRuntime<Application>: Send + Sync,
148        {
149            #(#methods)*
150        }
151
152        impl<Application> #crate_root::graphql::GraphQLMutationRoot<Application> for #enum_name
153        where
154            Application: #crate_root::Service,
155            Application::Abi: #crate_root::abi::ContractAbi<Operation = #enum_name>,
156            #crate_root::ServiceRuntime<Application>: Send + Sync,
157        {
158            type MutationRoot = #mutation_root_name<Application>;
159
160            fn mutation_root(
161                runtime: ::std::sync::Arc<#crate_root::ServiceRuntime<Application>>,
162            ) -> Self::MutationRoot {
163                #mutation_root_name { runtime }
164            }
165        }
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use syn::{__private::quote::quote, parse_quote, ItemEnum};
172
173    use crate::generate_mutation_root_code;
174
175    fn assert_eq_no_whitespace(mut actual: String, mut expected: String) {
176        // Intentionally left here for debugging purposes
177        println!("{actual}");
178
179        actual.retain(|c| !c.is_whitespace());
180        expected.retain(|c| !c.is_whitespace());
181
182        assert_eq!(actual, expected);
183    }
184
185    #[test]
186    fn test_derive_mutation_root() {
187        let operation: ItemEnum = parse_quote! {
188            enum SomeOperation {
189                TupleVariant(String),
190                StructVariant {
191                    a: u32,
192                    b: u64
193                },
194                EmptyVariant
195            }
196        };
197
198        let output = generate_mutation_root_code(operation, "linera_sdk");
199
200        let expected = quote! {
201            /// Mutation root
202            pub struct SomeOperationMutationRoot<Application>
203            where
204                Application: linera_sdk::Service,
205                Application::Abi: linera_sdk::abi::ContractAbi<Operation = SomeOperation>,
206                linera_sdk::ServiceRuntime<Application>: Send + Sync,
207            {
208                runtime: ::std::sync::Arc<linera_sdk::ServiceRuntime<Application>>,
209            }
210
211            #[async_graphql::Object]
212            impl<Application> SomeOperationMutationRoot<Application>
213            where
214                Application: linera_sdk::Service,
215                Application::Abi: linera_sdk::abi::ContractAbi<Operation = SomeOperation>,
216                linera_sdk::ServiceRuntime<Application>: Send + Sync,
217            {
218                async fn tuple_variant(&self, field0: String,) -> [u8; 0] {
219                    let operation = SomeOperation::TupleVariant(field0,);
220                    self.runtime.schedule_operation(&operation);
221                    []
222                }
223
224                async fn struct_variant(&self, a: u32, b: u64,) -> [u8; 0] {
225                    let operation = SomeOperation::StructVariant { a, b, };
226                    self.runtime.schedule_operation(&operation);
227                    []
228                }
229
230                async fn empty_variant(&self) -> [u8; 0] {
231                    let operation = SomeOperation::EmptyVariant;
232                    self.runtime.schedule_operation(&operation);
233                    []
234                }
235            }
236
237            impl<Application> linera_sdk::graphql::GraphQLMutationRoot<Application>
238                for SomeOperation
239            where
240                Application: linera_sdk::Service,
241                Application::Abi: linera_sdk::abi::ContractAbi<Operation = SomeOperation>,
242                linera_sdk::ServiceRuntime<Application>: Send + Sync,
243            {
244                type MutationRoot = SomeOperationMutationRoot<Application>;
245
246                fn mutation_root(
247                    runtime: ::std::sync::Arc<linera_sdk::ServiceRuntime<Application>>,
248                ) -> Self::MutationRoot {
249                    SomeOperationMutationRoot { runtime }
250                }
251            }
252        };
253
254        assert_eq_no_whitespace(output.to_string(), expected.to_string());
255    }
256}