Skip to content

Commit

Permalink
Allow setting constants via manifest (#1030)
Browse files Browse the repository at this point in the history
* Add constants to manifest

* fmt

* fix nit

* clippy

* Add custom parsing for consts
  • Loading branch information
preston-evans98 authored Oct 12, 2023
1 parent 0890fd1 commit 484a0e3
Show file tree
Hide file tree
Showing 8 changed files with 301 additions and 31 deletions.
28 changes: 23 additions & 5 deletions constants.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,29 @@
"comment": "Sovereign SDK constants",
"gas": {
"Bank": {
"create_token": [4, 4],
"transfer": [5, 5],
"burn": [2, 2],
"mint": [2, 2],
"freeze": [1, 1]
"create_token": [
4,
4
],
"transfer": [
5,
5
],
"burn": [
2,
2
],
"mint": [
2,
2
],
"freeze": [
1,
1
]
}
},
"constants": {
"TEST_U32": 42
}
}
2 changes: 2 additions & 0 deletions module-system/sov-modules-api/src/reexport_macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ pub use sov_modules_macros::ModuleInfo;
/// Procedural macros to assist with creating new modules.
#[cfg(feature = "macros")]
pub mod macros {
/// Sets the value of a constant at compile time by reading from the Manifest file.
pub use sov_modules_macros::config_constant;
/// The macro exposes RPC endpoints from all modules in the runtime.
/// It gets storage from the Context generic
/// and utilizes output of [`#[rpc_gen]`] macro to generate RPC methods.
Expand Down
11 changes: 11 additions & 0 deletions module-system/sov-modules-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ mod cli_parser;
mod common;
mod default_runtime;
mod dispatch;
mod make_constants;
mod manifest;
mod module_call_json_schema;
mod module_info;
Expand All @@ -28,6 +29,7 @@ use default_runtime::DefaultRuntimeMacro;
use dispatch::dispatch_call::DispatchCallMacro;
use dispatch::genesis::GenesisMacro;
use dispatch::message_codec::MessageCodec;
use make_constants::{make_const, PartialItemConst};
use module_call_json_schema::derive_module_call_json_schema;
use new_types::address_type_helper;
use offchain::offchain_generator;
Expand Down Expand Up @@ -82,6 +84,15 @@ pub fn codec(input: TokenStream) -> TokenStream {
handle_macro_error(codec_macro.derive_message_codec(input))
}

/// Sets a constant from the manifest file instead of hard-coding it inline.
#[proc_macro_attribute]
pub fn config_constant(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as PartialItemConst);
handle_macro_error(
make_const(&input.ident, &input.ty, input.vis, &input.attrs).map(|ok| ok.into()),
)
}

/// Derives a [`jsonrpsee`] implementation for the underlying type. Any code relying on this macro
/// must take jsonrpsee as a dependency with at least the following features enabled: `["macros", "client-core", "server"]`.
///
Expand Down
38 changes: 38 additions & 0 deletions module-system/sov-modules-macros/src/make_constants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use syn::parse::Parse;
use syn::{Attribute, Ident, Token, Type, Visibility};

use crate::manifest::Manifest;

/// A partial const declaration: `const MAX: u16;`.
pub struct PartialItemConst {
pub attrs: Vec<Attribute>,
pub vis: Visibility,
pub const_token: Token![const],
pub ident: Ident,
pub colon_token: Token![:],
pub ty: Type,
pub semi_token: Token![;],
}

impl Parse for PartialItemConst {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
Ok(Self {
attrs: Attribute::parse_outer(input)?,
vis: input.parse()?,
const_token: input.parse()?,
ident: input.parse()?,
colon_token: input.parse()?,
ty: input.parse()?,
semi_token: input.parse()?,
})
}
}

pub(crate) fn make_const(
field_ident: &Ident,
ty: &Type,
vis: syn::Visibility,
attrs: &[syn::Attribute],
) -> Result<proc_macro2::TokenStream, syn::Error> {
Manifest::read_constants(field_ident)?.parse_constant(ty, field_ident, vis, attrs)
}
137 changes: 111 additions & 26 deletions module-system/sov-modules-macros/src/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ use std::path::{Path, PathBuf};
use std::{env, fmt, fs, ops};

