diff --git a/crates/iroha/tests/integration/permissions.rs b/crates/iroha/tests/integration/permissions.rs index 66c79041a0b..2bfd07db699 100644 --- a/crates/iroha/tests/integration/permissions.rs +++ b/crates/iroha/tests/integration/permissions.rs @@ -16,6 +16,7 @@ use iroha_executor_data_model::permission::{ use iroha_genesis::GenesisBlock; use iroha_test_network::{PeerBuilder, *}; use iroha_test_samples::{gen_account_in, ALICE_ID, BOB_ID}; +use serde_json::json; #[test] fn genesis_transactions_are_validated_by_executor() { @@ -318,11 +319,7 @@ fn stored_vs_granted_permission_payload() -> Result<()> { .expect("Failed to register mouse"); // Allow alice to mint mouse asset and mint initial value - let value_json = JsonString::from_string_unchecked(format!( - // NOTE: Permissions is created explicitly as a json string to introduce additional whitespace - // This way, if the executor compares permissions just as JSON strings, the test will fail - r##"{{ "asset" : "xor#wonderland#{mouse_id}" }}"## - )); + let value_json = JsonString::new(json!({ "asset": format!("xor#wonderland#{mouse_id}") })); let mouse_asset = AssetId::new(asset_definition_id, mouse_id.clone()); let allow_alice_to_set_key_value_in_mouse_asset = Grant::account_permission( diff --git a/crates/iroha_codec/samples/account.bin b/crates/iroha_codec/samples/account.bin index 84168a56491..2166871fd7b 100644 Binary files a/crates/iroha_codec/samples/account.bin and b/crates/iroha_codec/samples/account.bin differ diff --git a/crates/iroha_codec/samples/domain.bin b/crates/iroha_codec/samples/domain.bin index 848687cb30e..17530187ee7 100644 --- a/crates/iroha_codec/samples/domain.bin +++ b/crates/iroha_codec/samples/domain.bin @@ -1 +1 @@ -(wonderlandÐ/ipfs/Qme7ss3ARVgxv6rXqVPiikMJ8u2NLgmgszg13pYrDKEoiuPIs_Jabberwocky_alivetrue \ No newline at end of file +(wonderlandÐ/ipfs/Qme7ss3ARVgxv6rXqVPiikMJ8u2NLgmgszg13pYrDKEoiuPIs_Jabberwocky_alive \ No newline at end of file diff --git a/crates/iroha_executor_data_model_derive/src/parameter.rs b/crates/iroha_executor_data_model_derive/src/parameter.rs index e98e4abbf25..430e162213b 100644 --- a/crates/iroha_executor_data_model_derive/src/parameter.rs +++ b/crates/iroha_executor_data_model_derive/src/parameter.rs @@ -22,7 +22,7 @@ pub fn impl_derive_parameter(input: &syn::DeriveInput) -> TokenStream { return Err(Self::Error::UnknownIdent(alloc::string::ToString::to_string(value_id.name().as_ref()))); } - serde_json::from_str::(value.payload().as_ref()).map_err(Self::Error::Deserialize) + value.payload().clone().try_into_any().map_err(Self::Error::Deserialize) } } diff --git a/crates/iroha_executor_data_model_derive/src/permission.rs b/crates/iroha_executor_data_model_derive/src/permission.rs index c6ef479f599..80bf8e9eb53 100644 --- a/crates/iroha_executor_data_model_derive/src/permission.rs +++ b/crates/iroha_executor_data_model_derive/src/permission.rs @@ -22,7 +22,7 @@ pub fn impl_derive_permission(input: &syn::DeriveInput) -> TokenStream { return Err(Self::Error::UnknownIdent(value.name().to_owned())); } - serde_json::from_str::(value.payload().as_ref()).map_err(Self::Error::Deserialize) + value.payload().clone().try_into_any().map_err(Self::Error::Deserialize) } } diff --git a/crates/iroha_primitives/src/json.rs b/crates/iroha_primitives/src/json.rs index f36be4d5f92..09b04d6bd81 100644 --- a/crates/iroha_primitives/src/json.rs +++ b/crates/iroha_primitives/src/json.rs @@ -3,27 +3,32 @@ #[cfg(not(feature = "std"))] use alloc::{ - format, + borrow::ToOwned, + collections::BTreeMap, string::{String, ToString}, vec::Vec, }; -use core::str::FromStr; +use core::{cmp::Ordering, str::FromStr}; #[cfg(feature = "std")] use std::{ + collections::BTreeMap, string::{String, ToString}, vec::Vec, }; use derive_more::Display; -use iroha_schema::IntoSchema; -use parity_scale_codec::{Decode, Encode}; +use parity_scale_codec::{Compact, Decode, Encode}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::Value; /// A valid `JsonString` that consists of valid String of Json type -#[derive(Debug, Display, Clone, PartialOrd, PartialEq, Ord, Eq, IntoSchema, Encode, Decode)] +#[derive(Debug, Display, Clone, Deserialize, Serialize)] #[display(fmt = "{_0}")] -pub struct JsonString(String); +#[serde(transparent)] +pub struct JsonString(Value); + +/// Helper struct to work with [`JsonString`] +struct JsonStringRef<'value>(&'value Value); impl JsonString { /// Constructs [`JsonString`] @@ -40,52 +45,20 @@ impl JsonString { /// /// # Errors /// - if invalid representation of `T` - pub fn try_into_any(&self) -> Result { - serde_json::from_str(&self.0) - } - - /// Create without checking whether the input is a valid JSON string. - /// - /// The caller must guarantee that the value is valid. - pub fn from_string_unchecked(value: String) -> Self { - Self(value) - } - - /// Getter for [`JsonString`] - pub fn get(&self) -> &String { - &self.0 - } -} - -impl<'de> serde::de::Deserialize<'de> for JsonString { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let json = Value::deserialize(deserializer)?; - Ok(Self(json.to_string())) - } -} - -impl serde::ser::Serialize for JsonString { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let json: Value = serde_json::from_str(&self.0).map_err(serde::ser::Error::custom)?; - json.serialize(serializer) + pub fn try_into_any(self) -> Result { + serde_json::from_value(self.0) } } impl From<&Value> for JsonString { fn from(value: &Value) -> Self { - JsonString(value.to_string()) + JsonString(value.clone()) } } impl From for JsonString { fn from(value: Value) -> Self { - JsonString(value.to_string()) + JsonString(value) } } @@ -131,25 +104,269 @@ impl FromStr for JsonString { fn from_str(s: &str) -> Result { if let Ok(value) = serde_json::from_str::(s) { - Ok(JsonString(value.to_string())) + Ok(JsonString(value)) } else { let json_formatted_string = serde_json::to_string(s)?; let value: Value = serde_json::from_str(&json_formatted_string)?; - Ok(JsonString(value.to_string())) + Ok(JsonString(value)) } } } impl Default for JsonString { fn default() -> Self { - // NOTE: empty string isn't valid JSON - Self("null".to_string()) + Self(Value::Null) + } +} + +impl Ord for JsonString { + fn cmp(&self, other: &Self) -> Ordering { + JsonStringRef(&self.0).cmp(&JsonStringRef(&other.0)) + } +} + +impl PartialOrd for JsonString { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for JsonString { + fn eq(&self, other: &Self) -> bool { + self.cmp(other).is_eq() + } +} + +impl Eq for JsonString {} + +impl Ord for JsonStringRef<'_> { + fn cmp(&self, other: &Self) -> Ordering { + discriminant(self.0) + .cmp(&discriminant(other.0)) + .then_with(|| match (self.0, other.0) { + (Value::Null, Value::Null) => Ordering::Equal, + (Value::String(l), Value::String(r)) => l.cmp(r), + (Value::Bool(l), Value::Bool(r)) => l.cmp(r), + (Value::Array(l), Value::Array(r)) => { + let l = l.iter().map(JsonStringRef); + let r = r.iter().map(JsonStringRef); + l.cmp(r) + } + (Value::Number(l), Value::Number(r)) => { + let l = l.to_string(); + let r = r.to_string(); + l.cmp(&r) + } + (Value::Object(l), Value::Object(r)) => { + let l = l.iter().map(|(k, v)| (k, JsonStringRef(v))); + let r = r.iter().map(|(k, v)| (k, JsonStringRef(v))); + l.cmp(r) + } + _ => unreachable!("other variants are handled through discriminant comparison"), + }) + } +} + +impl PartialOrd for JsonStringRef<'_> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for JsonStringRef<'_> { + fn eq(&self, other: &Self) -> bool { + self.cmp(other).is_eq() + } +} + +impl Eq for JsonStringRef<'_> {} + +mod schema { + use iroha_schema::{EnumMeta, EnumVariant, IntoSchema, Metadata, TypeId}; + + use super::*; + + impl TypeId for JsonString { + fn id() -> iroha_schema::Ident { + stringify!(JsonString).into() + } + } + + impl IntoSchema for JsonString { + fn type_name() -> iroha_schema::Ident { + Self::id() + } + + fn update_schema_map(metamap: &mut iroha_schema::MetaMap) { + if !metamap.contains_key::() { + if !metamap.contains_key::() { + ::update_schema_map(metamap); + } + if !metamap.contains_key::() { + ::update_schema_map(metamap); + } + if !metamap.contains_key::() { + ::update_schema_map(metamap); + } + if !metamap.contains_key::>() { + as iroha_schema::IntoSchema>::update_schema_map(metamap); + } + if !metamap.contains_key::>() { + as iroha_schema::IntoSchema>::update_schema_map( + metamap, + ); + } + + metamap.insert::(Metadata::Enum(EnumMeta { + variants: Vec::from([ + EnumVariant { + tag: "Null".to_owned(), + discriminant: discriminants::NULL, + ty: None, + }, + EnumVariant { + tag: "String".to_owned(), + discriminant: discriminants::STRING, + ty: Some(core::any::TypeId::of::()), + }, + EnumVariant { + tag: "Bool".to_owned(), + discriminant: discriminants::BOOL, + ty: Some(core::any::TypeId::of::()), + }, + EnumVariant { + tag: "Array".to_owned(), + discriminant: discriminants::ARRAY, + ty: Some(core::any::TypeId::of::>()), + }, + EnumVariant { + tag: "Number".to_owned(), + discriminant: discriminants::NUMBER, + ty: Some(core::any::TypeId::of::()), + }, + EnumVariant { + tag: "Object".to_owned(), + discriminant: discriminants::OBJECT, + ty: Some(core::any::TypeId::of::>()), + }, + ]), + })); + } + } } } -impl AsRef for JsonString { - fn as_ref(&self) -> &str { - &self.0 +mod scale { + use parity_scale_codec::{Error, Input, Output}; + use serde_json::{Map, Number}; + + use super::*; + + impl Encode for JsonStringRef<'_> { + fn encode_to(&self, dest: &mut T) { + match self.0 { + Value::Null => { + dest.push_byte(discriminants::NULL); + } + Value::String(string) => { + dest.push_byte(discriminants::STRING); + string.encode_to(dest); + } + Value::Bool(bool) => { + dest.push_byte(discriminants::BOOL); + bool.encode_to(dest); + } + Value::Array(arr) => { + dest.push_byte(discriminants::ARRAY); + Compact(arr.len() as u64).encode_to(dest); + for e in arr { + JsonStringRef(e).encode_to(dest); + } + } + Value::Number(num) => { + dest.push_byte(discriminants::NUMBER); + num.to_string().encode_to(dest); + } + Value::Object(obj) => { + dest.push_byte(discriminants::OBJECT); + Compact(obj.len() as u64).encode_to(dest); + for (key, value) in obj { + key.encode_to(dest); + JsonStringRef(value).encode_to(dest); + } + } + } + } + } + + impl Encode for JsonString { + fn encode(&self) -> Vec { + JsonStringRef(&self.0).encode() + } + } + + impl Decode for JsonString { + fn decode(input: &mut I) -> Result { + let discriminant = input.read_byte()?; + let value = match discriminant { + discriminants::NULL => Value::Null, + discriminants::STRING => { + let string = String::decode(input)?; + Value::String(string) + } + discriminants::BOOL => { + let bool = bool::decode(input)?; + Value::Bool(bool) + } + discriminants::ARRAY => { + let len = Compact::::decode(input)?.0; + let len = usize::try_from(len).map_err(|_| "length of array is too big")?; + let mut arr = Vec::with_capacity(len); + for _ in 0..len { + let value = JsonString::decode(input)?.0; + arr.push(value); + } + Value::Array(arr) + } + discriminants::NUMBER => { + let num = String::decode(input)?; + Value::Number(Number::from_str(&num).map_err(|_| "not valid number")?) + } + discriminants::OBJECT => { + let len = Compact::::decode(input)?.0; + let len = usize::try_from(len).map_err(|_| "length of array is too big")?; + let mut map = Map::with_capacity(len); + for _ in 0..len { + let key = String::decode(input)?; + let value = JsonString::decode(input)?.0; + map.insert(key, value); + } + Value::Object(map) + } + _ => return Err("get invalid JSON value discriminant".into()), + }; + Ok(JsonString(value)) + } + } +} + +mod discriminants { + pub const NULL: u8 = 0; + pub const STRING: u8 = 1; + pub const BOOL: u8 = 2; + pub const ARRAY: u8 = 3; + pub const NUMBER: u8 = 4; + pub const OBJECT: u8 = 5; +} + +fn discriminant(value: &Value) -> u8 { + match value { + Value::Null => discriminants::NULL, + Value::String(_) => discriminants::STRING, + Value::Bool(_) => discriminants::BOOL, + Value::Array(_) => discriminants::ARRAY, + Value::Number(_) => discriminants::NUMBER, + Value::Object(_) => discriminants::OBJECT, } } @@ -170,7 +387,7 @@ mod candidate { impl TryFrom> for JsonString { type Error = serde_json::Error; fn try_from(value: JsonCandidate) -> Result { - Ok(JsonString(serde_json::to_string(&value.0)?)) + serde_json::to_value(&value.0).map(JsonString) } } } diff --git a/crates/iroha_schema_gen/src/lib.rs b/crates/iroha_schema_gen/src/lib.rs index 276b328b68c..9ecb74c7f6f 100644 --- a/crates/iroha_schema_gen/src/lib.rs +++ b/crates/iroha_schema_gen/src/lib.rs @@ -166,6 +166,7 @@ types!( Box, BTreeMap, BTreeMap, + BTreeMap, BTreeSet, BTreeSet, BurnBox, @@ -483,6 +484,7 @@ types!( Vec>, Vec, Vec, + Vec, Vec, Vec, Vec, @@ -505,6 +507,7 @@ types!( u32, u64, u8, + bool, iroha_genesis::RawGenesisTransaction, ); diff --git a/docs/source/references/schema.json b/docs/source/references/schema.json index 6088c45dd7d..a767dcceddb 100644 --- a/docs/source/references/schema.json +++ b/docs/source/references/schema.json @@ -2392,7 +2392,39 @@ "IpfsPath": "String", "Ipv4Addr": "Array", "Ipv6Addr": "Array", - "JsonString": "String", + "JsonString": { + "Enum": [ + { + "tag": "Null", + "discriminant": 0 + }, + { + "tag": "String", + "discriminant": 1, + "type": "String" + }, + { + "tag": "Bool", + "discriminant": 2, + "type": "bool" + }, + { + "tag": "Array", + "discriminant": 3, + "type": "Vec" + }, + { + "tag": "Number", + "discriminant": 4, + "type": "String" + }, + { + "tag": "Object", + "discriminant": 5, + "type": "SortedMap" + } + ] + }, "Level": { "Enum": [ { @@ -4238,6 +4270,12 @@ "value": "JsonString" } }, + "SortedMap": { + "Map": { + "key": "String", + "value": "JsonString" + } + }, "SortedVec": { "Vec": "Permission" }, @@ -5003,6 +5041,9 @@ "Vec": { "Vec": "InstructionBox" }, + "Vec": { + "Vec": "JsonString" + }, "Vec": { "Vec": "Parameter" }, @@ -5045,6 +5086,7 @@ ] }, "WasmSmartContract": "Vec", + "bool": "bool", "u16": { "Int": "FixedWidth" }, diff --git a/wasm_samples/executor_custom_data_model/src/complex_isi.rs b/wasm_samples/executor_custom_data_model/src/complex_isi.rs index 6c358abe770..af1edd9ea8b 100644 --- a/wasm_samples/executor_custom_data_model/src/complex_isi.rs +++ b/wasm_samples/executor_custom_data_model/src/complex_isi.rs @@ -73,7 +73,7 @@ mod isi { type Error = serde_json::Error; fn try_from(payload: &JsonString) -> serde_json::Result { - serde_json::from_str::(payload.as_ref()) + payload.clone().try_into_any() } } diff --git a/wasm_samples/executor_custom_data_model/src/simple_isi.rs b/wasm_samples/executor_custom_data_model/src/simple_isi.rs index e89200fee18..e7015ca503a 100644 --- a/wasm_samples/executor_custom_data_model/src/simple_isi.rs +++ b/wasm_samples/executor_custom_data_model/src/simple_isi.rs @@ -57,6 +57,6 @@ impl TryFrom<&JsonString> for CustomInstructionBox { type Error = serde_json::Error; fn try_from(payload: &JsonString) -> serde_json::Result { - serde_json::from_str::(payload.as_ref()) + payload.clone().try_into_any() } } diff --git a/wasm_samples/mint_rose_trigger_args/src/lib.rs b/wasm_samples/mint_rose_trigger_args/src/lib.rs index c9999ea2069..59d871757b5 100644 --- a/wasm_samples/mint_rose_trigger_args/src/lib.rs +++ b/wasm_samples/mint_rose_trigger_args/src/lib.rs @@ -24,6 +24,7 @@ fn main(_id: TriggerId, owner: AccountId, event: EventBox) { EventBox::ExecuteTrigger(event) => event .args() .dbg_expect("Trigger expect parameters") + .clone() .try_into_any() .dbg_expect("Failed to parse args"), _ => dbg_panic("Only work as by call trigger"), diff --git a/wasm_samples/multisig/src/lib.rs b/wasm_samples/multisig/src/lib.rs index b3c7cf95ad8..6b211033a1f 100644 --- a/wasm_samples/multisig/src/lib.rs +++ b/wasm_samples/multisig/src/lib.rs @@ -24,6 +24,7 @@ fn main(id: TriggerId, _owner: AccountId, event: EventBox) { event .args() .dbg_expect("trigger expect args") + .clone() .try_into_any() .dbg_expect("failed to parse arguments"), event.authority().clone(), diff --git a/wasm_samples/multisig_register/src/lib.rs b/wasm_samples/multisig_register/src/lib.rs index e1f1a7c0488..c0c7e63e752 100644 --- a/wasm_samples/multisig_register/src/lib.rs +++ b/wasm_samples/multisig_register/src/lib.rs @@ -27,6 +27,7 @@ fn main(_id: TriggerId, _owner: AccountId, event: EventBox) { EventBox::ExecuteTrigger(event) => event .args() .dbg_expect("trigger expect args") + .clone() .try_into_any() .dbg_expect("failed to parse args"), _ => dbg_panic("Only work as by call trigger"),