@layerzerolabs/common-utils-macros-stellar-contracts 0.2.122
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +21 -0
- package/LICENSE +23 -0
- package/clippy.toml +7 -0
- package/package.json +37 -0
- package/rust-toolchain.toml +4 -0
- package/rustfmt.toml +15 -0
- package/src/auth.rs +95 -0
- package/src/contract_ttl.rs +92 -0
- package/src/error.rs +43 -0
- package/src/lib.rs +585 -0
- package/src/lz_contract.rs +105 -0
- package/src/rbac.rs +90 -0
- package/src/storage.rs +522 -0
- package/src/tests/auth.rs +230 -0
- package/src/tests/contract_ttl.rs +695 -0
- package/src/tests/error.rs +156 -0
- package/src/tests/lz_contract.rs +87 -0
- package/src/tests/mod.rs +11 -0
- package/src/tests/rbac.rs +523 -0
- package/src/tests/snapshots/common_macros__tests__auth__snapshot_generated_multisig_code.snap +31 -0
- package/src/tests/snapshots/common_macros__tests__auth__snapshot_generated_ownable_code.snap +39 -0
- package/src/tests/snapshots/common_macros__tests__auth__snapshot_only_auth_preserves_function_signature.snap +19 -0
- package/src/tests/snapshots/common_macros__tests__contract_ttl__snapshot_generated_contractimpl_code.snap +77 -0
- package/src/tests/snapshots/common_macros__tests__contract_ttl__snapshot_generated_contracttrait_code.snap +46 -0
- package/src/tests/snapshots/common_macros__tests__error__snapshot_generated_contract_error_code.snap +20 -0
- package/src/tests/snapshots/common_macros__tests__lz_contract__snapshot_generated_lz_contract_code.snap +51 -0
- package/src/tests/snapshots/common_macros__tests__rbac__snapshot_authorizer_role.snap +21 -0
- package/src/tests/snapshots/common_macros__tests__rbac__snapshot_preserve_function_signature.snap +21 -0
- package/src/tests/snapshots/common_macros__tests__ttl_configurable__snapshot_generated_ttl_configurable_code.snap +10 -0
- package/src/tests/snapshots/common_macros__tests__ttl_extendable__snapshot_generated_ttl_extendable_code.snap +8 -0
- package/src/tests/snapshots/common_macros__tests__upgradeable__snapshot_generated_upgradeable_code.snap +28 -0
- package/src/tests/storage/extract_fields.rs +87 -0
- package/src/tests/storage/gen_accessor_methods.rs +223 -0
- package/src/tests/storage/gen_args.rs +65 -0
- package/src/tests/storage/gen_enum_variant.rs +78 -0
- package/src/tests/storage/gen_key.rs +108 -0
- package/src/tests/storage/gen_params.rs +105 -0
- package/src/tests/storage/generate_storage.rs +410 -0
- package/src/tests/storage/is_primitive_type.rs +48 -0
- package/src/tests/storage/mod.rs +16 -0
- package/src/tests/storage/parse_default.rs +164 -0
- package/src/tests/storage/parse_name.rs +158 -0
- package/src/tests/storage/parse_no_ttl_extension.rs +124 -0
- package/src/tests/storage/parse_storage_type.rs +174 -0
- package/src/tests/storage/snapshots/common_macros__tests__storage__generate_storage__snapshot_generated_storage_code.snap +412 -0
- package/src/tests/storage/storage_kind.rs +39 -0
- package/src/tests/storage/test_setup.rs +25 -0
- package/src/tests/storage/validate_attrs.rs +138 -0
- package/src/tests/storage/variant_config.rs +226 -0
- package/src/tests/test_helpers.rs +87 -0
- package/src/tests/ttl_configurable.rs +34 -0
- package/src/tests/ttl_extendable.rs +32 -0
- package/src/tests/upgradeable.rs +169 -0
- package/src/tests/utils.rs +267 -0
- package/src/ttl_configurable.rs +24 -0
- package/src/ttl_extendable.rs +28 -0
- package/src/upgradeable.rs +136 -0
- package/src/utils.rs +56 -0
package/src/rbac.rs
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
//! RBAC attribute macros for Stellar contracts.
|
|
2
|
+
//!
|
|
3
|
+
//! Provides `#[has_role]` and `#[only_role]` for role-based access control,
|
|
4
|
+
//! delegating to `utils::rbac::ensure_role`.
|
|
5
|
+
|
|
6
|
+
use crate::utils;
|
|
7
|
+
use proc_macro2::TokenStream;
|
|
8
|
+
use quote::{quote, ToTokens};
|
|
9
|
+
use syn::parse_quote;
|
|
10
|
+
use syn::{
|
|
11
|
+
parse::{Parse, ParseStream},
|
|
12
|
+
Expr, FnArg, Ident, ItemFn, Pat, Token, Type,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/// Helper that generates the role check for both `has_role` and `only_role`.
|
|
16
|
+
/// If `require_auth` is true, also injects `account.require_auth()`.
|
|
17
|
+
pub fn generate_role_check(args: TokenStream, input: TokenStream, require_auth: bool) -> TokenStream {
|
|
18
|
+
let HasRoleArgs { param, role } =
|
|
19
|
+
syn::parse2(args).unwrap_or_else(|e| panic!("failed to parse has_role/only_role args: {}", e));
|
|
20
|
+
let mut input_fn: ItemFn = syn::parse2(input).unwrap_or_else(|e| panic!("failed to parse function: {}", e));
|
|
21
|
+
|
|
22
|
+
let is_address_ref = validate_address_type(&input_fn, ¶m);
|
|
23
|
+
let param_ref = if is_address_ref { quote!(#param) } else { quote!(&#param) };
|
|
24
|
+
|
|
25
|
+
let env_param = utils::expect_env_param(&input_fn.sig.inputs);
|
|
26
|
+
let env_ref = env_param.as_ref_tokens();
|
|
27
|
+
|
|
28
|
+
// Insert the role check at the beginning of the function body
|
|
29
|
+
input_fn.block.stmts.insert(
|
|
30
|
+
0,
|
|
31
|
+
parse_quote!(utils::rbac::ensure_role::<Self>(#env_ref, &soroban_sdk::Symbol::new(#env_ref, #role), #param_ref);),
|
|
32
|
+
);
|
|
33
|
+
if require_auth {
|
|
34
|
+
input_fn.block.stmts.insert(1, parse_quote!(#param.require_auth();));
|
|
35
|
+
}
|
|
36
|
+
input_fn.into_token_stream()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
struct HasRoleArgs {
|
|
40
|
+
param: Ident,
|
|
41
|
+
role: Expr,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
impl Parse for HasRoleArgs {
|
|
45
|
+
fn parse(input: ParseStream) -> syn::Result<Self> {
|
|
46
|
+
// Parse the parameter name (the account identifier to check)
|
|
47
|
+
let param: Ident = input.parse()?;
|
|
48
|
+
// Expect a comma separator between param and role
|
|
49
|
+
input.parse::<Token![,]>()?;
|
|
50
|
+
// Parse the role expression (e.g., a string literal or constant)
|
|
51
|
+
let role: Expr = input.parse()?;
|
|
52
|
+
Ok(HasRoleArgs { param, role })
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Looks up `param_name` in the function signature and validates that its type
|
|
57
|
+
/// is `Address` or `&Address`. Returns `true` when the parameter is a reference,
|
|
58
|
+
/// so the caller knows whether an extra `&` is needed when forwarding it.
|
|
59
|
+
///
|
|
60
|
+
/// Panics at macro-expansion time if the parameter doesn't exist.
|
|
61
|
+
fn validate_address_type(func: &ItemFn, param_name: &Ident) -> bool {
|
|
62
|
+
for arg in &func.sig.inputs {
|
|
63
|
+
let FnArg::Typed(pat_type) = arg else { continue };
|
|
64
|
+
let Pat::Ident(pat_ident) = &*pat_type.pat else { continue };
|
|
65
|
+
if pat_ident.ident != *param_name {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
return match &*pat_type.ty {
|
|
69
|
+
Type::Reference(r) => {
|
|
70
|
+
assert_is_address(&r.elem, param_name);
|
|
71
|
+
true
|
|
72
|
+
}
|
|
73
|
+
ty => {
|
|
74
|
+
assert_is_address(ty, param_name);
|
|
75
|
+
false
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
panic!("Parameter `{param_name}` not found in function signature");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Asserts that the type path resolves to `Address`, panicking otherwise.
|
|
83
|
+
fn assert_is_address(ty: &Type, param_name: &Ident) {
|
|
84
|
+
let Type::Path(tp) = ty else {
|
|
85
|
+
panic!("Parameter `{param_name}` must be of type `Address` or `&Address`");
|
|
86
|
+
};
|
|
87
|
+
if tp.path.segments.last().is_none_or(|s| s.ident != "Address") {
|
|
88
|
+
panic!("Parameter `{param_name}` must be of type `Address` or `&Address`");
|
|
89
|
+
}
|
|
90
|
+
}
|
package/src/storage.rs
ADDED
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
//! Storage macro implementation for Stellar smart contracts.
|
|
2
|
+
//!
|
|
3
|
+
//! Generates strongly-typed storage API from enum variants with automatic TTL management.
|
|
4
|
+
|
|
5
|
+
use heck::ToSnakeCase;
|
|
6
|
+
use itertools::Itertools;
|
|
7
|
+
use proc_macro2::{Ident, TokenStream};
|
|
8
|
+
use quote::{format_ident, quote};
|
|
9
|
+
use syn::{Attribute, Expr, Fields, FieldsNamed, Meta, Type, Variant};
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Public API
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
/// Generates the storage API from the `#[storage]` attribute macro.
|
|
16
|
+
pub fn generate_storage(input: TokenStream) -> TokenStream {
|
|
17
|
+
let item_enum: syn::ItemEnum = syn::parse2(input).unwrap_or_else(|e| panic!("failed to parse enum: {}", e));
|
|
18
|
+
let enum_name = &item_enum.ident;
|
|
19
|
+
let vis = &item_enum.vis;
|
|
20
|
+
|
|
21
|
+
let variants: Vec<_> = item_enum.variants.iter().map(gen_enum_variant).collect();
|
|
22
|
+
let methods: Vec<_> = item_enum.variants.iter().map(|v| gen_accessor_methods(enum_name, v)).collect();
|
|
23
|
+
|
|
24
|
+
quote! {
|
|
25
|
+
#[soroban_sdk::contracttype]
|
|
26
|
+
#vis enum #enum_name { #(#variants,)* }
|
|
27
|
+
|
|
28
|
+
impl #enum_name { #(#methods)* }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Types
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
/// Storage kind: instance, persistent, or temporary.
|
|
37
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
38
|
+
enum StorageKind {
|
|
39
|
+
Instance,
|
|
40
|
+
Persistent,
|
|
41
|
+
Temporary,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
impl StorageKind {
|
|
45
|
+
fn name(self) -> &'static str {
|
|
46
|
+
match self {
|
|
47
|
+
Self::Instance => "instance",
|
|
48
|
+
Self::Persistent => "persistent",
|
|
49
|
+
Self::Temporary => "temporary",
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// Generates `env.storage().{kind}()`.
|
|
54
|
+
fn accessor(self) -> TokenStream {
|
|
55
|
+
let method = format_ident!("{}", self.name());
|
|
56
|
+
quote! { env.storage().#method() }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// Parsed configuration for a storage enum variant.
|
|
61
|
+
#[derive(Debug, Clone)]
|
|
62
|
+
struct VariantConfig {
|
|
63
|
+
name: String,
|
|
64
|
+
kind: StorageKind,
|
|
65
|
+
value_type: Type,
|
|
66
|
+
default_value: Option<Expr>,
|
|
67
|
+
auto_ttl: bool,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
impl VariantConfig {
|
|
71
|
+
/// Returns (getter, setter, remover, set_or_remove, has, ttl_extender) function names.
|
|
72
|
+
fn method_names(&self) -> (Ident, Ident, Ident, Ident, Ident, Ident) {
|
|
73
|
+
let base = &self.name;
|
|
74
|
+
(
|
|
75
|
+
format_ident!("{}", base),
|
|
76
|
+
format_ident!("set_{}", base),
|
|
77
|
+
format_ident!("remove_{}", base),
|
|
78
|
+
format_ident!("set_or_remove_{}", base),
|
|
79
|
+
format_ident!("has_{}", base),
|
|
80
|
+
format_ident!("extend_{}_ttl", base),
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ============================================================================
|
|
86
|
+
// Enum Variant Code Generation
|
|
87
|
+
// ============================================================================
|
|
88
|
+
|
|
89
|
+
/// Generates contracttype enum variant: `Variant` or `Variant(Type1, Type2, ...)`.
|
|
90
|
+
fn gen_enum_variant(variant: &Variant) -> TokenStream {
|
|
91
|
+
let name = &variant.ident;
|
|
92
|
+
match &variant.fields {
|
|
93
|
+
Fields::Unit => quote! { #name },
|
|
94
|
+
Fields::Named(FieldsNamed { named, .. }) => {
|
|
95
|
+
let types = named.iter().map(|f| &f.ty);
|
|
96
|
+
quote! { #name(#(#types),*) }
|
|
97
|
+
}
|
|
98
|
+
_ => panic!("only unit variants or named fields are supported in storage enums"),
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// Generates all storage accessor methods for a variant.
|
|
103
|
+
fn gen_accessor_methods(enum_name: &Ident, variant: &Variant) -> TokenStream {
|
|
104
|
+
let config = VariantConfig::try_from(variant)
|
|
105
|
+
.unwrap_or_else(|e| panic!("failed to parse storage variant for {}: {}", variant.ident, e));
|
|
106
|
+
|
|
107
|
+
let (getter, setter, remover, set_or_remove, has, ttl_extender) = config.method_names();
|
|
108
|
+
let params = gen_params(variant);
|
|
109
|
+
let args = gen_args(variant);
|
|
110
|
+
let key = gen_key(enum_name, variant);
|
|
111
|
+
let accessor = config.kind.accessor();
|
|
112
|
+
let value_type = &config.value_type;
|
|
113
|
+
|
|
114
|
+
// Auto TTL extension call — emitted after reads/writes for persistent storage.
|
|
115
|
+
// `Option<TokenStream>` integrates directly with `quote!` (None emits nothing).
|
|
116
|
+
let extend_ttl = config.auto_ttl.then(|| {
|
|
117
|
+
quote! { utils::ttl_configurable::extend_persistent_ttl(env, &key); }
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Getter: returns the value directly (with default) or wrapped in Option.
|
|
121
|
+
let (ret_type, ret_expr) = match &config.default_value {
|
|
122
|
+
Some(default) => (quote! { #value_type }, quote! { value.unwrap_or_else(|| #default) }),
|
|
123
|
+
None => (quote! { Option<#value_type> }, quote! { value }),
|
|
124
|
+
};
|
|
125
|
+
let ttl_on_get = extend_ttl.as_ref().map(|call| quote! { if value.is_some() { #call } });
|
|
126
|
+
|
|
127
|
+
// Has: conditionally extend TTL when key exists.
|
|
128
|
+
let has_body = match &extend_ttl {
|
|
129
|
+
Some(call) => quote! { let exists = #accessor.has(&key); if exists { #call } exists },
|
|
130
|
+
None => quote! { #accessor.has(&key) },
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// TTL extender method — only for persistent/temporary storage (instance has no per-key TTL).
|
|
134
|
+
let ttl_extender_method = (config.kind != StorageKind::Instance).then(|| {
|
|
135
|
+
quote! {
|
|
136
|
+
pub fn #ttl_extender(#params, threshold: u32, extend_to: u32) {
|
|
137
|
+
let key = #key;
|
|
138
|
+
#accessor.extend_ttl(&key, threshold, extend_to);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
quote! {
|
|
144
|
+
pub fn #getter(#params) -> #ret_type {
|
|
145
|
+
let key = #key;
|
|
146
|
+
let value = #accessor.get::<_, #value_type>(&key);
|
|
147
|
+
#ttl_on_get
|
|
148
|
+
#ret_expr
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
pub fn #setter(#params, value: &#value_type) {
|
|
152
|
+
let key = #key;
|
|
153
|
+
#accessor.set(&key, value);
|
|
154
|
+
#extend_ttl
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
pub fn #remover(#params) {
|
|
158
|
+
let key = #key;
|
|
159
|
+
#accessor.remove(&key);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
pub fn #set_or_remove(#params, value: &Option<#value_type>) {
|
|
163
|
+
match value.as_ref() {
|
|
164
|
+
Some(v) => Self::#setter(#args, v),
|
|
165
|
+
None => Self::#remover(#args),
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
pub fn #has(#params) -> bool {
|
|
170
|
+
let key = #key;
|
|
171
|
+
#has_body
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#ttl_extender_method
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ============================================================================
|
|
179
|
+
// Parameter & Key Generation Helpers
|
|
180
|
+
// ============================================================================
|
|
181
|
+
|
|
182
|
+
/// Extracts (name, type) pairs from variant fields.
|
|
183
|
+
fn extract_fields(variant: &Variant) -> Vec<(&Ident, &Type)> {
|
|
184
|
+
match &variant.fields {
|
|
185
|
+
Fields::Unit => vec![],
|
|
186
|
+
Fields::Named(named) => named.named.iter().map(|f| (f.ident.as_ref().unwrap(), &f.ty)).collect(),
|
|
187
|
+
_ => panic!("only unit variants or named fields are supported in storage enums"),
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// Generates function parameters: `env: &Env` or `env: &Env, field1: Type1, ...`.
|
|
192
|
+
fn gen_params(variant: &Variant) -> TokenStream {
|
|
193
|
+
let fields = extract_fields(variant);
|
|
194
|
+
if fields.is_empty() {
|
|
195
|
+
quote! { env: &soroban_sdk::Env }
|
|
196
|
+
} else {
|
|
197
|
+
let params = fields.iter().map(|(name, ty)| {
|
|
198
|
+
if is_primitive_type(ty) {
|
|
199
|
+
quote! { #name: #ty }
|
|
200
|
+
} else {
|
|
201
|
+
quote! { #name: &#ty }
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
quote! { env: &soroban_sdk::Env, #(#params),* }
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/// Generates function arguments: `env` or `env, field1, field2, ...`.
|
|
209
|
+
fn gen_args(variant: &Variant) -> TokenStream {
|
|
210
|
+
let fields = extract_fields(variant);
|
|
211
|
+
if fields.is_empty() {
|
|
212
|
+
quote! { env }
|
|
213
|
+
} else {
|
|
214
|
+
let names = fields.iter().map(|(name, _)| name);
|
|
215
|
+
quote! { env, #(#names),* }
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/// Generates storage key: `Enum::Variant` or `Enum::Variant(field1.clone(), ...)`.
|
|
220
|
+
fn gen_key(enum_name: &Ident, variant: &Variant) -> TokenStream {
|
|
221
|
+
let variant_ident = &variant.ident;
|
|
222
|
+
let fields = extract_fields(variant);
|
|
223
|
+
|
|
224
|
+
if fields.is_empty() {
|
|
225
|
+
quote! { #enum_name::#variant_ident }
|
|
226
|
+
} else {
|
|
227
|
+
let args = fields.iter().map(|(name, ty)| {
|
|
228
|
+
if is_primitive_type(ty) {
|
|
229
|
+
quote! { #name }
|
|
230
|
+
} else {
|
|
231
|
+
quote! { #name.clone() }
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
quote! { #enum_name::#variant_ident(#(#args),*) }
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/// Checks if a type is a primitive (pass-by-value) type.
|
|
239
|
+
fn is_primitive_type(ty: &Type) -> bool {
|
|
240
|
+
// https://developers.stellar.org/docs/learn/fundamentals/contract-development/types/built-in-types#primitive-types
|
|
241
|
+
const PRIMITIVES: &[&str] = &["u32", "i32", "u64", "i64", "u128", "i128", "bool"];
|
|
242
|
+
matches!(ty, Type::Path(p) if p.path.segments.len() == 1
|
|
243
|
+
&& PRIMITIVES.contains(&p.path.segments[0].ident.to_string().as_str()))
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ============================================================================
|
|
247
|
+
// Attribute Parsing
|
|
248
|
+
// ============================================================================
|
|
249
|
+
|
|
250
|
+
/// Known attributes for storage variants ("doc" allows /// comments).
|
|
251
|
+
const KNOWN_ATTRS: &[&str] = &["doc", "instance", "persistent", "temporary", "default", "name", "no_ttl_extension"];
|
|
252
|
+
|
|
253
|
+
impl TryFrom<&Variant> for VariantConfig {
|
|
254
|
+
type Error = String;
|
|
255
|
+
|
|
256
|
+
fn try_from(variant: &Variant) -> Result<Self, Self::Error> {
|
|
257
|
+
let attrs = &variant.attrs;
|
|
258
|
+
validate_attrs(attrs, &variant.ident)?;
|
|
259
|
+
|
|
260
|
+
let (kind, value_type) = parse_storage_type(attrs)?;
|
|
261
|
+
let default_value = parse_default(attrs)?;
|
|
262
|
+
let name = parse_name(attrs)?.unwrap_or_else(|| variant.ident.to_string().to_snake_case());
|
|
263
|
+
let no_ttl_extension = parse_no_ttl_extension(attrs)?;
|
|
264
|
+
|
|
265
|
+
if no_ttl_extension && kind != StorageKind::Persistent {
|
|
266
|
+
return Err("#[no_ttl_extension] can only be used with #[persistent(...)] storage".to_string());
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
Ok(Self {
|
|
270
|
+
name,
|
|
271
|
+
kind,
|
|
272
|
+
value_type,
|
|
273
|
+
default_value,
|
|
274
|
+
auto_ttl: kind == StorageKind::Persistent && !no_ttl_extension,
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
fn validate_attrs(attrs: &[Attribute], variant_ident: &Ident) -> Result<(), String> {
|
|
280
|
+
for attr in attrs {
|
|
281
|
+
let path = attr.path();
|
|
282
|
+
let name = path.get_ident().map(|i| i.to_string()).unwrap_or_else(|| quote!(#path).to_string());
|
|
283
|
+
|
|
284
|
+
if !KNOWN_ATTRS.contains(&name.as_str()) {
|
|
285
|
+
return Err(format!(
|
|
286
|
+
"unknown attribute '{}' on variant '{}'. Supported attributes are: {}",
|
|
287
|
+
name,
|
|
288
|
+
variant_ident,
|
|
289
|
+
KNOWN_ATTRS.join(", ")
|
|
290
|
+
));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
Ok(())
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
fn parse_storage_type(attrs: &[Attribute]) -> Result<(StorageKind, Type), String> {
|
|
297
|
+
attrs
|
|
298
|
+
.iter()
|
|
299
|
+
.filter_map(|attr| {
|
|
300
|
+
let ident = attr.path().get_ident()?;
|
|
301
|
+
let kind = match ident.to_string().as_str() {
|
|
302
|
+
"instance" => StorageKind::Instance,
|
|
303
|
+
"persistent" => StorageKind::Persistent,
|
|
304
|
+
"temporary" => StorageKind::Temporary,
|
|
305
|
+
_ => return None,
|
|
306
|
+
};
|
|
307
|
+
let value_type = attr
|
|
308
|
+
.parse_args::<Type>()
|
|
309
|
+
.unwrap_or_else(|e| panic!("failed to parse storage type for #[{}(...)] : {}", ident, e));
|
|
310
|
+
Some((kind, value_type))
|
|
311
|
+
})
|
|
312
|
+
.exactly_one()
|
|
313
|
+
.map_err(|e| {
|
|
314
|
+
format!(
|
|
315
|
+
"storage type must be specified exactly once as \
|
|
316
|
+
'#[instance(Type)]', '#[persistent(Type)]', or '#[temporary(Type)]': {}",
|
|
317
|
+
e
|
|
318
|
+
)
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
fn parse_default(attrs: &[Attribute]) -> Result<Option<Expr>, String> {
|
|
323
|
+
attrs
|
|
324
|
+
.iter()
|
|
325
|
+
.filter(|attr| attr.path().is_ident("default"))
|
|
326
|
+
.at_most_one()
|
|
327
|
+
.map_err(|e| format!("multiple default values specified: {}", e))?
|
|
328
|
+
.map(|attr| attr.parse_args::<Expr>())
|
|
329
|
+
.transpose()
|
|
330
|
+
.map_err(|e| format!("failed to parse default value: {}", e))
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
fn parse_name(attrs: &[Attribute]) -> Result<Option<String>, String> {
|
|
334
|
+
attrs
|
|
335
|
+
.iter()
|
|
336
|
+
.filter(|attr| attr.path().is_ident("name"))
|
|
337
|
+
.at_most_one()
|
|
338
|
+
.map_err(|e| format!("multiple name attributes specified: {}", e))?
|
|
339
|
+
.map(|attr| attr.parse_args::<syn::LitStr>().map(|lit| lit.value()))
|
|
340
|
+
.transpose()
|
|
341
|
+
.map_err(|e| format!("failed to parse name attribute: {}", e))
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
fn parse_no_ttl_extension(attrs: &[Attribute]) -> Result<bool, String> {
|
|
345
|
+
let attr = attrs
|
|
346
|
+
.iter()
|
|
347
|
+
.filter(|attr| attr.path().is_ident("no_ttl_extension"))
|
|
348
|
+
.at_most_one()
|
|
349
|
+
.map_err(|e| format!("multiple #[no_ttl_extension] attributes specified: {}", e))?;
|
|
350
|
+
|
|
351
|
+
// Reject `#[no_ttl_extension(...)]` / `#[no_ttl_extension = ...]`
|
|
352
|
+
match attr {
|
|
353
|
+
None => Ok(false),
|
|
354
|
+
Some(attr) if matches!(attr.meta, Meta::Path(_)) => Ok(true),
|
|
355
|
+
Some(_) => Err("#[no_ttl_extension] does not accept arguments".to_string()),
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ============================================================================
|
|
360
|
+
// Test-only Functions
|
|
361
|
+
// ============================================================================
|
|
362
|
+
|
|
363
|
+
#[cfg(test)]
|
|
364
|
+
pub(crate) mod test {
|
|
365
|
+
use super::*;
|
|
366
|
+
|
|
367
|
+
// ========================================================================
|
|
368
|
+
// VariantConfigInfo - wrapper struct for test access
|
|
369
|
+
// ========================================================================
|
|
370
|
+
|
|
371
|
+
/// Test-only wrapper struct exposing VariantConfig data without exposing the type.
|
|
372
|
+
#[derive(Debug, Clone)]
|
|
373
|
+
pub struct VariantConfigInfo {
|
|
374
|
+
pub name: String,
|
|
375
|
+
pub kind_name: String,
|
|
376
|
+
pub value_type: String,
|
|
377
|
+
pub has_default: bool,
|
|
378
|
+
pub auto_ttl: bool,
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ========================================================================
|
|
382
|
+
// StorageKind wrapper functions
|
|
383
|
+
// ========================================================================
|
|
384
|
+
|
|
385
|
+
/// Returns the name for Instance storage kind.
|
|
386
|
+
pub fn storage_kind_instance_name() -> &'static str {
|
|
387
|
+
StorageKind::Instance.name()
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/// Returns the name for Persistent storage kind.
|
|
391
|
+
pub fn storage_kind_persistent_name() -> &'static str {
|
|
392
|
+
StorageKind::Persistent.name()
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/// Returns the name for Temporary storage kind.
|
|
396
|
+
pub fn storage_kind_temporary_name() -> &'static str {
|
|
397
|
+
StorageKind::Temporary.name()
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/// Returns the accessor TokenStream for Instance storage kind.
|
|
401
|
+
pub fn storage_kind_instance_accessor() -> TokenStream {
|
|
402
|
+
StorageKind::Instance.accessor()
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/// Returns the accessor TokenStream for Persistent storage kind.
|
|
406
|
+
pub fn storage_kind_persistent_accessor() -> TokenStream {
|
|
407
|
+
StorageKind::Persistent.accessor()
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/// Returns the accessor TokenStream for Temporary storage kind.
|
|
411
|
+
pub fn storage_kind_temporary_accessor() -> TokenStream {
|
|
412
|
+
StorageKind::Temporary.accessor()
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ========================================================================
|
|
416
|
+
// VariantConfig wrapper functions
|
|
417
|
+
// ========================================================================
|
|
418
|
+
|
|
419
|
+
/// Gets VariantConfig info from a Variant.
|
|
420
|
+
pub fn get_variant_config_for_test(variant: &Variant) -> Result<VariantConfigInfo, String> {
|
|
421
|
+
VariantConfig::try_from(variant).map(|c| {
|
|
422
|
+
let value_type = c.value_type;
|
|
423
|
+
VariantConfigInfo {
|
|
424
|
+
name: c.name,
|
|
425
|
+
kind_name: c.kind.name().to_string(),
|
|
426
|
+
value_type: quote!(#value_type).to_string(),
|
|
427
|
+
has_default: c.default_value.is_some(),
|
|
428
|
+
auto_ttl: c.auto_ttl,
|
|
429
|
+
}
|
|
430
|
+
})
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/// Gets method names from a Variant as strings.
|
|
434
|
+
pub fn get_variant_method_names_for_test(
|
|
435
|
+
variant: &Variant,
|
|
436
|
+
) -> Result<(String, String, String, String, String, String), String> {
|
|
437
|
+
VariantConfig::try_from(variant).map(|c| {
|
|
438
|
+
let (getter, setter, remover, set_or_remove, has, ttl_extender) = c.method_names();
|
|
439
|
+
(
|
|
440
|
+
getter.to_string(),
|
|
441
|
+
setter.to_string(),
|
|
442
|
+
remover.to_string(),
|
|
443
|
+
set_or_remove.to_string(),
|
|
444
|
+
has.to_string(),
|
|
445
|
+
ttl_extender.to_string(),
|
|
446
|
+
)
|
|
447
|
+
})
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ========================================================================
|
|
451
|
+
// parse_storage_type wrapper - returns kind name instead of enum
|
|
452
|
+
// ========================================================================
|
|
453
|
+
|
|
454
|
+
/// Test-only wrapper for parse_storage_type that returns kind name as string.
|
|
455
|
+
pub fn parse_storage_type_for_test(attrs: &[Attribute]) -> Result<(String, Type), String> {
|
|
456
|
+
parse_storage_type(attrs).map(|(kind, ty)| (kind.name().to_string(), ty))
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ========================================================================
|
|
460
|
+
// Other wrapper functions
|
|
461
|
+
// ========================================================================
|
|
462
|
+
|
|
463
|
+
/// Test-only wrapper for is_primitive_type.
|
|
464
|
+
pub fn is_primitive_type_for_test(ty: &Type) -> bool {
|
|
465
|
+
is_primitive_type(ty)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/// Test-only wrapper for extract_fields.
|
|
469
|
+
pub fn extract_fields_for_test(variant: &Variant) -> Vec<(&Ident, &Type)> {
|
|
470
|
+
extract_fields(variant)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/// Test-only wrapper for gen_params.
|
|
474
|
+
pub fn gen_params_for_test(variant: &Variant) -> TokenStream {
|
|
475
|
+
gen_params(variant)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/// Test-only wrapper for gen_args.
|
|
479
|
+
pub fn gen_args_for_test(variant: &Variant) -> TokenStream {
|
|
480
|
+
gen_args(variant)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/// Test-only wrapper for gen_key.
|
|
484
|
+
pub fn gen_key_for_test(enum_name: &Ident, variant: &Variant) -> TokenStream {
|
|
485
|
+
gen_key(enum_name, variant)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/// Test-only wrapper for gen_enum_variant.
|
|
489
|
+
pub fn gen_enum_variant_for_test(variant: &Variant) -> TokenStream {
|
|
490
|
+
gen_enum_variant(variant)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/// Test-only wrapper for gen_accessor_methods.
|
|
494
|
+
pub fn gen_accessor_methods_for_test(enum_name: &Ident, variant: &Variant) -> TokenStream {
|
|
495
|
+
gen_accessor_methods(enum_name, variant)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/// Test-only wrapper for validate_attrs.
|
|
499
|
+
pub fn validate_attrs_for_test(attrs: &[Attribute], variant_ident: &Ident) -> Result<(), String> {
|
|
500
|
+
validate_attrs(attrs, variant_ident)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/// Test-only wrapper for parse_default.
|
|
504
|
+
pub fn parse_default_for_test(attrs: &[Attribute]) -> Result<Option<Expr>, String> {
|
|
505
|
+
parse_default(attrs)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/// Test-only wrapper for parse_name.
|
|
509
|
+
pub fn parse_name_for_test(attrs: &[Attribute]) -> Result<Option<String>, String> {
|
|
510
|
+
parse_name(attrs)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/// Test-only wrapper for parse_no_ttl_extension.
|
|
514
|
+
pub fn parse_no_ttl_extension_for_test(attrs: &[Attribute]) -> Result<bool, String> {
|
|
515
|
+
parse_no_ttl_extension(attrs)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/// Returns the list of known attributes for testing.
|
|
519
|
+
pub fn known_attrs_for_test() -> &'static [&'static str] {
|
|
520
|
+
KNOWN_ATTRS
|
|
521
|
+
}
|
|
522
|
+
}
|