use proc_macro2::{Ident, TokenStream};
use serde_json::Value;
use quote::format_ident;
use serde_json::{Map, Value};
use syn::{PathArguments, Type, TypePath};

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -48,12 +49,19 @@ impl<'a> Manifest<'a> {
/// The `parent` is used to report the errors to the correct span location.
pub fn read_constants(parent: &'a Ident) -> Result<Self, syn::Error> {
let manifest = "constants.json";
let initial_path = match env::var("CONSTANTS_MANIFEST") {
let initial_path = match env::var("CONSTANTS_MANIFEST"){
Ok(p) if p.is_empty() => {
Err(Self::err(
&p,
parent,
"failed to read target path for sovereign manifest file: env var `CONSTANTS_MANIFEST` was set to the empty string".to_string(),
))
},
Ok(p) => PathBuf::from(&p).canonicalize().map_err(|e| {
Self::err(
&p,
parent,
format!("failed access base dir for sovereign manifest file `{p}`: {e}"),
format!("failed to canonicalize path for sovereign manifest file from env var `{p}`: {e}"),
)
}),
Err(_) => {
Expand Down Expand Up @@ -112,6 +120,29 @@ impl<'a> Manifest<'a> {
Self::read_str(manifest, path, parent)
}

/// Gets the requested object from the manifest by key
fn get_object(&self, field: &Ident, key: &str) -> Result<&Map<String, Value>, syn::Error> {
self.value
.as_object()
.ok_or_else(|| Self::err(&self.path, field, "manifest is not an object"))?
.get(key)
.ok_or_else(|| {
Self::err(
&self.path,
field,
format!("manifest does not contain a `{key}` attribute"),
)
})?
.as_object()
.ok_or_else(|| {
Self::err(
&self.path,
field,
format!("`{key}` attribute of `{field}` is not an object"),
)
})
}

/// Parses a gas config constant from the manifest file. Returns a `TokenStream` with the
/// following structure:
///
Expand All @@ -127,28 +158,7 @@ impl<'a> Manifest<'a> {
/// The `gas` field resolution will first attempt to query `gas.parent`, and then fallback to
/// `gas`. They must be objects with arrays of integers as fields.
pub fn parse_gas_config(&self, ty: &Type, field: &Ident) -> Result<TokenStream, syn::Error> {
let map = self
.value
.as_object()
.ok_or_else(|| Self::err(&self.path, field, "manifest is not an object"))?;

let root = map
.get("gas")
.ok_or_else(|| {
Self::err(
&self.path,
field,
"manifest does not contain a `gas` attribute",
)
})?
.as_object()
.ok_or_else(|| {
Self::err(
&self.path,
field,
format!("`gas` attribute of `{}` is not an object", field),
)
})?;
let root = self.get_object(field, "gas")?;

let root = match root.get(&self.parent.to_string()) {
Some(Value::Object(m)) => m,
Expand All @@ -168,7 +178,7 @@ impl<'a> Manifest<'a> {
Self::err(
&self.path,
field,
format!("failed to parse key attribyte `{}`: {}", k, e),
format!("failed to parse key attribute `{}`: {}", k, e),
)
})?;

Expand Down Expand Up @@ -236,6 +246,81 @@ impl<'a> Manifest<'a> {
})
}

pub fn parse_constant(
&self,
ty: &Type,
field: &Ident,
vis: syn::Visibility,
attrs: &[syn::Attribute],
) -> Result<TokenStream, syn::Error> {
let root = self.get_object(field, "constants")?;
let value = root.get(&field.to_string()).ok_or_else(|| {
Self::err(
&self.path,
field,
format!("manifest does not contain a `{}` attribute", field),
)
})?;
let value = self.value_to_tokens(field, value, ty)?;
let output = quote::quote! {
#(#attrs)*
#vis const #field: #ty = #value;
};
Ok(output)
}

fn value_to_tokens(
&self,
field: &Ident,
value: &serde_json::Value,
ty: &Type,
) -> Result<TokenStream, syn::Error> {
match value {
Value::Null => Err(Self::err(
&self.path,
field,
format!("`{}` is `null`", field),
)),
Value::Bool(b) => Ok(quote::quote!(#b)),
Value::Number(n) => {
if n.is_u64() {
let n = n.as_u64().unwrap();
Ok(quote::quote!(#n as #ty))
} else if n.is_i64() {
let n = n.as_i64().unwrap();
Ok(quote::quote!(#n as #ty))
} else {
Err(Self::err(&self.path, field, "All numeric values must be representable as 64 bit integers during parsing.".to_string()))
}
}
Value::String(s) => Ok(quote::quote!(#s)),
Value::Array(arr) => {
let mut values = Vec::with_capacity(arr.len());
let ty = if let Type::Array(ty) = ty {
&ty.elem
} else {
return Err(Self::err(
&self.path,
field,
format!(
"Found value of type {:?} while parsing `{}` but expected an array type ",
ty, field
),
));
};
for (idx, value) in arr.iter().enumerate() {
values.push(self.value_to_tokens(
&format_ident!("{field}_{idx}"),
value,
ty,
)?);
}
Ok(quote::quote!([#(#values,)*]))
}
Value::Object(_) => todo!(),
}
}

fn err<P, T>(path: P, ident: &syn::Ident, msg: T) -> syn::Error
where
P: AsRef<Path>,
Expand Down
13 changes: 13 additions & 0 deletions module-system/sov-modules-macros/tests/all_tests.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::path::PathBuf;

#[test]
fn module_info_tests() {
let t = trybuild::TestCases::new();
Expand Down Expand Up @@ -46,3 +48,14 @@ fn cli_wallet_arg_tests() {
t.pass("tests/cli_wallet_arg/derive_enum_unnamed_fields.rs");
t.pass("tests/cli_wallet_arg/derive_wallet.rs");
}

#[test]
fn constants_from_manifests_test() {
let t: trybuild::TestCases = trybuild::TestCases::new();
let manifest_dir = std::env::var_os("CARGO_MANIFEST_DIR").unwrap();
std::env::set_var(
"CONSTANTS_MANIFEST",
PathBuf::from(manifest_dir).join("tests"),
);
t.pass("tests/constants/create_constant.rs");
}
78 changes: 78 additions & 0 deletions module-system/sov-modules-macros/tests/constants.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
"comment": "Sovereign SDK constants",
"constants": {
"TEST_U32": 42,
"TEST_BOOL": true,
"TEST_STRING": "Some Other String",
"TEST_NESTED_ARRAY": [
[
7,
7,
7
],
[
7,
7,
7
]
],
"TEST_ARRAY_OF_U8": [
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
11,
11
]
},
"gas": {
"Bank": {
"create_token": [
4,
4
],
"transfer": [
5,
5
],
"burn": [
2,
2
],
"mint": [
2,
2
],
"freeze": [
1,
1
]
}
}
}
Loading

0 comments on commit 484a0e3

Please sign in to comment.