Skip to content

Commit

Permalink
New ModuleCallJsonSchema macro to generate JSON Schemas via `schema…
Browse files Browse the repository at this point in the history
…rs` (#517)

* Introduce `ModuleCallJsonSchema` trait

* Refactor `get_generics_type_param`

* Add `ModuleInfo` test

* Allow `#[derive(ModuleCallJsonSchema)]`

* Impl `ModuleCallJsonSchema` for `Bank`

* New crate `sov-module-schemas`

* `#![deny(missing_docs)]` for `sov_modules_macros`

* Improve macro hygiene

* Clearer docs on module type generics

* ci: run coverage on bigger machine
  • Loading branch information
neysofu authored and citizen-stig committed Jul 20, 2023
1 parent 836253b commit 0e35f53
Show file tree
Hide file tree
Showing 25 changed files with 493 additions and 71 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"] }

Expand Down
2 changes: 1 addition & 1 deletion module-system/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion module-system/module-implementations/sov-bank/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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"]
9 changes: 7 additions & 2 deletions module-system/module-implementations/sov-bank/src/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<C: sov_modules_api::Context> {
pub enum CallMessage<C>
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.
Expand Down
1 change: 1 addition & 0 deletions module-system/module-implementations/sov-bank/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub struct BankConfig<C: sov_modules_api::Context> {
/// - Token creation.
/// - Token transfers.
/// - Token burn.
#[cfg_attr(feature = "native", derive(sov_modules_macros::ModuleCallJsonSchema))]
#[derive(ModuleInfo, Clone)]
pub struct Bank<C: sov_modules_api::Context> {
/// The address of the sov-bank module.
Expand Down
4 changes: 3 additions & 1 deletion module-system/module-implementations/sov-bank/src/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<C: sov_modules_api::Context> {
Expand Down
16 changes: 16 additions & 0 deletions module-system/module-schemas/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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" }
3 changes: 3 additions & 0 deletions module-system/module-schemas/README.md
Original file line number Diff line number Diff line change
@@ -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.
16 changes: 16 additions & 0 deletions module-system/module-schemas/build.rs
Original file line number Diff line number Diff line change
@@ -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::Bank<C>>("sov-bank.json")?;
Ok(())
}

fn store_json_schema<M: ModuleCallJsonSchema>(filename: &str) -> io::Result<()> {
let mut file = File::create(format!("schemas/{}", filename))?;
file.write_all(M::json_schema().as_bytes())?;
Ok(())
}
219 changes: 219 additions & 0 deletions module-system/module-schemas/schemas/sov-bank.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
}
1 change: 1 addition & 0 deletions module-system/module-schemas/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

3 changes: 2 additions & 1 deletion module-system/sov-modules-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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"]
16 changes: 16 additions & 0 deletions module-system/sov-modules-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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<AddressBech32>
+ From<AddressBech32>;

Expand Down Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 0e35f53

Please sign in to comment.