Browse Source

Merge branch 'versioned_store_monorepo_migration' into monorepo_external_modules
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.

Shamil Gadelshin 5 years ago
parent
commit
5530093ea5

+ 50 - 0
runtime-modules/versioned-store/Cargo.toml

@@ -0,0 +1,50 @@
+[package]
+name = 'substrate-versioned-store'
+version = '1.0.1'
+authors = ['Joystream']
+edition = '2018'
+
+[dependencies]
+hex-literal = '0.1.0'
+serde = { version = '1.0', optional = true }
+serde_derive = { version = '1.0', optional = true }
+rstd = { package = 'sr-std', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+runtime-primitives = { package = 'sr-primitives', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support = { package = 'srml-support', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support-procedural = { package = 'srml-support-procedural', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+system = { package = 'srml-system', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+balances = { package = 'srml-balances', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+codec = { package = 'parity-scale-codec', version = '1.0.0', default-features = false, features = ['derive'] }
+# https://users.rust-lang.org/t/failure-derive-compilation-error/39062
+quote = '<=1.0.2'
+
+[dependencies.timestamp]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-timestamp'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.runtime-io]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-io'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dev-dependencies]
+runtime-io = { package = 'sr-io', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+primitives = { package = 'substrate-primitives', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+
+[features]
+default = ['std']
+std = [
+	'serde',
+	'serde_derive',
+	'codec/std',
+	'rstd/std',
+	'runtime-io/std',
+	'runtime-primitives/std',
+	'srml-support/std',
+	'system/std',
+	'balances/std',
+	'timestamp/std',
+]

+ 528 - 0
runtime-modules/versioned-store/src/example.rs

@@ -0,0 +1,528 @@
+#![cfg(test)]
+
+use super::*;
+use crate::mock::*;
+
+use srml_support::assert_ok;
+
+/// This example uses Class, Properties, Schema and Entity structures
+/// to describe the Staked podcast channel and its second episode.
+/// See https://staked.libsyn.com/rss
+
+#[test]
+fn create_podcast_class_schema() {
+    with_test_externalities(|| {
+        fn common_text_prop() -> PropertyType {
+            PropertyType::Text(200)
+        }
+
+        fn long_text_prop() -> PropertyType {
+            PropertyType::Text(4000)
+        }
+
+        // Channel props:
+        // ------------------------------------------
+
+        let channel_props = vec![
+            // 0
+            Property {
+                prop_type: common_text_prop(),
+                required: true,
+                name: b"atom:link".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 1
+            Property {
+                prop_type: common_text_prop(),
+                required: true,
+                name: b"title".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 2
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"pubDate".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 3
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"lastBuildDate".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 4
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"generator".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 5
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"link".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 6
+            // Example: en-us
+            Property {
+                prop_type: PropertyType::Text(5),
+                required: false,
+                name: b"language".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 7
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"copyright".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 8
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"docs".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 9
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"managingEditor".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 10
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"image/url".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 11
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"image/title".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 12
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"image/link".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 13
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:summary".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 14
+            // TODO this could be Internal prop.
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:author".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 15
+            // TODO make this as a text vec?
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:keywords".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 16
+            Property {
+                prop_type: PropertyType::TextVec(10, 100),
+                required: false,
+                name: b"itunes:category".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 17
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:image".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 18
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:explicit".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 19
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:owner/itunes:name".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 20
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:owner/itunes:email".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 21
+            Property {
+                prop_type: PropertyType::Text(4000),
+                required: false,
+                name: b"description".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 22
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:subtitle".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 23
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:type".to_vec(),
+                description: b"".to_vec(),
+            },
+        ];
+
+        // Episode props
+        // ------------------------------------------
+
+        let episode_props = vec![
+            // 0
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"title".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 1
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:title".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 2
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"pubDate".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 3
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"guid".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 4
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"link".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 5
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:image".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 6
+            Property {
+                prop_type: long_text_prop(),
+                required: false,
+                name: b"description".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 7
+            Property {
+                prop_type: long_text_prop(),
+                required: false,
+                name: b"content:encoded".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 8
+            Property {
+                prop_type: PropertyType::Text(50),
+                required: false,
+                name: b"enclosure/length".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 9
+            Property {
+                prop_type: PropertyType::Text(50),
+                required: false,
+                name: b"enclosure/type".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 10
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"enclosure/url".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 11
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:duration".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 12
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:explicit".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 13
+            // TODO make this as a text vec?
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:keywords".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 14
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:subtitle".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 15
+            Property {
+                prop_type: long_text_prop(),
+                required: false,
+                name: b"itunes:summary".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 16
+            Property {
+                prop_type: PropertyType::Uint16,
+                required: false,
+                name: b"itunes:season".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 17
+            Property {
+                prop_type: PropertyType::Uint16,
+                required: false,
+                name: b"itunes:episode".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 18
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:episodeType".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 19
+            // TODO this could be Internal prop.
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:author".to_vec(),
+                description: b"".to_vec(),
+            },
+        ];
+
+        // Channel
+
+        let channel_class_id = TestModule::next_class_id();
+        assert_ok!(
+            TestModule::create_class(b"PodcastChannel".to_vec(), b"A podcast channel".to_vec(),),
+            channel_class_id
+        );
+
+        let channel_schema_id: u16 = 0;
+
+        assert_ok!(
+            TestModule::add_class_schema(channel_class_id, vec![], channel_props,),
+            channel_schema_id
+        );
+
+        // Episodes:
+
+        let episode_class_id = TestModule::next_class_id();
+        assert_ok!(
+            TestModule::create_class(b"PodcastEpisode".to_vec(), b"A podcast episode".to_vec(),),
+            episode_class_id
+        );
+
+        let episode_schema_id: u16 = 0;
+
+        assert_ok!(
+            TestModule::add_class_schema(episode_class_id, vec![], episode_props,),
+            episode_schema_id
+        );
+
+        let mut p = PropHelper::new();
+        let channel_entity_id = TestModule::next_entity_id();
+
+        assert_ok!(
+            TestModule::create_entity(channel_class_id,),
+            channel_entity_id
+        );
+
+        assert_ok!(
+            TestModule::add_schema_support_to_entity(
+                channel_entity_id,
+                channel_schema_id,
+                vec![
+                    // 0
+                    p.next_text_value(b"https://staked.libsyn.com/rss".to_vec()),
+                    // 1
+                    p.next_text_value(b"Staked".to_vec()),
+                    // 2
+                    p.next_text_value(b"Wed, 15 May 2019 20:36:20 +0000".to_vec()),
+                    // 3
+                    p.next_text_value(b"Fri, 23 Aug 2019 11:26:24 +0000".to_vec()),
+                    // 4
+                    p.next_text_value(b"Libsyn WebEngine 2.0".to_vec()),
+                    // 5
+                    p.next_text_value(b"https://twitter.com/staked_podcast".to_vec()),
+                    // 6
+                    p.next_text_value(b"en".to_vec()),
+                    // 7
+                    p.next_value(PropertyValue::None),
+                    // 8
+                    p.next_text_value(b"https://twitter.com/staked_podcast".to_vec()),
+                    // 9
+                    p.next_text_value(b"staked@jsgenesis.com (staked@jsgenesis.com)".to_vec()),
+                    // 10
+                    p.next_text_value(b"https://ssl-static.libsyn.com/p/assets/2/d/2/5/2d25eb5fa72739f7/iTunes_Cover.png".to_vec()),
+                    // 11
+                    p.next_text_value(b"Staked".to_vec()),
+                    // 12
+                    p.next_text_value(b"https://twitter.com/staked_podcast".to_vec()),
+                    // 13
+                    p.next_text_value(b"Exploring crypto and blockchain governance.".to_vec()),
+                    // 14
+                    p.next_text_value(b"Staked".to_vec()),
+                    // 15
+                    p.next_text_value(b"crypto,blockchain,governance,staking,bitcoin,ethereum".to_vec()),
+                    // 16
+                    p.next_value(PropertyValue::TextVec(vec![
+                        b"Technology".to_vec(), 
+                        b"Software How-To".to_vec()
+                    ])),
+                    // 17
+                    p.next_text_value(b"https://ssl-static.libsyn.com/p/assets/2/d/2/5/2d25eb5fa72739f7/iTunes_Cover.png".to_vec()),
+                    // 18
+                    p.next_text_value(b"yes".to_vec()),
+                    // 19
+                    p.next_text_value(b"Martin Wessel-Berg".to_vec()),
+                    // 20
+                    p.next_text_value(b"staked@jsgenesis.com".to_vec()),
+                    // 21
+                    p.next_text_value(b"Exploring crypto and blockchain governance.".to_vec()),
+                    // 22
+                    p.next_text_value(b"Exploring crypto and blockchain governance.".to_vec()),
+                    // 23
+                    p.next_text_value(b"episodic".to_vec()),
+                ]
+            )
+        );
+
+        let episode_2_summary = b"<p>In July 2017, the SEC published a report following their <a href=\"https://www.sec.gov/litigation/investreport/34-81207.pdf\">investigation of the DAO</a>. This was significant as it was the first actionable statement from the SEC, giving some insight as to how they interpret this new asset class in light of existing securities laws.</p> <p>Staked is brought to you by Joystream - A user governed media platform.</p>".to_vec();
+
+        p = PropHelper::new();
+        let episode_2_entity_id = TestModule::next_entity_id();
+
+        assert_ok!(
+            TestModule::create_entity(episode_class_id,),
+            episode_2_entity_id
+        );
+
+        assert_ok!(
+            TestModule::add_schema_support_to_entity(
+                episode_2_entity_id,
+                episode_schema_id,
+                vec![
+                    // 0
+                    p.next_text_value(b"Implications of the DAO Report for Crypto Governance".to_vec()),
+                    // 1
+                    p.next_text_value(b"Implications of the DAO Report for Crypto Governance".to_vec()),
+                    // 2
+                    p.next_text_value(b"Wed, 13 Mar 2019 11:20:33 +0000".to_vec()),
+                    // 3
+                    p.next_text_value(b"1bf862ba81ab4ee797526d98e09ad301".to_vec()),
+                    // 4
+                    p.next_text_value(b"http://staked.libsyn.com/implications-of-the-dao-report-for-crypto-governance".to_vec()),
+                    // 5
+                    p.next_text_value(b"https://ssl-static.libsyn.com/p/assets/2/d/2/5/2d25eb5fa72739f7/iTunes_Cover.png".to_vec()),
+                    // 6
+                    p.next_text_value(episode_2_summary.clone()),
+                    // 7
+                    p.next_text_value(episode_2_summary.clone()),
+                    // 8
+                    p.next_text_value(b"87444374".to_vec()),
+                    // 9
+                    p.next_text_value(b"audio/mpeg".to_vec()),
+                    // 10
+                    p.next_text_value(b"https://traffic.libsyn.com/secure/staked/Staked_-_Ep._2_final_cut.mp3?dest-id=1097396".to_vec()),
+                    // 11
+                    p.next_text_value(b"36:27".to_vec()),
+                    // 12
+                    p.next_text_value(b"yes".to_vec()),
+                    // 13
+                    p.next_text_value(b"governance,crypto,sec,securities,dao,bitcoin,blockchain,ethereum".to_vec()),
+                    // 14
+                    p.next_text_value(b"Part I in a series exploring decentralized governance and securities law".to_vec()),
+                    // 15
+                    p.next_text_value(episode_2_summary),
+                    // 16
+                    p.next_value(PropertyValue::Uint16(1)),
+                    // 17
+                    p.next_value(PropertyValue::Uint16(2)),
+                    // 18
+                    p.next_text_value(b"full".to_vec()),
+                    // 19
+                    p.next_text_value(b"Staked".to_vec()),
+                ]
+            )
+        );
+    })
+}
+
+struct PropHelper {
+    prop_idx: u16,
+}
+
+impl PropHelper {
+    fn new() -> PropHelper {
+        PropHelper { prop_idx: 0 }
+    }
+
+    fn next_value(&mut self, value: PropertyValue) -> ClassPropertyValue {
+        let value = ClassPropertyValue {
+            in_class_index: self.prop_idx,
+            value: value,
+        };
+        self.prop_idx += 1;
+        value
+    }
+
+    fn next_text_value(&mut self, text: Vec<u8>) -> ClassPropertyValue {
+        self.next_value(PropertyValue::Text(text))
+    }
+}

+ 809 - 0
runtime-modules/versioned-store/src/lib.rs

@@ -0,0 +1,809 @@
+// Copyright 2019 Jsgenesis.
+//
+// This is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Substrate. If not, see <http://www.gnu.org/licenses/>.
+
+// Ensure we're `no_std` when compiling for Wasm.
+#![cfg_attr(not(feature = "std"), no_std)]
+
+#[cfg(feature = "std")]
+use serde_derive::{Deserialize, Serialize};
+
+use codec::{Decode, Encode};
+use rstd::collections::btree_set::BTreeSet;
+use rstd::prelude::*;
+use srml_support::{decl_event, decl_module, decl_storage, dispatch, ensure};
+use system;
+
+mod example;
+mod mock;
+mod tests;
+
+// Validation errors
+// --------------------------------------
+
+const ERROR_PROPERTY_NAME_TOO_SHORT: &str = "Property name is too short";
+const ERROR_PROPERTY_NAME_TOO_LONG: &str = "Property name is too long";
+const ERROR_PROPERTY_DESCRIPTION_TOO_SHORT: &str = "Property description is too long";
+const ERROR_PROPERTY_DESCRIPTION_TOO_LONG: &str = "Property description is too long";
+
+const ERROR_CLASS_NAME_TOO_SHORT: &str = "Class name is too short";
+const ERROR_CLASS_NAME_TOO_LONG: &str = "Class name is too long";
+const ERROR_CLASS_DESCRIPTION_TOO_SHORT: &str = "Class description is too long";
+const ERROR_CLASS_DESCRIPTION_TOO_LONG: &str = "Class description is too long";
+
+// Main logic errors
+// --------------------------------------
+
+const ERROR_CLASS_NOT_FOUND: &str = "Class was not found by id";
+const ERROR_UNKNOWN_CLASS_SCHEMA_ID: &str = "Unknown class schema id";
+const ERROR_CLASS_SCHEMA_REFERS_UNKNOWN_PROP_INDEX: &str =
+    "New class schema refers to an unknown property index";
+const ERROR_CLASS_SCHEMA_REFERS_UNKNOWN_INTERNAL_ID: &str =
+    "New class schema refers to an unknown internal class id";
+const ERROR_NO_PROPS_IN_CLASS_SCHEMA: &str =
+    "Cannot add a class schema with an empty list of properties";
+const ERROR_ENTITY_NOT_FOUND: &str = "Entity was not found by id";
+// const ERROR_ENTITY_ALREADY_DELETED: &str = "Entity is already deleted";
+const ERROR_SCHEMA_ALREADY_ADDED_TO_ENTITY: &str =
+    "Cannot add a schema that is already added to this entity";
+const ERROR_PROP_VALUE_DONT_MATCH_TYPE: &str =
+    "Some of the provided property values don't match the expected property type";
+const ERROR_PROP_NAME_NOT_UNIQUE_IN_CLASS: &str = "Property name is not unique within its class";
+const ERROR_MISSING_REQUIRED_PROP: &str =
+    "Some required property was not found when adding schema support to entity";
+const ERROR_UNKNOWN_ENTITY_PROP_ID: &str = "Some of the provided property ids cannot be found on the current list of propery values of this entity";
+const ERROR_TEXT_PROP_IS_TOO_LONG: &str = "Text propery is too long";
+const ERROR_VEC_PROP_IS_TOO_LONG: &str = "Vector propery is too long";
+const ERROR_INTERNAL_RPOP_DOES_NOT_MATCH_ITS_CLASS: &str =
+    "Internal property does not match its class";
+
+/// Length constraint for input validation
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
+pub struct InputValidationLengthConstraint {
+    /// Minimum length
+    pub min: u16,
+
+    /// Difference between minimum length and max length.
+    /// While having max would have been more direct, this
+    /// way makes max < min unrepresentable semantically,
+    /// which is safer.
+    pub max_min_diff: u16,
+}
+
+impl InputValidationLengthConstraint {
+    /// Helper for computing max
+    pub fn max(&self) -> u16 {
+        self.min + self.max_min_diff
+    }
+
+    pub fn ensure_valid(
+        &self,
+        len: usize,
+        too_short_msg: &'static str,
+        too_long_msg: &'static str,
+    ) -> Result<(), &'static str> {
+        let length = len as u16;
+        if length < self.min {
+            Err(too_short_msg)
+        } else if length > self.max() {
+            Err(too_long_msg)
+        } else {
+            Ok(())
+        }
+    }
+}
+
+pub type ClassId = u64;
+pub type EntityId = u64;
+
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
+pub struct Class {
+    pub id: ClassId,
+
+    /// All properties that have been used on this class across different class schemas.
+    /// Unlikely to be more than roughly 20 properties per class, often less.
+    /// For Person, think "height", "weight", etc.
+    pub properties: Vec<Property>,
+
+    /// All scehmas that are available for this class, think v0.0 Person, v.1.0 Person, etc.
+    pub schemas: Vec<ClassSchema>,
+
+    pub name: Vec<u8>,
+    pub description: Vec<u8>,
+}
+
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
+pub struct Entity {
+    pub id: EntityId,
+
+    /// The class id of this entity.
+    pub class_id: ClassId,
+
+    /// What schemas under which this entity of a class is available, think
+    /// v.2.0 Person schema for John, v3.0 Person schema for John
+    /// Unlikely to be more than roughly 20ish, assuming schemas for a given class eventually stableize, or that very old schema are eventually removed.
+    pub in_class_schema_indexes: Vec<u16>, // indices of schema in corresponding class
+
+    /// Values for properties on class that are used by some schema used by this entity!
+    /// Length is no more than Class.properties.
+    pub values: Vec<ClassPropertyValue>,
+    // pub deleted: bool,
+}
+
+/// A schema defines what properties describe an entity
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
+pub struct ClassSchema {
+    /// Indices into properties vector for the corresponding class.
+    pub properties: Vec<u16>,
+}
+
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
+pub struct Property {
+    pub prop_type: PropertyType,
+    pub required: bool,
+    pub name: Vec<u8>,
+    pub description: Vec<u8>,
+}
+
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
+pub enum PropertyType {
+    None,
+
+    // Single value:
+    Bool,
+    Uint16,
+    Uint32,
+    Uint64,
+    Int16,
+    Int32,
+    Int64,
+    Text(u16),
+    Internal(ClassId),
+
+    // Vector of values.
+    // The first u16 value is the max length of this vector.
+    BoolVec(u16),
+    Uint16Vec(u16),
+    Uint32Vec(u16),
+    Uint64Vec(u16),
+    Int16Vec(u16),
+    Int32Vec(u16),
+    Int64Vec(u16),
+
+    /// The first u16 value is the max length of this vector.
+    /// The second u16 value is the max length of every text item in this vector.
+    TextVec(u16, u16),
+
+    /// The first u16 value is the max length of this vector.
+    /// The second ClassId value tells that an every element of this vector
+    /// should be of a specific ClassId.
+    InternalVec(u16, ClassId),
+    // External(ExternalProperty),
+    // ExternalVec(u16, ExternalProperty),
+}
+
+impl Default for PropertyType {
+    fn default() -> Self {
+        PropertyType::None
+    }
+}
+
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
+pub enum PropertyValue {
+    None,
+
+    // Single value:
+    Bool(bool),
+    Uint16(u16),
+    Uint32(u32),
+    Uint64(u64),
+    Int16(i16),
+    Int32(i32),
+    Int64(i64),
+    Text(Vec<u8>),
+    Internal(EntityId),
+
+    // Vector of values:
+    BoolVec(Vec<bool>),
+    Uint16Vec(Vec<u16>),
+    Uint32Vec(Vec<u32>),
+    Uint64Vec(Vec<u64>),
+    Int16Vec(Vec<i16>),
+    Int32Vec(Vec<i32>),
+    Int64Vec(Vec<i64>),
+    TextVec(Vec<Vec<u8>>),
+    InternalVec(Vec<EntityId>),
+    // External(ExternalPropertyType),
+    // ExternalVec(Vec<ExternalPropertyType>),
+}
+
+impl Default for PropertyValue {
+    fn default() -> Self {
+        PropertyValue::None
+    }
+}
+
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
+pub struct ClassPropertyValue {
+    /// Index is into properties vector of class.
+    pub in_class_index: u16,
+
+    /// Value of property with index `in_class_index` in a given class.
+    pub value: PropertyValue,
+}
+
+pub trait Trait: system::Trait + Sized {
+    type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
+}
+
+decl_storage! {
+
+    trait Store for Module<T: Trait> as VersionedStore {
+
+        pub ClassById get(class_by_id) config(): map ClassId => Class;
+
+        pub EntityById get(entity_by_id) config(): map EntityId => Entity;
+
+        pub NextClassId get(next_class_id) config(): ClassId;
+
+        pub NextEntityId get(next_entity_id) config(): EntityId;
+
+        pub PropertyNameConstraint get(property_name_constraint)
+            config(): InputValidationLengthConstraint;
+
+        pub PropertyDescriptionConstraint get(property_description_constraint)
+            config(): InputValidationLengthConstraint;
+
+        pub ClassNameConstraint get(class_name_constraint)
+            config(): InputValidationLengthConstraint;
+
+        pub ClassDescriptionConstraint get(class_description_constraint)
+            config(): InputValidationLengthConstraint;
+    }
+}
+
+decl_event!(
+    pub enum Event<T>
+    where
+        <T as system::Trait>::AccountId,
+    {
+        ClassCreated(ClassId),
+        ClassSchemaAdded(ClassId, u16),
+
+        EntityCreated(EntityId),
+        // EntityDeleted(EntityId),
+        EntityPropertiesUpdated(EntityId),
+        EntitySchemaAdded(EntityId, u16),
+
+        /// This is a fake event that uses AccountId type just to make Rust compiler happy to compile this module.
+        FixCompilation(AccountId),
+    }
+);
+
+decl_module! {
+    pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+        fn deposit_event() = default;
+    }
+}
+
+// Shortcuts for faster readability of match expression:
+use PropertyType as PT;
+use PropertyValue as PV;
+
+impl<T: Trait> Module<T> {
+    /// Returns an id of a newly added class.
+    pub fn create_class(name: Vec<u8>, description: Vec<u8>) -> Result<ClassId, &'static str> {
+        Self::ensure_class_name_is_valid(&name)?;
+
+        Self::ensure_class_description_is_valid(&description)?;
+
+        let class_id = NextClassId::get();
+
+        let new_class = Class {
+            id: class_id,
+            properties: vec![],
+            schemas: vec![],
+            name,
+            description,
+        };
+
+        // Save newly created class:
+        ClassById::insert(class_id, new_class);
+
+        // Increment the next class id:
+        NextClassId::mutate(|n| *n += 1);
+
+        Self::deposit_event(RawEvent::ClassCreated(class_id));
+        Ok(class_id)
+    }
+
+    /// Returns an index of a newly added class schema on success.
+    pub fn add_class_schema(
+        class_id: ClassId,
+        existing_properties: Vec<u16>,
+        new_properties: Vec<Property>,
+    ) -> Result<u16, &'static str> {
+        Self::ensure_known_class_id(class_id)?;
+
+        let non_empty_schema = !existing_properties.is_empty() || !new_properties.is_empty();
+
+        ensure!(non_empty_schema, ERROR_NO_PROPS_IN_CLASS_SCHEMA);
+
+        let class = ClassById::get(class_id);
+
+        // TODO Use BTreeSet for prop unique names when switched to Substrate 2.
+        // There is no support for BTreeSet in Substrate 1 runtime.
+        // use rstd::collections::btree_set::BTreeSet;
+        let mut unique_prop_names = BTreeSet::new();
+        for prop in class.properties.iter() {
+            unique_prop_names.insert(prop.name.clone());
+        }
+
+        for prop in new_properties.iter() {
+            Self::ensure_property_name_is_valid(&prop.name)?;
+            Self::ensure_property_description_is_valid(&prop.description)?;
+
+            // Check that the name of a new property is unique within its class.
+            ensure!(
+                !unique_prop_names.contains(&prop.name),
+                ERROR_PROP_NAME_NOT_UNIQUE_IN_CLASS
+            );
+            unique_prop_names.insert(prop.name.clone());
+        }
+
+        // Check that existing props are valid indices of class properties vector:
+        let has_unknown_props = existing_properties
+            .iter()
+            .any(|&prop_id| prop_id >= class.properties.len() as u16);
+        ensure!(
+            !has_unknown_props,
+            ERROR_CLASS_SCHEMA_REFERS_UNKNOWN_PROP_INDEX
+        );
+
+        // Check validity of Internal(ClassId) for new_properties.
+        let has_unknown_internal_id = new_properties.iter().any(|prop| match prop.prop_type {
+            PropertyType::Internal(other_class_id) => !ClassById::exists(other_class_id),
+            _ => false,
+        });
+        ensure!(
+            !has_unknown_internal_id,
+            ERROR_CLASS_SCHEMA_REFERS_UNKNOWN_INTERNAL_ID
+        );
+
+        // Use the current length of schemas in this class as an index
+        // for the next schema that will be sent in a result of this function.
+        let schema_idx = class.schemas.len() as u16;
+
+        let mut schema = ClassSchema {
+            properties: existing_properties,
+        };
+
+        let mut updated_class_props = class.properties;
+        new_properties.into_iter().for_each(|prop| {
+            let prop_id = updated_class_props.len() as u16;
+            updated_class_props.push(prop);
+            schema.properties.push(prop_id);
+        });
+
+        ClassById::mutate(class_id, |class| {
+            class.properties = updated_class_props;
+            class.schemas.push(schema);
+        });
+
+        Self::deposit_event(RawEvent::ClassSchemaAdded(class_id, schema_idx));
+        Ok(schema_idx)
+    }
+
+    pub fn create_entity(class_id: ClassId) -> Result<EntityId, &'static str> {
+        Self::ensure_known_class_id(class_id)?;
+
+        let entity_id = NextEntityId::get();
+
+        let new_entity = Entity {
+            id: entity_id,
+            class_id,
+            in_class_schema_indexes: vec![],
+            values: vec![],
+            // deleted: false,
+        };
+
+        // Save newly created entity:
+        EntityById::insert(entity_id, new_entity);
+
+        // Increment the next entity id:
+        NextEntityId::mutate(|n| *n += 1);
+
+        Self::deposit_event(RawEvent::EntityCreated(entity_id));
+        Ok(entity_id)
+    }
+
+    pub fn add_schema_support_to_entity(
+        entity_id: EntityId,
+        schema_id: u16,
+        property_values: Vec<ClassPropertyValue>,
+    ) -> dispatch::Result {
+        Self::ensure_known_entity_id(entity_id)?;
+
+        let (entity, class) = Self::get_entity_and_class(entity_id);
+
+        // Check that schema_id is a valid index of class schemas vector:
+        let known_schema_id = schema_id < class.schemas.len() as u16;
+        ensure!(known_schema_id, ERROR_UNKNOWN_CLASS_SCHEMA_ID);
+
+        // Check that schema id is not yet added to this entity:
+        let schema_not_added = entity
+            .in_class_schema_indexes
+            .iter()
+            .position(|x| *x == schema_id)
+            .is_none();
+        ensure!(schema_not_added, ERROR_SCHEMA_ALREADY_ADDED_TO_ENTITY);
+
+        let class_schema_opt = class.schemas.get(schema_id as usize);
+        let schema_prop_ids = class_schema_opt.unwrap().properties.clone();
+
+        let current_entity_values = entity.values.clone();
+        let mut appended_entity_values = entity.values;
+
+        for &prop_id in schema_prop_ids.iter() {
+            let prop_already_added = current_entity_values
+                .iter()
+                .any(|prop| prop.in_class_index == prop_id);
+
+            if prop_already_added {
+                // A property is already added to the entity and cannot be updated
+                // while adding a schema support to this entity.
+                continue;
+            }
+
+            let class_prop = class.properties.get(prop_id as usize).unwrap();
+
+            // If a value was not povided for the property of this schema:
+            match property_values
+                .iter()
+                .find(|prop| prop.in_class_index == prop_id)
+            {
+                Some(new_prop) => {
+                    let ClassPropertyValue {
+                        in_class_index: new_id,
+                        value: new_value,
+                    } = new_prop;
+
+                    Self::ensure_property_value_is_valid(new_value.clone(), class_prop.clone())?;
+
+                    appended_entity_values.push(ClassPropertyValue {
+                        in_class_index: *new_id,
+                        value: new_value.clone(),
+                    });
+                }
+                None => {
+                    // All required prop values should be are provided
+                    if class_prop.required {
+                        return Err(ERROR_MISSING_REQUIRED_PROP);
+                    }
+                    // Add all missing non required schema prop values as PropertyValue::None
+                    else {
+                        appended_entity_values.push(ClassPropertyValue {
+                            in_class_index: prop_id,
+                            value: PropertyValue::None,
+                        });
+                    }
+                }
+            }
+        }
+
+        EntityById::mutate(entity_id, |entity| {
+            // Add a new schema to the list of schemas supported by this entity.
+            entity.in_class_schema_indexes.push(schema_id);
+
+            // Update entity values only if new properties have been added.
+            if appended_entity_values.len() > entity.values.len() {
+                entity.values = appended_entity_values;
+            }
+        });
+
+        Self::deposit_event(RawEvent::EntitySchemaAdded(entity_id, schema_id));
+        Ok(())
+    }
+
+    pub fn update_entity_property_values(
+        entity_id: EntityId,
+        new_property_values: Vec<ClassPropertyValue>,
+    ) -> dispatch::Result {
+        Self::ensure_known_entity_id(entity_id)?;
+
+        let (entity, class) = Self::get_entity_and_class(entity_id);
+
+        // Get current property values of an entity as a mutable vector,
+        // so we can update them if new values provided present in new_property_values.
+        let mut updated_values = entity.values;
+        let mut updates_count = 0;
+
+        // Iterate over a vector of new values and update corresponding properties
+        // of this entity if new values are valid.
+        for new_prop_value in new_property_values.iter() {
+            let ClassPropertyValue {
+                in_class_index: id,
+                value: new_value,
+            } = new_prop_value;
+
+            // Try to find a current property value in the entity
+            // by matching its id to the id of a property with an updated value.
+            if let Some(current_prop_value) = updated_values
+                .iter_mut()
+                .find(|prop| *id == prop.in_class_index)
+            {
+                let ClassPropertyValue {
+                    in_class_index: valid_id,
+                    value: current_value,
+                } = current_prop_value;
+
+                // Get class-level information about this property
+                let class_prop = class.properties.get(*valid_id as usize).unwrap();
+
+                // Validate a new property value against the type of this property
+                // and check any additional constraints like the length of a vector
+                // if it's a vector property or the length of a text if it's a text property.
+                Self::ensure_property_value_is_valid(new_value.clone(), class_prop.clone())?;
+
+                // Update a current prop value in a mutable vector, if a new value is valid.
+                *current_value = new_value.clone();
+                updates_count += 1;
+            } else {
+                // Throw an error if a property was not found on entity
+                // by an in-class index of a property update.
+                return Err(ERROR_UNKNOWN_ENTITY_PROP_ID);
+            }
+        }
+
+        // If at least one of the entity property values should be update:
+        if updates_count > 0 {
+            EntityById::mutate(entity_id, |entity| {
+                entity.values = updated_values;
+            });
+            Self::deposit_event(RawEvent::EntityPropertiesUpdated(entity_id));
+        }
+
+        Ok(())
+    }
+
+    // Commented out for now <- requested by Bedeho.
+    // pub fn delete_entity(entity_id: EntityId) -> dispatch::Result {
+    //     Self::ensure_known_entity_id(entity_id)?;
+
+    //     let is_deleted = EntityById::get(entity_id).deleted;
+    //     ensure!(!is_deleted, ERROR_ENTITY_ALREADY_DELETED);
+
+    //     EntityById::mutate(entity_id, |x| {
+    //         x.deleted = true;
+    //     });
+
+    //     Self::deposit_event(RawEvent::EntityDeleted(entity_id));
+    //     Ok(())
+    // }
+
+    // Helper functions:
+    // ----------------------------------------------------------------
+
+    pub fn ensure_known_class_id(class_id: ClassId) -> dispatch::Result {
+        ensure!(ClassById::exists(class_id), ERROR_CLASS_NOT_FOUND);
+        Ok(())
+    }
+
+    pub fn ensure_known_entity_id(entity_id: EntityId) -> dispatch::Result {
+        ensure!(EntityById::exists(entity_id), ERROR_ENTITY_NOT_FOUND);
+        Ok(())
+    }
+
+    pub fn ensure_valid_internal_prop(value: PropertyValue, prop: Property) -> dispatch::Result {
+        match (value, prop.prop_type) {
+            (PV::Internal(entity_id), PT::Internal(class_id)) => {
+                Self::ensure_known_class_id(class_id)?;
+                Self::ensure_known_entity_id(entity_id)?;
+                let entity = Self::entity_by_id(entity_id);
+                ensure!(
+                    entity.class_id == class_id,
+                    ERROR_INTERNAL_RPOP_DOES_NOT_MATCH_ITS_CLASS
+                );
+                Ok(())
+            }
+            _ => Ok(()),
+        }
+    }
+
+    pub fn is_unknown_internal_entity_id(id: PropertyValue) -> bool {
+        if let PropertyValue::Internal(entity_id) = id {
+            !EntityById::exists(entity_id)
+        } else {
+            false
+        }
+    }
+
+    pub fn get_entity_and_class(entity_id: EntityId) -> (Entity, Class) {
+        let entity = EntityById::get(entity_id);
+        let class = ClassById::get(entity.class_id);
+        (entity, class)
+    }
+
+    pub fn ensure_property_value_is_valid(
+        value: PropertyValue,
+        prop: Property,
+    ) -> dispatch::Result {
+        Self::ensure_prop_value_matches_its_type(value.clone(), prop.clone())?;
+        Self::ensure_valid_internal_prop(value.clone(), prop.clone())?;
+        Self::validate_max_len_if_text_prop(value.clone(), prop.clone())?;
+        Self::validate_max_len_if_vec_prop(value.clone(), prop.clone())?;
+        Ok(())
+    }
+
+    pub fn validate_max_len_if_text_prop(value: PropertyValue, prop: Property) -> dispatch::Result {
+        match (value, prop.prop_type) {
+            (PV::Text(text), PT::Text(max_len)) => Self::validate_max_len_of_text(text, max_len),
+            _ => Ok(()),
+        }
+    }
+
+    pub fn validate_max_len_of_text(text: Vec<u8>, max_len: u16) -> dispatch::Result {
+        if text.len() <= max_len as usize {
+            Ok(())
+        } else {
+            Err(ERROR_TEXT_PROP_IS_TOO_LONG)
+        }
+    }
+
+    #[rustfmt::skip]
+    pub fn validate_max_len_if_vec_prop(
+        value: PropertyValue,
+        prop: Property,
+    ) -> dispatch::Result {
+
+        fn validate_vec_len<T>(vec: Vec<T>, max_len: u16) -> bool {
+            vec.len() <= max_len as usize
+        }
+
+        fn validate_vec_len_ref<T>(vec: &Vec<T>, max_len: u16) -> bool {
+            vec.len() <= max_len as usize
+        }
+
+        let is_valid_len = match (value, prop.prop_type) {
+            (PV::BoolVec(vec),     PT::BoolVec(max_len))   => validate_vec_len(vec, max_len),
+            (PV::Uint16Vec(vec),   PT::Uint16Vec(max_len)) => validate_vec_len(vec, max_len),
+            (PV::Uint32Vec(vec),   PT::Uint32Vec(max_len)) => validate_vec_len(vec, max_len),
+            (PV::Uint64Vec(vec),   PT::Uint64Vec(max_len)) => validate_vec_len(vec, max_len),
+            (PV::Int16Vec(vec),    PT::Int16Vec(max_len))  => validate_vec_len(vec, max_len),
+            (PV::Int32Vec(vec),    PT::Int32Vec(max_len))  => validate_vec_len(vec, max_len),
+            (PV::Int64Vec(vec),    PT::Int64Vec(max_len))  => validate_vec_len(vec, max_len),
+
+            (PV::TextVec(vec),     PT::TextVec(vec_max_len, text_max_len)) => {
+                if validate_vec_len_ref(&vec, vec_max_len) {
+                    for text_item in vec.iter() {
+                        Self::validate_max_len_of_text(text_item.clone(), text_max_len)?;
+                    }
+                    true
+                } else {
+                    false
+                }
+            },
+
+            (PV::InternalVec(vec), PT::InternalVec(vec_max_len, class_id)) => {
+                Self::ensure_known_class_id(class_id)?;
+                if validate_vec_len_ref(&vec, vec_max_len) {
+                    for entity_id in vec.iter() {
+                        Self::ensure_known_entity_id(entity_id.clone())?;
+                        let entity = Self::entity_by_id(entity_id);
+                        ensure!(entity.class_id == class_id, ERROR_INTERNAL_RPOP_DOES_NOT_MATCH_ITS_CLASS);
+                    }
+                    true
+                } else {
+                    false
+                }
+            },
+
+            _ => true
+        };
+
+        if is_valid_len {
+            Ok(())
+        } else {
+            Err(ERROR_VEC_PROP_IS_TOO_LONG)
+        }
+    }
+
+    pub fn ensure_prop_value_matches_its_type(
+        value: PropertyValue,
+        prop: Property,
+    ) -> dispatch::Result {
+        if Self::does_prop_value_match_type(value, prop) {
+            Ok(())
+        } else {
+            Err(ERROR_PROP_VALUE_DONT_MATCH_TYPE)
+        }
+    }
+
+    #[rustfmt::skip]
+    pub fn does_prop_value_match_type(
+        value: PropertyValue,
+        prop: Property,
+    ) -> bool {
+
+        // A non required property can be updated to None:
+        if !prop.required && value == PV::None {
+            return true
+        }
+
+        match (value, prop.prop_type) {
+            (PV::None,        PT::None) |
+
+            // Single values
+            (PV::Bool(_),     PT::Bool) |
+            (PV::Uint16(_),   PT::Uint16) |
+            (PV::Uint32(_),   PT::Uint32) |
+            (PV::Uint64(_),   PT::Uint64) |
+            (PV::Int16(_),    PT::Int16) |
+            (PV::Int32(_),    PT::Int32) |
+            (PV::Int64(_),    PT::Int64) |
+            (PV::Text(_),     PT::Text(_)) |
+            (PV::Internal(_), PT::Internal(_)) |
+
+            // Vectors:
+            (PV::BoolVec(_),     PT::BoolVec(_)) |
+            (PV::Uint16Vec(_),   PT::Uint16Vec(_)) |
+            (PV::Uint32Vec(_),   PT::Uint32Vec(_)) |
+            (PV::Uint64Vec(_),   PT::Uint64Vec(_)) |
+            (PV::Int16Vec(_),    PT::Int16Vec(_)) |
+            (PV::Int32Vec(_),    PT::Int32Vec(_)) |
+            (PV::Int64Vec(_),    PT::Int64Vec(_)) |
+            (PV::TextVec(_),     PT::TextVec(_, _)) |
+            (PV::InternalVec(_), PT::InternalVec(_, _)) => true,
+
+            // (PV::External(_), PT::External(_)) => true,
+            // (PV::ExternalVec(_), PT::ExternalVec(_, _)) => true,
+            _ => false,
+        }
+    }
+
+    pub fn ensure_property_name_is_valid(text: &Vec<u8>) -> dispatch::Result {
+        PropertyNameConstraint::get().ensure_valid(
+            text.len(),
+            ERROR_PROPERTY_NAME_TOO_SHORT,
+            ERROR_PROPERTY_NAME_TOO_LONG,
+        )
+    }
+
+    pub fn ensure_property_description_is_valid(text: &Vec<u8>) -> dispatch::Result {
+        PropertyDescriptionConstraint::get().ensure_valid(
+            text.len(),
+            ERROR_PROPERTY_DESCRIPTION_TOO_SHORT,
+            ERROR_PROPERTY_DESCRIPTION_TOO_LONG,
+        )
+    }
+
+    pub fn ensure_class_name_is_valid(text: &Vec<u8>) -> dispatch::Result {
+        ClassNameConstraint::get().ensure_valid(
+            text.len(),
+            ERROR_CLASS_NAME_TOO_SHORT,
+            ERROR_CLASS_NAME_TOO_LONG,
+        )
+    }
+
+    pub fn ensure_class_description_is_valid(text: &Vec<u8>) -> dispatch::Result {
+        ClassDescriptionConstraint::get().ensure_valid(
+            text.len(),
+            ERROR_CLASS_DESCRIPTION_TOO_SHORT,
+            ERROR_CLASS_DESCRIPTION_TOO_LONG,
+        )
+    }
+}

+ 259 - 0
runtime-modules/versioned-store/src/mock.rs

@@ -0,0 +1,259 @@
+#![cfg(test)]
+
+use crate::*;
+use crate::{GenesisConfig, Module, Trait};
+
+use primitives::H256;
+use runtime_primitives::{
+    testing::Header,
+    traits::{BlakeTwo256, IdentityLookup},
+    Perbill,
+};
+use srml_support::{assert_err, assert_ok, impl_outer_origin, parameter_types};
+
+impl_outer_origin! {
+    pub enum Origin for Runtime {}
+}
+
+// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted.
+#[derive(Clone, PartialEq, Eq, Debug)]
+pub struct Runtime;
+parameter_types! {
+    pub const BlockHashCount: u64 = 250;
+    pub const MaximumBlockWeight: u32 = 1024;
+    pub const MaximumBlockLength: u32 = 2 * 1024;
+    pub const AvailableBlockRatio: Perbill = Perbill::one();
+    pub const MinimumPeriod: u64 = 5;
+}
+
+impl system::Trait for Runtime {
+    type Origin = Origin;
+    type Index = u64;
+    type BlockNumber = u64;
+    type Call = ();
+    type Hash = H256;
+    type Hashing = BlakeTwo256;
+    type AccountId = u64;
+    type Lookup = IdentityLookup<Self::AccountId>;
+    type Header = Header;
+    type Event = ();
+    type BlockHashCount = BlockHashCount;
+    type MaximumBlockWeight = MaximumBlockWeight;
+    type MaximumBlockLength = MaximumBlockLength;
+    type AvailableBlockRatio = AvailableBlockRatio;
+    type Version = ();
+}
+
+impl timestamp::Trait for Runtime {
+    type Moment = u64;
+    type OnTimestampSet = ();
+    type MinimumPeriod = MinimumPeriod;
+}
+
+impl Trait for Runtime {
+    type Event = ();
+}
+
+pub const UNKNOWN_CLASS_ID: ClassId = 111;
+
+pub const UNKNOWN_ENTITY_ID: EntityId = 222;
+
+pub const UNKNOWN_PROP_ID: u16 = 333;
+
+// pub const UNKNOWN_SCHEMA_ID: u16 = 444;
+
+pub const SCHEMA_ID_0: u16 = 0;
+pub const SCHEMA_ID_1: u16 = 1;
+
+// pub fn generate_text(len: usize) -> Vec<u8> {
+//     vec![b'x'; len]
+// }
+
+pub fn good_class_name() -> Vec<u8> {
+    b"Name of a class".to_vec()
+}
+
+pub fn good_class_description() -> Vec<u8> {
+    b"Description of a class".to_vec()
+}
+
+impl Property {
+    fn required(&self) -> Property {
+        let mut new_self = self.clone();
+        new_self.required = true;
+        new_self
+    }
+}
+
+pub fn good_prop_bool() -> Property {
+    Property {
+        prop_type: PropertyType::Bool,
+        required: false,
+        name: b"Name of a bool property".to_vec(),
+        description: b"Description of a bool property".to_vec(),
+    }
+}
+
+pub fn good_prop_u32() -> Property {
+    Property {
+        prop_type: PropertyType::Uint32,
+        required: false,
+        name: b"Name of a u32 property".to_vec(),
+        description: b"Description of a u32 property".to_vec(),
+    }
+}
+
+pub fn good_prop_text() -> Property {
+    Property {
+        prop_type: PropertyType::Text(20),
+        required: false,
+        name: b"Name of a text property".to_vec(),
+        description: b"Description of a text property".to_vec(),
+    }
+}
+
+pub fn new_internal_class_prop(class_id: ClassId) -> Property {
+    Property {
+        prop_type: PropertyType::Internal(class_id),
+        required: false,
+        name: b"Name of a internal property".to_vec(),
+        description: b"Description of a internal property".to_vec(),
+    }
+}
+
+pub fn good_props() -> Vec<Property> {
+    vec![good_prop_bool(), good_prop_u32()]
+}
+
+pub fn good_prop_ids() -> Vec<u16> {
+    vec![0, 1]
+}
+
+pub fn create_class() -> ClassId {
+    let class_id = TestModule::next_class_id();
+    assert_ok!(
+        TestModule::create_class(good_class_name(), good_class_description(),),
+        class_id
+    );
+    class_id
+}
+
+pub fn bool_prop_value() -> ClassPropertyValue {
+    ClassPropertyValue {
+        in_class_index: 0,
+        value: PropertyValue::Bool(true),
+    }
+}
+
+pub fn prop_value(index: u16, value: PropertyValue) -> ClassPropertyValue {
+    ClassPropertyValue {
+        in_class_index: index,
+        value: value,
+    }
+}
+
+pub fn create_class_with_schema_and_entity() -> (ClassId, u16, EntityId) {
+    let class_id = create_class();
+    if let Ok(schema_id) = TestModule::add_class_schema(
+        class_id,
+        vec![],
+        vec![
+            good_prop_bool().required(),
+            good_prop_u32(),
+            new_internal_class_prop(class_id),
+        ],
+    ) {
+        let entity_id = create_entity_of_class(class_id);
+        (class_id, schema_id, entity_id)
+    } else {
+        panic!("This should not happen")
+    }
+}
+
+pub const PROP_ID_BOOL: u16 = 0;
+pub const PROP_ID_U32: u16 = 1;
+pub const PROP_ID_INTERNAL: u16 = 2;
+
+pub fn create_entity_with_schema_support() -> EntityId {
+    let (_, schema_id, entity_id) = create_class_with_schema_and_entity();
+    assert_ok!(TestModule::add_schema_support_to_entity(
+        entity_id,
+        schema_id,
+        vec![prop_value(PROP_ID_BOOL, PropertyValue::Bool(true))]
+    ));
+    entity_id
+}
+
+pub fn create_entity_of_class(class_id: ClassId) -> EntityId {
+    let entity_id = TestModule::next_entity_id();
+    assert_ok!(TestModule::create_entity(class_id,), entity_id);
+    entity_id
+}
+
+pub fn assert_class_props(class_id: ClassId, expected_props: Vec<Property>) {
+    let class = TestModule::class_by_id(class_id);
+    assert_eq!(class.properties, expected_props);
+}
+
+pub fn assert_class_schemas(class_id: ClassId, expected_schema_prop_ids: Vec<Vec<u16>>) {
+    let class = TestModule::class_by_id(class_id);
+    let schemas: Vec<_> = expected_schema_prop_ids
+        .iter()
+        .map(|prop_ids| ClassSchema {
+            properties: prop_ids.clone(),
+        })
+        .collect();
+    assert_eq!(class.schemas, schemas);
+}
+
+pub fn assert_entity_not_found(result: dispatch::Result) {
+    assert_err!(result, ERROR_ENTITY_NOT_FOUND);
+}
+
+// This function basically just builds a genesis storage key/value store according to
+// our desired mockup.
+
+pub fn default_genesis_config() -> GenesisConfig {
+    GenesisConfig {
+        class_by_id: vec![],
+        entity_by_id: vec![],
+        next_class_id: 1,
+        next_entity_id: 1,
+        property_name_constraint: InputValidationLengthConstraint {
+            min: 1,
+            max_min_diff: 49,
+        },
+        property_description_constraint: InputValidationLengthConstraint {
+            min: 0,
+            max_min_diff: 500,
+        },
+        class_name_constraint: InputValidationLengthConstraint {
+            min: 1,
+            max_min_diff: 49,
+        },
+        class_description_constraint: InputValidationLengthConstraint {
+            min: 0,
+            max_min_diff: 500,
+        },
+    }
+}
+
+fn build_test_externalities(config: GenesisConfig) -> runtime_io::TestExternalities {
+    let mut t = system::GenesisConfig::default()
+        .build_storage::<Runtime>()
+        .unwrap();
+
+    config.assimilate_storage(&mut t).unwrap();
+
+    t.into()
+}
+
+pub fn with_test_externalities<R, F: FnOnce() -> R>(f: F) -> R {
+    let config = default_genesis_config();
+    build_test_externalities(config).execute_with(f)
+}
+
+// pub type System = system::Module;
+
+/// Export module on a test runtime
+pub type TestModule = Module<Runtime>;

+ 503 - 0
runtime-modules/versioned-store/src/tests.rs

@@ -0,0 +1,503 @@
+#![cfg(test)]
+
+use super::*;
+use crate::mock::*;
+
+use srml_support::{assert_err, assert_ok};
+
+// Create class
+// --------------------------------------
+
+#[test]
+fn create_class_successfully() {
+    with_test_externalities(|| {
+        let class_id = TestModule::next_class_id();
+        assert_ok!(
+            TestModule::create_class(good_class_name(), good_class_description(),),
+            class_id
+        );
+        assert_eq!(TestModule::next_class_id(), class_id + 1);
+    })
+}
+
+#[test]
+fn cannot_create_class_with_empty_name() {
+    with_test_externalities(|| {
+        let empty_name = vec![];
+        assert_err!(
+            TestModule::create_class(empty_name, good_class_description(),),
+            ERROR_CLASS_NAME_TOO_SHORT
+        );
+    })
+}
+
+#[test]
+fn create_class_with_empty_description() {
+    with_test_externalities(|| {
+        let empty_description = vec![];
+        assert_eq!(
+            TestModule::create_class(good_class_name(), empty_description,),
+            Ok(1)
+        );
+    })
+}
+
+// Add class schema
+// --------------------------------------
+
+#[test]
+fn cannot_add_schema_to_unknown_class() {
+    with_test_externalities(|| {
+        assert_err!(
+            TestModule::add_class_schema(UNKNOWN_CLASS_ID, good_prop_ids(), good_props()),
+            ERROR_CLASS_NOT_FOUND
+        );
+    })
+}
+
+#[test]
+fn cannot_add_class_schema_when_no_props_passed() {
+    with_test_externalities(|| {
+        let class_id = create_class();
+        assert_err!(
+            TestModule::add_class_schema(class_id, vec![], vec![]),
+            ERROR_NO_PROPS_IN_CLASS_SCHEMA
+        );
+    })
+}
+
+#[test]
+fn cannot_add_class_schema_when_it_refers_unknown_prop_index_and_class_has_no_props() {
+    with_test_externalities(|| {
+        let class_id = create_class();
+        assert_err!(
+            TestModule::add_class_schema(class_id, vec![UNKNOWN_PROP_ID], vec![]),
+            ERROR_CLASS_SCHEMA_REFERS_UNKNOWN_PROP_INDEX
+        );
+    })
+}
+
+#[test]
+fn cannot_add_class_schema_when_it_refers_unknown_prop_index() {
+    with_test_externalities(|| {
+        let class_id = create_class();
+
+        assert_eq!(
+            TestModule::add_class_schema(class_id, vec![], good_props()),
+            Ok(SCHEMA_ID_0)
+        );
+
+        // Try to add a new schema that is based on one valid prop ids
+        // plus another prop id is unknown on this class.
+        assert_err!(
+            TestModule::add_class_schema(class_id, vec![0, UNKNOWN_PROP_ID], vec![]),
+            ERROR_CLASS_SCHEMA_REFERS_UNKNOWN_PROP_INDEX
+        );
+
+        // Verify that class props and schemas remain unchanged:
+        assert_class_props(class_id, good_props());
+        assert_class_schemas(class_id, vec![good_prop_ids()]);
+    })
+}
+
+#[test]
+fn cannot_add_class_schema_when_it_refers_unknown_internal_id() {
+    with_test_externalities(|| {
+        let class_id = create_class();
+        let bad_internal_prop = new_internal_class_prop(UNKNOWN_CLASS_ID);
+
+        assert_err!(
+            TestModule::add_class_schema(
+                class_id,
+                vec![],
+                vec![good_prop_bool(), bad_internal_prop]
+            ),
+            ERROR_CLASS_SCHEMA_REFERS_UNKNOWN_INTERNAL_ID
+        );
+    })
+}
+
+#[test]
+fn should_add_class_schema_with_internal_class_prop() {
+    with_test_externalities(|| {
+        let class_id = create_class();
+        let internal_class_prop = new_internal_class_prop(class_id);
+
+        // Add first schema with new props.
+        // No other props on the class at this time.
+        assert_eq!(
+            TestModule::add_class_schema(class_id, vec![], vec![internal_class_prop.clone()]),
+            Ok(SCHEMA_ID_0)
+        );
+
+        assert_class_props(class_id, vec![internal_class_prop]);
+        assert_class_schemas(class_id, vec![vec![SCHEMA_ID_0]]);
+    })
+}
+
+#[test]
+fn should_add_class_schema_when_only_new_props_passed() {
+    with_test_externalities(|| {
+        let class_id = create_class();
+
+        // Add first schema with new props.
+        // No other props on the class at this time.
+        assert_eq!(
+            TestModule::add_class_schema(class_id, vec![], good_props()),
+            Ok(SCHEMA_ID_0)
+        );
+
+        assert_class_props(class_id, good_props());
+        assert_class_schemas(class_id, vec![good_prop_ids()]);
+    })
+}
+
+#[test]
+fn should_add_class_schema_when_only_prop_ids_passed() {
+    with_test_externalities(|| {
+        let class_id = create_class();
+
+        // Add first schema with new props.
+        // No other props on the class at this time.
+        assert_eq!(
+            TestModule::add_class_schema(class_id, vec![], good_props()),
+            Ok(SCHEMA_ID_0)
+        );
+
+        // Add a new schema that is based solely on the props ids
+        // of the previously added schema.
+        assert_eq!(
+            TestModule::add_class_schema(class_id, good_prop_ids(), vec![]),
+            Ok(SCHEMA_ID_1)
+        );
+    })
+}
+
+#[test]
+fn cannot_add_class_schema_when_new_props_have_duplicate_names() {
+    with_test_externalities(|| {
+        let class_id = create_class();
+
+        // Add first schema with new props.
+        // No other props on the class at this time.
+        assert_eq!(
+            TestModule::add_class_schema(class_id, vec![], good_props()),
+            Ok(SCHEMA_ID_0)
+        );
+
+        // Add a new schema with not unique property names:
+        assert_err!(
+            TestModule::add_class_schema(class_id, vec![], good_props()),
+            ERROR_PROP_NAME_NOT_UNIQUE_IN_CLASS
+        );
+    })
+}
+
+#[test]
+fn should_add_class_schema_when_both_prop_ids_and_new_props_passed() {
+    with_test_externalities(|| {
+        let class_id = create_class();
+
+        // Add first schema with new props.
+        // No other props on the class at this time.
+        assert_eq!(
+            TestModule::add_class_schema(class_id, vec![], vec![good_prop_bool(), good_prop_u32()]),
+            Ok(SCHEMA_ID_0)
+        );
+
+        // Add a new schema that is based on some prop ids
+        // added with previous schema plus some new props,
+        // introduced by this new schema.
+        assert_eq!(
+            TestModule::add_class_schema(class_id, vec![1], vec![good_prop_text()]),
+            Ok(SCHEMA_ID_1)
+        );
+
+        assert_class_props(
+            class_id,
+            vec![good_prop_bool(), good_prop_u32(), good_prop_text()],
+        );
+
+        assert_class_schemas(class_id, vec![vec![0, 1], vec![1, 2]]);
+    })
+}
+
+// Create entity
+// --------------------------------------
+
+#[test]
+fn create_entity_successfully() {
+    with_test_externalities(|| {
+        let class_id = create_class();
+        let entity_id_1 = TestModule::next_entity_id();
+        assert_ok!(TestModule::create_entity(class_id,), entity_id_1);
+        // TODO assert entity from storage
+        assert_eq!(TestModule::next_entity_id(), entity_id_1 + 1);
+    })
+}
+
+#[test]
+fn cannot_create_entity_with_unknown_class_id() {
+    with_test_externalities(|| {
+        assert_err!(
+            TestModule::create_entity(UNKNOWN_CLASS_ID,),
+            ERROR_CLASS_NOT_FOUND
+        );
+    })
+}
+
+// Add schema support to entity
+// --------------------------------------
+
+#[test]
+fn cannot_add_schema_to_entity_when_entity_not_found() {
+    with_test_externalities(|| {
+        assert_entity_not_found(TestModule::add_schema_support_to_entity(
+            UNKNOWN_ENTITY_ID,
+            1,
+            vec![],
+        ));
+    })
+}
+
+#[test]
+fn cannot_add_schema_to_entity_when_schema_already_added_to_entity() {
+    with_test_externalities(|| {
+        let (_, schema_id, entity_id) = create_class_with_schema_and_entity();
+
+        // Firstly we just add support for a valid class schema.
+        assert_ok!(TestModule::add_schema_support_to_entity(
+            entity_id,
+            schema_id,
+            vec![bool_prop_value()]
+        ));
+
+        // Secondly we try to add support for the same schema.
+        assert_err!(
+            TestModule::add_schema_support_to_entity(entity_id, schema_id, vec![]),
+            ERROR_SCHEMA_ALREADY_ADDED_TO_ENTITY
+        );
+    })
+}
+
+#[test]
+fn cannot_add_schema_to_entity_when_schema_id_is_unknown() {
+    with_test_externalities(|| {
+        let (_, schema_id, entity_id) = create_class_with_schema_and_entity();
+        let unknown_schema_id = schema_id + 1;
+        assert_err!(
+            TestModule::add_schema_support_to_entity(
+                entity_id,
+                unknown_schema_id,
+                vec![prop_value(0, PropertyValue::None)]
+            ),
+            ERROR_UNKNOWN_CLASS_SCHEMA_ID
+        );
+    })
+}
+
+#[test]
+fn cannot_add_schema_to_entity_when_prop_value_dont_match_type() {
+    with_test_externalities(|| {
+        let (_, schema_id, entity_id) = create_class_with_schema_and_entity();
+        assert_err!(
+            TestModule::add_schema_support_to_entity(
+                entity_id,
+                schema_id,
+                vec![
+                    bool_prop_value(),
+                    prop_value(PROP_ID_U32, PropertyValue::Bool(true))
+                ]
+            ),
+            ERROR_PROP_VALUE_DONT_MATCH_TYPE
+        );
+    })
+}
+
+#[test]
+fn cannot_add_schema_to_entity_when_unknown_internal_entity_id() {
+    with_test_externalities(|| {
+        let (_, schema_id, entity_id) = create_class_with_schema_and_entity();
+        assert_err!(
+            TestModule::add_schema_support_to_entity(
+                entity_id,
+                schema_id,
+                vec![
+                    bool_prop_value(),
+                    prop_value(PROP_ID_INTERNAL, PropertyValue::Internal(UNKNOWN_ENTITY_ID))
+                ]
+            ),
+            ERROR_ENTITY_NOT_FOUND
+        );
+    })
+}
+
+#[test]
+fn cannot_add_schema_to_entity_when_missing_required_prop() {
+    with_test_externalities(|| {
+        let (_, schema_id, entity_id) = create_class_with_schema_and_entity();
+        assert_err!(
+            TestModule::add_schema_support_to_entity(
+                entity_id,
+                schema_id,
+                vec![prop_value(PROP_ID_U32, PropertyValue::Uint32(456))]
+            ),
+            ERROR_MISSING_REQUIRED_PROP
+        );
+    })
+}
+
+#[test]
+fn should_add_schema_to_entity_when_some_optional_props_provided() {
+    with_test_externalities(|| {
+        let (_, schema_id, entity_id) = create_class_with_schema_and_entity();
+        assert_ok!(TestModule::add_schema_support_to_entity(
+            entity_id,
+            schema_id,
+            vec![
+                bool_prop_value(),
+                prop_value(PROP_ID_U32, PropertyValue::Uint32(123)),
+                // Note that an optional internal prop is not provided here.
+            ]
+        ));
+
+        let entity = TestModule::entity_by_id(entity_id);
+        assert_eq!(entity.in_class_schema_indexes, [SCHEMA_ID_0]);
+        assert_eq!(
+            entity.values,
+            vec![
+                bool_prop_value(),
+                prop_value(PROP_ID_U32, PropertyValue::Uint32(123)),
+                prop_value(PROP_ID_INTERNAL, PropertyValue::None),
+            ]
+        );
+    })
+}
+
+// Update entity properties
+// --------------------------------------
+
+#[test]
+fn cannot_update_entity_props_when_entity_not_found() {
+    with_test_externalities(|| {
+        assert_entity_not_found(TestModule::update_entity_property_values(
+            UNKNOWN_ENTITY_ID,
+            vec![],
+        ));
+    })
+}
+
+#[test]
+fn cannot_update_entity_props_when_prop_value_dont_match_type() {
+    with_test_externalities(|| {
+        let entity_id = create_entity_with_schema_support();
+        assert_err!(
+            TestModule::update_entity_property_values(
+                entity_id,
+                vec![prop_value(PROP_ID_BOOL, PropertyValue::Uint32(1))]
+            ),
+            ERROR_PROP_VALUE_DONT_MATCH_TYPE
+        );
+    })
+}
+
+#[test]
+fn cannot_update_entity_props_when_unknown_internal_entity_id() {
+    with_test_externalities(|| {
+        let entity_id = create_entity_with_schema_support();
+        assert_err!(
+            TestModule::update_entity_property_values(
+                entity_id,
+                vec![prop_value(
+                    PROP_ID_INTERNAL,
+                    PropertyValue::Internal(UNKNOWN_ENTITY_ID)
+                )]
+            ),
+            ERROR_ENTITY_NOT_FOUND
+        );
+    })
+}
+
+#[test]
+fn cannot_update_entity_props_when_unknown_entity_prop_id() {
+    with_test_externalities(|| {
+        let entity_id = create_entity_with_schema_support();
+        assert_err!(
+            TestModule::update_entity_property_values(
+                entity_id,
+                vec![prop_value(UNKNOWN_PROP_ID, PropertyValue::Bool(true))]
+            ),
+            ERROR_UNKNOWN_ENTITY_PROP_ID
+        );
+    })
+}
+
+#[test]
+fn update_entity_props_successfully() {
+    with_test_externalities(|| {
+        let entity_id = create_entity_with_schema_support();
+        assert_eq!(
+            TestModule::entity_by_id(entity_id).values,
+            vec![
+                prop_value(PROP_ID_BOOL, PropertyValue::Bool(true)),
+                prop_value(PROP_ID_U32, PropertyValue::None),
+                prop_value(PROP_ID_INTERNAL, PropertyValue::None),
+            ]
+        );
+        assert_ok!(TestModule::update_entity_property_values(
+            entity_id,
+            vec![
+                prop_value(PROP_ID_BOOL, PropertyValue::Bool(false)),
+                prop_value(PROP_ID_U32, PropertyValue::Uint32(123)),
+                prop_value(PROP_ID_INTERNAL, PropertyValue::Internal(entity_id)),
+            ]
+        ));
+        assert_eq!(
+            TestModule::entity_by_id(entity_id).values,
+            vec![
+                prop_value(PROP_ID_BOOL, PropertyValue::Bool(false)),
+                prop_value(PROP_ID_U32, PropertyValue::Uint32(123)),
+                prop_value(PROP_ID_INTERNAL, PropertyValue::Internal(entity_id)),
+            ]
+        );
+    })
+}
+
+// TODO test text max len
+
+// TODO test vec max len
+
+// Delete entity
+// --------------------------------------
+
+// #[test]
+// fn delete_entity_successfully() {
+//     with_test_externalities(|| {
+//         let entity_id = create_entity();
+//         assert_ok!(
+//             TestModule::delete_entity(entity_id),
+//             ()
+//         );
+//     })
+// }
+
+// #[test]
+// fn cannot_delete_entity_when_entity_not_found() {
+//     with_test_externalities(|| {
+//         assert_entity_not_found(
+//             TestModule::delete_entity(UNKNOWN_ENTITY_ID)
+//         );
+//     })
+// }
+
+// #[test]
+// fn cannot_delete_already_deleted_entity() {
+//     with_test_externalities(|| {
+//         let entity_id = create_entity();
+//         let _ok = TestModule::delete_entity(entity_id);
+//         assert_err!(
+//             TestModule::delete_entity(entity_id),
+//             ERROR_ENTITY_ALREADY_DELETED
+//         );
+//     })
+// }