use core::fmt::{Display, Formatter, Result as DisplayResult, Write};
use semver::Version;
use serde::{Serialize, Serializer};
use serde_json::{Map, Value};
use url::Url;
const METADATA_VERSION: &str = "0.1.0";
#[derive(Debug, Serialize)]
pub struct ContractMetadata {
#[serde(rename = "metadataVersion")]
metadata_version: semver::Version,
source: Source,
contract: Contract,
#[serde(skip_serializing_if = "Option::is_none")]
user: Option<User>,
#[serde(flatten)]
abi: Map<String, Value>,
}
impl ContractMetadata {
pub fn new(
source: Source,
contract: Contract,
user: Option<User>,
abi: Map<String, Value>,
) -> Self {
let metadata_version = semver::Version::parse(METADATA_VERSION)
.expect("METADATA_VERSION is a valid semver string");
Self {
metadata_version,
source,
contract,
user,
abi,
}
}
}
#[derive(Debug, Serialize)]
pub struct Source {
#[serde(serialize_with = "serialize_as_byte_str")]
hash: [u8; 32],
language: SourceLanguage,
compiler: SourceCompiler,
}
impl Source {
pub fn new(hash: [u8; 32], language: SourceLanguage, compiler: SourceCompiler) -> Self {
Source {
hash,
language,
compiler,
}
}
}
#[derive(Debug)]
pub struct SourceLanguage {
language: Language,
version: Version,
}
impl SourceLanguage {
pub fn new(language: Language, version: Version) -> Self {
SourceLanguage { language, version }
}
}
impl Serialize for SourceLanguage {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl Display for SourceLanguage {
fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult {
write!(f, "{} {}", self.language, self.version)
}
}
#[derive(Debug)]
pub enum Language {
Ink,
Solidity,
AssemblyScript,
}
impl Display for Language {
fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult {
match self {
Self::Ink => write!(f, "ink!"),
Self::Solidity => write!(f, "Solidity"),
Self::AssemblyScript => write!(f, "AssemblyScript"),
}
}
}
#[derive(Debug)]
pub struct SourceCompiler {
compiler: Compiler,
version: Version,
}
impl Display for SourceCompiler {
fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult {
write!(f, "{} {}", self.compiler, self.version)
}
}
impl Serialize for SourceCompiler {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl SourceCompiler {
pub fn new(compiler: Compiler, version: Version) -> Self {
SourceCompiler { compiler, version }
}
}
#[derive(Debug, Serialize)]
pub enum Compiler {
RustC,
Solang,
}
impl Display for Compiler {
fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult {
match self {
Self::RustC => write!(f, "rustc"),
Self::Solang => write!(f, "solang"),
}
}
}
#[derive(Debug, Serialize)]
pub struct Contract {
name: String,
version: Version,
authors: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
documentation: Option<Url>,
#[serde(skip_serializing_if = "Option::is_none")]
repository: Option<Url>,
#[serde(skip_serializing_if = "Option::is_none")]
homepage: Option<Url>,
#[serde(skip_serializing_if = "Option::is_none")]
license: Option<String>,
}
impl Contract {
pub fn builder() -> ContractBuilder {
ContractBuilder::default()
}
}
#[derive(Debug, Serialize)]
pub struct User {
#[serde(flatten)]
json: Map<String, Value>,
}
impl User {
pub fn new(json: Map<String, Value>) -> Self {
User { json }
}
}
#[derive(Default)]
pub struct ContractBuilder {
name: Option<String>,
version: Option<Version>,
authors: Option<Vec<String>>,
description: Option<String>,
documentation: Option<Url>,
repository: Option<Url>,
homepage: Option<Url>,
license: Option<String>,
}
impl ContractBuilder {
pub fn name<S>(&mut self, name: S) -> &mut Self
where
S: AsRef<str>,
{
if self.name.is_some() {
panic!("name has already been set")
}
self.name = Some(name.as_ref().to_string());
self
}
pub fn version(&mut self, version: Version) -> &mut Self {
if self.version.is_some() {
panic!("version has already been set")
}
self.version = Some(version);
self
}
pub fn authors<I, S>(&mut self, authors: I) -> &mut Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
if self.authors.is_some() {
panic!("authors has already been set")
}
let authors = authors
.into_iter()
.map(|s| s.as_ref().to_string())
.collect::<Vec<_>>();
if authors.len() == 0 {
panic!("must have at least one author")
}
self.authors = Some(authors);
self
}
pub fn description<S>(&mut self, description: S) -> &mut Self
where
S: AsRef<str>,
{
if self.description.is_some() {
panic!("description has already been set")
}
self.description = Some(description.as_ref().to_string());
self
}
pub fn documentation(&mut self, documentation: Url) -> &mut Self {
if self.documentation.is_some() {
panic!("documentation is already set")
}
self.documentation = Some(documentation);
self
}
pub fn repository(&mut self, repository: Url) -> &mut Self {
if self.repository.is_some() {
panic!("repository is already set")
}
self.repository = Some(repository);
self
}
pub fn homepage(&mut self, homepage: Url) -> &mut Self {
if self.homepage.is_some() {
panic!("homepage is already set")
}
self.homepage = Some(homepage);
self
}
pub fn license<S>(&mut self, license: S) -> &mut Self
where
S: AsRef<str>,
{
if self.license.is_some() {
panic!("license has already been set")
}
self.license = Some(license.as_ref().to_string());
self
}
pub fn build(&self) -> Result<Contract, String> {
let mut required = Vec::new();
if let (Some(name), Some(version), Some(authors)) =
(&self.name, &self.version, &self.authors)
{
Ok(Contract {
name: name.to_string(),
version: version.clone(),
authors: authors.to_vec(),
description: self.description.clone(),
documentation: self.documentation.clone(),
repository: self.repository.clone(),
homepage: self.homepage.clone(),
license: self.license.clone(),
})
} else {
if self.name.is_none() {
required.push("name");
}
if self.version.is_none() {
required.push("version")
}
if self.authors.is_none() {
required.push("authors")
}
Err(format!(
"Missing required non-default fields: {}",
required.join(", ")
))
}
}
}
fn serialize_as_byte_str<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
if bytes.is_empty() {
return serializer.serialize_str("");
}
let mut hex = String::with_capacity(bytes.len() * 2 + 2);
write!(hex, "0x").expect("failed writing to string");
for byte in bytes {
write!(hex, "{:02x}", byte).expect("failed writing to string");
}
serializer.serialize_str(&hex)
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use serde_json::json;
#[test]
fn builder_fails_with_missing_required_fields() {
let missing_name = Contract::builder()
.version(Version::new(2, 1, 0))
.authors(vec!["Parity Technologies <admin@parity.io>".to_string()])
.build();
assert_eq!(
missing_name.unwrap_err(),
"Missing required non-default fields: name"
);
let missing_version = Contract::builder()
.name("incrementer".to_string())
.authors(vec!["Parity Technologies <admin@parity.io>".to_string()])
.build();
assert_eq!(
missing_version.unwrap_err(),
"Missing required non-default fields: version"
);
let missing_authors = Contract::builder()
.name("incrementer".to_string())
.version(Version::new(2, 1, 0))
.build();
assert_eq!(
missing_authors.unwrap_err(),
"Missing required non-default fields: authors"
);
let missing_all = Contract::builder()
.build();
assert_eq!(
missing_all.unwrap_err(),
"Missing required non-default fields: name, version, authors"
);
}
#[test]
fn json_with_optional_fields() {
let language = SourceLanguage::new(Language::Ink, Version::new(2, 1, 0));
let compiler =
SourceCompiler::new(Compiler::RustC, Version::parse("1.46.0-nightly").unwrap());
let source = Source::new([0u8; 32], language, compiler);
let contract = Contract::builder()
.name("incrementer".to_string())
.version(Version::new(2, 1, 0))
.authors(vec!["Parity Technologies <admin@parity.io>".to_string()])
.description("increment a value".to_string())
.documentation(Url::parse("http://docs.rs/").unwrap())
.repository(Url::parse("http://github.com/paritytech/ink/").unwrap())
.homepage(Url::parse("http://example.com/").unwrap())
.license("Apache-2.0".to_string())
.build()
.unwrap();
let user_json = json! {
{
"more-user-provided-fields": [
"and",
"their",
"values"
],
"some-user-provided-field": "and-its-value"
}
};
let user = User::new(user_json.as_object().unwrap().clone());
let abi_json = json! {
{
"spec": {},
"storage": {},
"types": []
}
}
.as_object()
.unwrap()
.clone();
let metadata = ContractMetadata::new(source, contract, Some(user), abi_json);
let json = serde_json::to_value(&metadata).unwrap();
let expected = json! {
{
"metadataVersion": "0.1.0",
"source": {
"hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"language": "ink! 2.1.0",
"compiler": "rustc 1.46.0-nightly"
},
"contract": {
"name": "incrementer",
"version": "2.1.0",
"authors": [
"Parity Technologies <admin@parity.io>"
],
"description": "increment a value",
"documentation": "http://docs.rs/",
"repository": "http://github.com/paritytech/ink/",
"homepage": "http://example.com/",
"license": "Apache-2.0",
},
"user": {
"more-user-provided-fields": [
"and",
"their",
"values"
],
"some-user-provided-field": "and-its-value"
},
"spec": {},
"storage": {},
"types": []
}
};
assert_eq!(json, expected);
}
#[test]
fn json_excludes_optional_fields() {
let language = SourceLanguage::new(Language::Ink, Version::new(2, 1, 0));
let compiler =
SourceCompiler::new(Compiler::RustC, Version::parse("1.46.0-nightly").unwrap());
let source = Source::new([0u8; 32], language, compiler);
let contract = Contract::builder()
.name("incrementer".to_string())
.version(Version::new(2, 1, 0))
.authors(vec!["Parity Technologies <admin@parity.io>".to_string()])
.build()
.unwrap();
let abi_json = json! {
{
"spec": {},
"storage": {},
"types": []
}
}
.as_object()
.unwrap()
.clone();
let metadata = ContractMetadata::new(source, contract, None, abi_json);
let json = serde_json::to_value(&metadata).unwrap();
let expected = json! {
{
"metadataVersion": "0.1.0",
"source": {
"hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"language": "ink! 2.1.0",
"compiler": "rustc 1.46.0-nightly"
},
"contract": {
"name": "incrementer",
"version": "2.1.0",
"authors": [
"Parity Technologies <admin@parity.io>"
],
},
"spec": {},
"storage": {},
"types": []
}
};
assert_eq!(json, expected);
}
}