diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index bf2a9b750d..fb59879c1d 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -89,7 +89,8 @@ jobs: - name: Run cargo test run: cargo test coverage: - runs-on: ubuntu-latest + runs-on: + group: 8-cores_32GB_Ubuntu Group timeout-minutes: 90 env: SCCACHE_GHA_ENABLED: "true" diff --git a/Cargo.toml b/Cargo.toml index 52b230c39b..e4617910c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ members = [ "module-system/sov-modules-macros", "module-system/sov-state", "module-system/sov-modules-api", + "module-system/module-schemas", "module-system/utils/sov-first-read-last-write-cache", "module-system/module-implementations/sov-accounts", "module-system/module-implementations/sov-bank", @@ -73,6 +74,7 @@ derive_more = "0.99.11" clap = { version = "4.2.7", features = ["derive"] } toml = "0.7.3" jsonrpsee = "0.16.2" +schemars = { version = "0.8.12", features = ["derive"] } tempfile = "3.5" tokio = { version = "1", features = ["full"] } diff --git a/module-system/README.md b/module-system/README.md index 0ab47330be..e5979c4d53 100644 --- a/module-system/README.md +++ b/module-system/README.md @@ -189,7 +189,7 @@ pub trait Context: Spec + Clone + Debug + PartialEq { } ``` -Modules are expected to be generic over the `Context` type. This trait gives them a convenient handle to access all of the cryptographic operations +Modules are expected to be generic over the `Context` type. If a module is generic over multiple type parameters, then the type bound over `Context` is always on the *first* of those type parameters. The `Context` trait gives them a convenient handle to access all of the cryptographic operations defined by a `Spec`, while also making it easy for the Module System to pass in authenticated transaction-specific information which would not otherwise be available to a module. Currently, a `Context` is only required to contain the `sender` (signer) of the transaction, but this trait might be extended in the future. diff --git a/module-system/module-implementations/sov-bank/Cargo.toml b/module-system/module-implementations/sov-bank/Cargo.toml index a0d1db27c5..f777baf137 100644 --- a/module-system/module-implementations/sov-bank/Cargo.toml +++ b/module-system/module-implementations/sov-bank/Cargo.toml @@ -13,6 +13,7 @@ resolver = "2" [dependencies] anyhow = { workspace = true } +schemars = { workspace = true, optional = true } sov-modules-api = { path = "../../sov-modules-api", version = "0.1", default-features = false } sov-modules-macros = { path = "../../sov-modules-macros", version = "0.1" } sov-state = { path = "../../sov-state", version = "0.1", default-features = false } @@ -31,4 +32,4 @@ tempfile = { workspace = true } [features] default = ["native"] serde = ["dep:serde", "dep:serde_json"] -native = ["serde", "sov-state/native", "dep:jsonrpsee", "sov-modules-api/native"] +native = ["serde", "sov-state/native", "dep:jsonrpsee", "sov-modules-api/native", "dep:schemars"] diff --git a/module-system/module-implementations/sov-bank/src/call.rs b/module-system/module-implementations/sov-bank/src/call.rs index c1effcab28..9e8ce2e7bf 100644 --- a/module-system/module-implementations/sov-bank/src/call.rs +++ b/module-system/module-implementations/sov-bank/src/call.rs @@ -8,10 +8,15 @@ use crate::{Amount, Bank, Coins, Token}; #[cfg_attr( feature = "native", derive(serde::Serialize), - derive(serde::Deserialize) + derive(serde::Deserialize), + derive(schemars::JsonSchema), + schemars(bound = "C::Address: ::schemars::JsonSchema", rename = "CallMessage") )] #[derive(borsh::BorshDeserialize, borsh::BorshSerialize, Debug, PartialEq, Clone)] -pub enum CallMessage { +pub enum CallMessage +where + C: sov_modules_api::Context, +{ /// Creates a new token with the specified name and initial balance. CreateToken { /// Random value use to create a unique token address. diff --git a/module-system/module-implementations/sov-bank/src/lib.rs b/module-system/module-implementations/sov-bank/src/lib.rs index 9fba1bd0e5..fb0c43306d 100644 --- a/module-system/module-implementations/sov-bank/src/lib.rs +++ b/module-system/module-implementations/sov-bank/src/lib.rs @@ -28,6 +28,7 @@ pub struct BankConfig { /// - Token creation. /// - Token transfers. /// - Token burn. +#[cfg_attr(feature = "native", derive(sov_modules_macros::ModuleCallJsonSchema))] #[derive(ModuleInfo, Clone)] pub struct Bank { /// The address of the sov-bank module. diff --git a/module-system/module-implementations/sov-bank/src/token.rs b/module-system/module-implementations/sov-bank/src/token.rs index d945894b26..24bf82fcba 100644 --- a/module-system/module-implementations/sov-bank/src/token.rs +++ b/module-system/module-implementations/sov-bank/src/token.rs @@ -11,7 +11,9 @@ pub type Amount = u64; #[cfg_attr( feature = "native", derive(serde::Serialize), - derive(serde::Deserialize) + derive(serde::Deserialize), + derive(schemars::JsonSchema), + schemars(bound = "C::Address: ::schemars::JsonSchema", rename = "Coins") )] #[derive(borsh::BorshDeserialize, borsh::BorshSerialize, Debug, PartialEq, Clone)] pub struct Coins { diff --git a/module-system/module-schemas/Cargo.toml b/module-system/module-schemas/Cargo.toml new file mode 100644 index 0000000000..656eac5c36 --- /dev/null +++ b/module-system/module-schemas/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "sov-module-schemas" +description = "A dummy crate that stores as files the JSON Schemas for all Sovereign modules" +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +rust-version = { workspace = true } +version = { workspace = true } +readme = "README.md" +resolver = "2" + +[build-dependencies] +sov-modules-api = { path = "../sov-modules-api" } +sov-bank = { path = "../module-implementations/sov-bank" } diff --git a/module-system/module-schemas/README.md b/module-system/module-schemas/README.md new file mode 100644 index 0000000000..2781bfc5a0 --- /dev/null +++ b/module-system/module-schemas/README.md @@ -0,0 +1,3 @@ +# `sov-module-schemas` + +This crate is not intended to be used directly, but serves as generated documentation in the form of JSON Schemas for modules built-in into the SDK. The schemas are tracked by `git` to make them linkable and searchable on GitHub. diff --git a/module-system/module-schemas/build.rs b/module-system/module-schemas/build.rs new file mode 100644 index 0000000000..f019f64226 --- /dev/null +++ b/module-system/module-schemas/build.rs @@ -0,0 +1,16 @@ +use std::fs::File; +use std::io::{self, Write}; + +use sov_modules_api::default_context::DefaultContext as C; +use sov_modules_api::ModuleCallJsonSchema; + +fn main() -> io::Result<()> { + store_json_schema::>("sov-bank.json")?; + Ok(()) +} + +fn store_json_schema(filename: &str) -> io::Result<()> { + let mut file = File::create(format!("schemas/{}", filename))?; + file.write_all(M::json_schema().as_bytes())?; + Ok(()) +} diff --git a/module-system/module-schemas/schemas/sov-bank.json b/module-system/module-schemas/schemas/sov-bank.json new file mode 100644 index 0000000000..d367a8439b --- /dev/null +++ b/module-system/module-schemas/schemas/sov-bank.json @@ -0,0 +1,219 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CallMessage", + "description": "This enumeration represents the available call messages for interacting with the sov-bank module.", + "oneOf": [ + { + "description": "Creates a new token with the specified name and initial balance.", + "type": "object", + "required": [ + "CreateToken" + ], + "properties": { + "CreateToken": { + "type": "object", + "required": [ + "authorized_minters", + "initial_balance", + "minter_address", + "salt", + "token_name" + ], + "properties": { + "authorized_minters": { + "description": "Authorized minter list.", + "type": "array", + "items": { + "$ref": "#/definitions/Address" + } + }, + "initial_balance": { + "description": "The initial balance of the new token.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "minter_address": { + "description": "The address of the account that the new tokens are minted to.", + "allOf": [ + { + "$ref": "#/definitions/Address" + } + ] + }, + "salt": { + "description": "Random value use to create a unique token address.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "token_name": { + "description": "The name of the new token.", + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Transfers a specified amount of tokens to the specified address.", + "type": "object", + "required": [ + "Transfer" + ], + "properties": { + "Transfer": { + "type": "object", + "required": [ + "coins", + "to" + ], + "properties": { + "coins": { + "description": "The amount of tokens to transfer.", + "allOf": [ + { + "$ref": "#/definitions/Coins" + } + ] + }, + "to": { + "description": "The address to which the tokens will be transferred.", + "allOf": [ + { + "$ref": "#/definitions/Address" + } + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Burns a specified amount of tokens.", + "type": "object", + "required": [ + "Burn" + ], + "properties": { + "Burn": { + "type": "object", + "required": [ + "coins" + ], + "properties": { + "coins": { + "description": "The amount of tokens to burn.", + "allOf": [ + { + "$ref": "#/definitions/Coins" + } + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Mints a specified amount of tokens.", + "type": "object", + "required": [ + "Mint" + ], + "properties": { + "Mint": { + "type": "object", + "required": [ + "coins", + "minter_address" + ], + "properties": { + "coins": { + "description": "The amount of tokens to mint.", + "allOf": [ + { + "$ref": "#/definitions/Coins" + } + ] + }, + "minter_address": { + "description": "Address to mint tokens to", + "allOf": [ + { + "$ref": "#/definitions/Address" + } + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Freeze a token so that the supply is frozen", + "type": "object", + "required": [ + "Freeze" + ], + "properties": { + "Freeze": { + "type": "object", + "required": [ + "token_address" + ], + "properties": { + "token_address": { + "description": "Address of the token to be frozen", + "allOf": [ + { + "$ref": "#/definitions/Address" + } + ] + } + } + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Address": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "maxItems": 32, + "minItems": 32 + } + } + }, + "Coins": { + "type": "object", + "required": [ + "amount", + "token_address" + ], + "properties": { + "amount": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "token_address": { + "$ref": "#/definitions/Address" + } + } + } + } +} \ No newline at end of file diff --git a/module-system/module-schemas/src/lib.rs b/module-system/module-schemas/src/lib.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/module-system/module-schemas/src/lib.rs @@ -0,0 +1 @@ + diff --git a/module-system/sov-modules-api/Cargo.toml b/module-system/sov-modules-api/Cargo.toml index 885a9fef17..67188aa0c8 100644 --- a/module-system/sov-modules-api/Cargo.toml +++ b/module-system/sov-modules-api/Cargo.toml @@ -23,6 +23,7 @@ sha2 = { workspace = true } bech32 = { workspace = true } derive_more = { workspace = true } serde_json = { workspace = true } +schemars = { workspace = true, optional = true, features = [] } ed25519-dalek = { version = "1.0.1", default-features = false, features = ["alloc", "u64_backend"] } rand = { version = "0.7", optional = true } @@ -33,4 +34,4 @@ serde_json = { workspace = true } [features] default = ["native"] -native = ["sov-state/native", "rand", "hex", "ed25519-dalek/default"] +native = ["sov-state/native", "rand", "hex", "schemars", "ed25519-dalek/default"] diff --git a/module-system/sov-modules-api/src/lib.rs b/module-system/sov-modules-api/src/lib.rs index ae07ef7c75..89e643b6e9 100644 --- a/module-system/sov-modules-api/src/lib.rs +++ b/module-system/sov-modules-api/src/lib.rs @@ -37,6 +37,7 @@ impl AsRef<[u8]> for Address { impl AddressTrait for Address {} +#[cfg_attr(feature = "native", derive(schemars::JsonSchema))] #[derive(PartialEq, Clone, Eq, borsh::BorshDeserialize, borsh::BorshSerialize)] pub struct Address { addr: [u8; 32], @@ -122,6 +123,9 @@ pub trait Spec { type Address: AddressTrait + BorshSerialize + BorshDeserialize + // Do we always need this, even when the module does not have a JSON + // Schema? That feels a bit wrong. + + ::schemars::JsonSchema + Into + From; @@ -235,6 +239,18 @@ pub trait Module { } } +/// A [`Module`] that has a well-defined and known [JSON +/// Schema](https://json-schema.org/) for its [`Module::CallMessage`]. +/// +/// This trait is intended to support code generation tools, CLIs, and +/// documentation. You can derive it with `#[derive(ModuleCallJsonSchema)]`, or +/// implement it manually if your use case demands more control over the JSON +/// Schema generation. +pub trait ModuleCallJsonSchema: Module { + /// Returns the JSON schema for [`Module::CallMessage`]. + fn json_schema() -> String; +} + /// Every module has to implement this trait. pub trait ModuleInfo: Default { type Context: Context; diff --git a/module-system/sov-modules-macros/Cargo.toml b/module-system/sov-modules-macros/Cargo.toml index f5a1efb7ef..1f604e19ac 100644 --- a/module-system/sov-modules-macros/Cargo.toml +++ b/module-system/sov-modules-macros/Cargo.toml @@ -20,21 +20,23 @@ name = "tests" path = "tests/all_tests.rs" [dev-dependencies] +serde_json = "1" +jsonrpsee = { workspace = true, features = ["macros", "http-client", "server"] } +tempfile = { workspace = true } trybuild = "1.0" + sov-modules-api = { path = "../sov-modules-api", version = "0.1", default-features = false } -jsonrpsee = { workspace = true, features = ["macros", "http-client", "server"] } sov-state = { path = "../sov-state", version = "0.1", default-features = false } -tempfile = { workspace = true } +sov-bank = { path = "../module-implementations/sov-bank", version = "0.1" } [dependencies] anyhow = { workspace = true } +borsh = { workspace = true } jsonrpsee = { workspace = true, features = ["http-client", "server"] } -sov-modules-api = { path = "../sov-modules-api", version = "0.1", default-features = false } -sov-rollup-interface = { path = "../../rollup-interface", version = "0.1" } - - -syn = { version = "1.0", features = ["full"] } -quote = "1.0" proc-macro2 = "1.0" -borsh = { workspace = true } +quote = "1.0" +schemars = { workspace = true } +syn = { version = "1.0", features = ["full"] } +sov-modules-api = { path = "../sov-modules-api", version = "0.1", default-features = false } +sov-rollup-interface = { path = "../../rollup-interface", version = "0.1" } diff --git a/module-system/sov-modules-macros/src/common.rs b/module-system/sov-modules-macros/src/common.rs index b4761f9f62..1a35835ad3 100644 --- a/module-system/sov-modules-macros/src/common.rs +++ b/module-system/sov-modules-macros/src/common.rs @@ -1,4 +1,4 @@ -use proc_macro2::{Ident, TokenStream}; +use proc_macro2::{Ident, Span, TokenStream}; use quote::{format_ident, ToTokens}; use syn::{DataStruct, GenericParam, Generics, ImplGenerics, Meta, TypeGenerics, WhereClause}; @@ -123,9 +123,16 @@ impl<'a> StructDef<'a> { } } -/// Gets type parameter from `Generics` declaration. -pub(crate) fn parse_generic_params(generics: &Generics) -> Result { - let generic_param = match generics.params.first().unwrap() { +/// Gets the type parameter's identifier from [`syn::Generics`]. +pub(crate) fn get_generics_type_param( + generics: &Generics, + error_span: Span, +) -> Result { + let generic_param = match generics + .params + .first() + .ok_or_else(|| syn::Error::new(error_span, "No generic parameters found"))? + { GenericParam::Type(ty) => &ty.ident, GenericParam::Lifetime(lf) => { return Err(syn::Error::new_spanned( diff --git a/module-system/sov-modules-macros/src/dispatch/dispatch_call.rs b/module-system/sov-modules-macros/src/dispatch/dispatch_call.rs index d44ce13e20..5da10e0a15 100644 --- a/module-system/sov-modules-macros/src/dispatch/dispatch_call.rs +++ b/module-system/sov-modules-macros/src/dispatch/dispatch_call.rs @@ -1,7 +1,8 @@ +use proc_macro2::Span; use syn::DeriveInput; use crate::common::{ - get_serialization_attrs, parse_generic_params, StructDef, StructFieldExtractor, CALL, + get_generics_type_param, get_serialization_attrs, StructDef, StructFieldExtractor, CALL, }; impl<'a> StructDef<'a> { @@ -110,7 +111,7 @@ impl DispatchCallMacro { .. } = input; - let generic_param = parse_generic_params(&generics)?; + let generic_param = get_generics_type_param(&generics, Span::call_site())?; let (impl_generics, type_generics, where_clause) = generics.split_for_impl(); let fields = self.field_extractor.get_fields_from_struct(&data)?; diff --git a/module-system/sov-modules-macros/src/dispatch/genesis.rs b/module-system/sov-modules-macros/src/dispatch/genesis.rs index b421483457..949f12f8b2 100644 --- a/module-system/sov-modules-macros/src/dispatch/genesis.rs +++ b/module-system/sov-modules-macros/src/dispatch/genesis.rs @@ -1,7 +1,7 @@ -use proc_macro2::Ident; +use proc_macro2::{Ident, Span}; use syn::{DeriveInput, TypeGenerics}; -use crate::common::{parse_generic_params, StructFieldExtractor, StructNamedField}; +use crate::common::{get_generics_type_param, StructFieldExtractor, StructNamedField}; pub(crate) struct GenesisMacro { field_extractor: StructFieldExtractor, @@ -28,7 +28,7 @@ impl GenesisMacro { let (impl_generics, type_generics, where_clause) = generics.split_for_impl(); let fields = self.field_extractor.get_fields_from_struct(&data)?; - let generic_param = parse_generic_params(&generics)?; + let generic_param = get_generics_type_param(&generics, Span::call_site())?; let genesis_config = Self::make_genesis_config(&fields, &type_generics, &generic_param); let genesis_fn_body = Self::make_genesis_fn_body(&fields); diff --git a/module-system/sov-modules-macros/src/dispatch/message_codec.rs b/module-system/sov-modules-macros/src/dispatch/message_codec.rs index bcb534fec7..11c34ff16d 100644 --- a/module-system/sov-modules-macros/src/dispatch/message_codec.rs +++ b/module-system/sov-modules-macros/src/dispatch/message_codec.rs @@ -1,8 +1,8 @@ -use proc_macro2::TokenStream; +use proc_macro2::{Span, TokenStream}; use quote::format_ident; use syn::DeriveInput; -use crate::common::{parse_generic_params, StructDef, StructFieldExtractor, CALL}; +use crate::common::{get_generics_type_param, StructDef, StructFieldExtractor, CALL}; impl<'a> StructDef<'a> { fn create_message_codec(&self) -> TokenStream { @@ -66,7 +66,7 @@ impl MessageCodec { .. } = input; - let generic_param = parse_generic_params(&generics)?; + let generic_param = get_generics_type_param(&generics, Span::call_site())?; let (impl_generics, type_generics, where_clause) = generics.split_for_impl(); let fields = self.field_extractor.get_fields_from_struct(&data)?; diff --git a/module-system/sov-modules-macros/src/lib.rs b/module-system/sov-modules-macros/src/lib.rs index 35b0d037fe..df448a1818 100644 --- a/module-system/sov-modules-macros/src/lib.rs +++ b/module-system/sov-modules-macros/src/lib.rs @@ -1,8 +1,13 @@ +//! Procedural macros to assist in the creation of Sovereign modules. + +#![deny(missing_docs)] #![feature(log_syntax)] + mod cli_parser; mod common; mod default_runtime; mod dispatch; +mod module_call_json_schema; mod module_info; mod rpc; @@ -11,35 +16,49 @@ use default_runtime::DefaultRuntimeMacro; use dispatch::dispatch_call::DispatchCallMacro; use dispatch::genesis::GenesisMacro; use dispatch::message_codec::MessageCodec; +use module_call_json_schema::derive_module_call_json_schema; use proc_macro::TokenStream; use rpc::ExposeRpcMacro; use syn::parse_macro_input; -/// Derives the `sov-modules-api::ModuleInfo` implementation for the underlying type. +/// Derives the [`sov_modules_api::ModuleInfo`] trait for the underlying `struct`. +/// +/// The underlying type must respect the following conditions, or compilation +/// will fail: +/// - It must be a named `struct`. Tuple `struct`s, `enum`s, and others are +/// not supported. +/// - It must have *exactly one* field with the `#[address]` attribute. This field +/// represents the **module address**. +/// - All other fields must have either the `#[state]` or `#[module]` attribute. +/// - `#[state]` is used for state members. +/// - `#[module]` is used for module members. +/// +/// In addition to implementing [`sov_modules_api::ModuleInfo`], this macro will +/// also generate so-called "prefix" methods. See the [`sov_modules_api`] docs +/// for more information about prefix methods. /// -/// See `sov-modules-api` for definition of `prefix`. /// ## Example /// -/// ``` ignore -/// #[derive(ModuleInfo)] -/// pub(crate) struct TestModule { -/// #[state] -/// pub test_state1: TestState, +/// ``` +/// use sov_modules_macros::ModuleInfo; +/// use sov_modules_api::{Context, ModuleInfo}; +/// use sov_state::StateMap; +/// +/// #[derive(ModuleInfo)] +/// struct TestModule { +/// #[address] +/// admin: C::Address, /// /// #[state] -/// pub test_state2: TestState, -/// } +/// pub state_map: StateMap, +/// } +/// +/// // You can then get the prefix of `state_map` like this: +/// fn get_prefix(some_storage: C::Storage) { +/// let test_struct = TestModule::::default(); +/// let prefix1 = test_struct.state_map.prefix(); +/// } /// ``` -/// allows getting a prefix of a member field like: -/// ```ignore -/// let test_struct = as sov_modules_api::ModuleInfo>::new(some_storage); -/// let prefix1 = test_struct.test_state1.prefix; -/// ```` -/// ## Attributes -/// -/// * `state` - attribute for state members -/// * `module` - attribute for module members -/// * `address` - attribute for module address #[proc_macro_derive(ModuleInfo, attributes(state, module, address))] pub fn module_info(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input); @@ -59,7 +78,8 @@ pub fn default_runtime(input: TokenStream) -> TokenStream { handle_macro_error(default_config_macro.derive_default_runtime(input)) } -/// Derives the `sov-modules-api::Genesis` implementation for the underlying type. +/// Derives the [`sov_modules_api::Genesis`] trait for the underlying runtime +/// `struct`. #[proc_macro_derive(Genesis)] pub fn genesis(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input); @@ -68,7 +88,7 @@ pub fn genesis(input: TokenStream) -> TokenStream { handle_macro_error(genesis_macro.derive_genesis(input)) } -/// Derives the `sov-modules-api::DispatchCall` implementation for the underlying type. +/// Derives the [`sov_modules_api::DispatchCall`] trait for the underlying type. #[proc_macro_derive(DispatchCall, attributes(serialization))] pub fn dispatch_call(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input); @@ -77,6 +97,42 @@ pub fn dispatch_call(input: TokenStream) -> TokenStream { handle_macro_error(call_macro.derive_dispatch_call(input)) } +/// Derives the [`sov_modules_api::ModuleCallJsonSchema`] trait for the underlying type. +/// +/// ## Example +/// +/// ``` +/// use std::marker::PhantomData; +/// +/// use sov_modules_api::{Context, Module, ModuleInfo, ModuleCallJsonSchema}; +/// use sov_modules_api::default_context::ZkDefaultContext; +/// use sov_modules_macros::{ModuleInfo, ModuleCallJsonSchema}; +/// use sov_state::StateMap; +/// use sov_bank::call::CallMessage; +/// +/// #[derive(ModuleInfo, ModuleCallJsonSchema)] +/// struct TestModule { +/// #[address] +/// admin: C::Address, +/// +/// #[state] +/// pub state_map: StateMap, +/// } +/// +/// impl Module for TestModule { +/// type Context = C; +/// type Config = PhantomData; +/// type CallMessage = CallMessage; +/// } +/// +/// println!("JSON Schema: {}", TestModule::::json_schema()); +/// ``` +#[proc_macro_derive(ModuleCallJsonSchema)] +pub fn module_call_json_schema(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input); + handle_macro_error(derive_module_call_json_schema(input)) +} + /// Adds encoding functionality to the underlying type. #[proc_macro_derive(MessageCodec)] pub fn codec(input: TokenStream) -> TokenStream { @@ -86,7 +142,7 @@ pub fn codec(input: TokenStream) -> TokenStream { handle_macro_error(codec_macro.derive_message_codec(input)) } -/// Derive a `jsonrpsee` implementation for the underlying type. Any code relying on this macro +/// 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"]`. /// /// Syntax is identical to `jsonrpsee`'s `#[rpc]` execept that: @@ -95,42 +151,58 @@ pub fn codec(input: TokenStream) -> TokenStream { /// 3. `#[method]` is renamed to with `#[rpc_method]` to avoid import confusion and clarify the purpose of the annotation /// /// ## Example -/// ```rust,ignore -/// struct MyModule {}; +/// ``` +/// use sov_modules_macros::{rpc_gen, ModuleInfo}; +/// use sov_modules_api::Context; /// -/// #[rpc_gen(client, server, namespace ="myNamespace")] -/// impl MyModule { -/// #[rpc_method(name = "myMethod")] +/// #[derive(ModuleInfo)] +/// struct MyModule { +/// #[address] +/// addr: C::Address, +/// // ... +/// } +/// +/// #[rpc_gen(client, server, namespace = "myNamespace")] +/// impl MyModule { +/// #[rpc_method(name = "myMethod")] /// fn my_method(&self, param: u32) -> u32 { -/// 1 +/// 1 /// } /// } /// ``` /// /// This is exactly equivalent to hand-writing -/// ```rust,ignore +/// +/// ``` +/// use sov_modules_macros::{rpc_gen, ModuleInfo}; +/// use sov_modules_api::Context; +/// use sov_state::WorkingSet; +/// +/// #[derive(ModuleInfo)] /// struct MyModule { -/// ... +/// #[address] +/// addr: C::Address, +/// // ... /// }; /// -/// impl MyModule { +/// impl MyModule { /// fn my_method(&self, working_set: &mut WorkingSet, param: u32) -> u32 { /// 1 /// } /// } /// -/// #[jsonrpsee::rpc(client, server, namespace ="myNamespace")] +/// #[jsonrpsee::proc_macros::rpc(client, server, namespace ="myNamespace")] /// pub trait MyModuleRpc { -/// #[jsonrpsee::method(name = "myMethod")] -/// fn my_method(&self, param: u32) -> Result; -/// #[method(name = "health")] -/// fn health() -> Result<(), jsonrpsee::Error> { -/// Ok(()) -/// } +/// #[method(name = "myMethod")] +/// fn my_method(&self, param: u32) -> Result; +/// +/// #[method(name = "health")] +/// fn health(&self) -> Result<(), jsonrpsee::core::Error> { +/// Ok(()) +/// } /// } /// ``` /// -/// /// This proc macro also generates an implementation trait intended to be used by a Runtime struct. This trait /// is named `MyModuleRpcImpl`, and allows a Runtime to be converted into a functional RPC server /// by simply implementing the two required methods - `get_backing_impl(&self) -> MyModule` and `get_working_set(&self) -> ::sov_modules_api::WorkingSet` @@ -169,6 +241,22 @@ pub fn expose_rpc(attr: TokenStream, input: TokenStream) -> TokenStream { handle_macro_error(expose_macro.generate_rpc(original, input, context_type)) } +/// Generates a CLI arguments parser for the specified runtime. +/// +/// ## Examples +/// ``` +/// use sov_modules_api::Context; +/// use sov_modules_api::default_context::DefaultContext; +/// use sov_modules_macros::{DispatchCall, MessageCodec, cli_parser}; +/// +/// #[derive(DispatchCall, MessageCodec)] +/// #[serialization(borsh::BorshDeserialize, borsh::BorshSerialize)] +/// #[cli_parser(DefaultContext)] +/// pub struct Runtime { +/// pub bank: sov_bank::Bank, +/// // ... +/// } +/// ``` #[proc_macro_attribute] pub fn cli_parser(attr: TokenStream, input: TokenStream) -> TokenStream { let context_type = parse_macro_input!(attr); diff --git a/module-system/sov-modules-macros/src/module_call_json_schema.rs b/module-system/sov-modules-macros/src/module_call_json_schema.rs new file mode 100644 index 0000000000..c18ed5d851 --- /dev/null +++ b/module-system/sov-modules-macros/src/module_call_json_schema.rs @@ -0,0 +1,26 @@ +use syn::DeriveInput; + +pub fn derive_module_call_json_schema( + input: DeriveInput, +) -> Result { + let DeriveInput { + ident, generics, .. + } = input; + + let (impl_generics, type_generics, where_clause) = generics.split_for_impl(); + + let tokens = quote::quote! { + use ::schemars::JsonSchema; + + impl #impl_generics ::sov_modules_api::ModuleCallJsonSchema for #ident #type_generics #where_clause { + fn json_schema() -> ::std::string::String { + let schema = ::schemars::schema_for!( + ::CallMessage + ); + ::serde_json::to_string_pretty(&schema).expect("Failed to serialize JSON schema; this is a bug in the module") + } + } + }; + + Ok(tokens.into()) +} diff --git a/module-system/sov-modules-macros/src/module_info.rs b/module-system/sov-modules-macros/src/module_info.rs index b1ab094c86..2cddf204c5 100644 --- a/module-system/sov-modules-macros/src/module_info.rs +++ b/module-system/sov-modules-macros/src/module_info.rs @@ -1,7 +1,7 @@ use proc_macro2::{self, Ident, Span}; use syn::{DataStruct, DeriveInput, ImplGenerics, PathArguments, TypeGenerics, WhereClause}; -use crate::common::parse_generic_params; +use crate::common::get_generics_type_param; #[derive(Clone)] struct StructNamedField { @@ -38,7 +38,7 @@ pub(crate) fn derive_module_info( .. } = input; - let generic_param = parse_generic_params(&generics)?; + let generic_param = get_generics_type_param(&generics, Span::call_site())?; let (impl_generics, type_generics, where_clause) = generics.split_for_impl(); let fields = get_fields_from_struct(&data); diff --git a/module-system/sov-modules-macros/tests/all_tests.rs b/module-system/sov-modules-macros/tests/all_tests.rs index fb82347054..b5aaca92ed 100644 --- a/module-system/sov-modules-macros/tests/all_tests.rs +++ b/module-system/sov-modules-macros/tests/all_tests.rs @@ -3,12 +3,13 @@ fn module_info_tests() { let t = trybuild::TestCases::new(); t.pass("tests/module_info/parse.rs"); t.pass("tests/module_info/mod_and_state.rs"); + t.compile_fail("tests/module_info/derive_on_enum_not_supported.rs"); t.compile_fail("tests/module_info/field_missing_attribute.rs"); + t.compile_fail("tests/module_info/missing_address.rs"); + t.compile_fail("tests/module_info/no_generics.rs"); t.compile_fail("tests/module_info/not_supported_attribute.rs"); - t.compile_fail("tests/module_info/derive_on_enum_not_supported.rs"); t.compile_fail("tests/module_info/not_supported_type.rs"); t.compile_fail("tests/module_info/second_addr_not_supported.rs"); - t.compile_fail("tests/module_info/missing_address.rs"); } #[test] diff --git a/module-system/sov-modules-macros/tests/module_info/no_generics.rs b/module-system/sov-modules-macros/tests/module_info/no_generics.rs new file mode 100644 index 0000000000..853addfe29 --- /dev/null +++ b/module-system/sov-modules-macros/tests/module_info/no_generics.rs @@ -0,0 +1,6 @@ +use sov_modules_macros::ModuleInfo; + +#[derive(ModuleInfo)] +struct TestStruct {} + +fn main() {} diff --git a/module-system/sov-modules-macros/tests/module_info/no_generics.stderr b/module-system/sov-modules-macros/tests/module_info/no_generics.stderr new file mode 100644 index 0000000000..e4d4f997b2 --- /dev/null +++ b/module-system/sov-modules-macros/tests/module_info/no_generics.stderr @@ -0,0 +1,7 @@ +error: No generic parameters found + --> tests/module_info/no_generics.rs:3:10 + | +3 | #[derive(ModuleInfo)] + | ^^^^^^^^^^ + | + = note: this error originates in the derive macro `ModuleInfo` (in Nightly builds, run with -Z macro-backtrace for more info)