node-stone
Version:
A library for creating, validating and verifying stones
301 lines (251 loc) • 12.8 kB
JavaScript
/**
* Validate module contains functions required
* to perform stone comformity checks against the Ellcrys stone
* standard
*/
var Validator = {};
var _ = require('lodash');
var moment = require('moment');
var hasFloat = require('has-float');
module.exports = Validator;
// Ellcrys start time. Stones must never be created before this time
var START_TIME = moment.unix(1453975575);
/**
* Set start time
* @param {Number} unixTime unix time
*/
Validator.setStartTime = function(unixTime) {
START_TIME = moment.unix(unixTime)
}
/**
* Get start time
* @return {Number} start time value
*/
Validator.getStartTime = function() {
return START_TIME.unix();
}
/**
* Given a value, it checks whether value is an array of objects.
* @param {[type]} val value or variable to check
* @return {Boolean} true if value is an array of objects, otherwise, false
*/
Validator.isArrayOfObjects = function(val) {
if (!_.isArray(val)) return false;
for (var i = 0; i < val.length; i++) {
if (!_.isPlainObject(val[i]))
return false;
}
return true;
}
/**
* Given a stone in , it checks whether
* the stone passes for a valid stone.
* @param {object} stoneJSON the stone as a json object
* @return {object|null} null if valid, otherwise an Error object
*/
Validator.validate = function(stoneJSON) {
// expect a json object
if (!_.isPlainObject(stoneJSON)) return new Error("Expects a json object as parameter");
var metaBlock = stoneJSON.meta;
var ownershipBlock = stoneJSON.ownership;
var attributesBlock = stoneJSON.attributes;
var embedsBlock = stoneJSON.embeds;
// meta block is required
if (!metaBlock) {
return new Error("missing `meta` block");
} else {
if (!_.isPlainObject(metaBlock)) return new Error("`meta` block value type is invalid. Expects a JSON object");
var vResult = this.validateMetaBlock(metaBlock);
if (vResult != null) return vResult;
}
// validate ownership block if provided
if (ownershipBlock) {
if (!_.isPlainObject(ownershipBlock)) return new Error("`ownership` block value type is invalid. Expects a JSON object");
if (!_.isEmpty(ownershipBlock)) {
var vResult = this.validateOwnershipBlock(ownershipBlock, metaBlock.id);
if (vResult != null) return vResult;
}
}
// validate attributes block if provided
if (attributesBlock) {
if (!_.isPlainObject(attributesBlock)) return new Error('`attributes` block value type is invalid. Expects a JSON object');
if (!_.isEmpty(attributesBlock)) {
var vResult = this.validateAttributesBlock(attributesBlock, metaBlock.id);
if (vResult != null) return vResult;
}
}
// validate embeds block if provided
if (embedsBlock) {
if (!_.isPlainObject(embedsBlock)) return new Error('`embeds` block value type is invalid. Expects a JSON object');
if (!_.isEmpty(embedsBlock)) {
var vResult = this.validateEmbedsBlock(embedsBlock, metaBlock.id);
if (vResult != null) return vResult;
}
}
return null;
}
/**
* Given a json object, it checks whether the object conforms to the standards a valid meta block.
* Rules:
* * Must not contain unknown properties
* * A valid meta block must contain id, type and created_at properties.
* * id must be string and 40 characters in length
* * type must be string
* * created_at must be a number and a valid unix date in the past but not beyond a start/launch time
*
* @param {object} meta an object that represents a stone's meta information
* @return {object|null} will return Error if validation fails or null if no error
*/
Validator.validateMetaBlock = function (meta) {
// expect a json object
if (!_.isPlainObject(meta)) return new Error("`meta` block value type is invalid. Expects a JSON object");
// must reject unexpected properties
var accetableProps = ["id", "type", "created_at"];
var unexpectedProps = _.difference(Object.keys(meta), accetableProps);
if (unexpectedProps.length > 0) {
return new Error('`'+unexpectedProps[0] + '` property is unexpected in `meta` block');
}
// must contain acceptable properties such as id, type and created_at
for (var i = 0; i < accetableProps.length; i++) {
if (_.indexOf(Object.keys(meta), accetableProps[i]) === -1) {
return new Error('`meta` block is missing `'+accetableProps[i]+'` property');
}
}
// id property must be a string and 40 characters in length
if ("string" !== typeof meta.id) return new Error('`meta.id` value type is invalid. Expects string value');
if (meta.id.length !== 40) return new Error('`meta.id` must have 40 characters. Preferrable a UUIDv4 SHA1 hashed string');
// type property must be a string
if ("string" !== typeof meta.type) return new Error('`meta.type` value type is invalid. Expects string value');
// created_at must be a number and a valid unix date in the past but not before a start/launch time
if (!_.isNumber(meta.created_at)) return new Error('`meta.created_at` value type is invalid. Expects an integer');
if (moment.unix(meta.created_at).isBefore(START_TIME)) return new Error('`meta.created_at` value is too far in the past. Expects unix time on or after ' + START_TIME.format());
if (moment.unix(meta.created_at).isAfter(moment())) return new Error('`meta.created_at` value cannot be a unix time in the future');
return null
}
/**
* Given an array of stones, it will validate every stone. To prevent
* deeper levels of validation of nested stones found in individual stone embeds block,
* it will temporarily remove the embeds property of each stone, validate it and reassign
* the removed embeds property.
* Rules:
* * expects a json object as parameter
* * must not contain expected properties other than `ref_id` and `data`
* * ref_id must be set and should have a string value
* * ref_id must be equal to meta id
* * data property must be set and value type must be an array of json objects
*
* @param {object} embeds an object that represents a stone's embeds information
* @param {string} metaID id property in meta block
* @return {object|null} will return Error if validation fails or null if no error
*/
Validator.validateEmbedsBlock = function (embeds, metaID) {
// expect a json object
if (!_.isPlainObject(embeds)) return new Error('`embeds` block value type is invalid. Expects a JSON object');
// must reject unexpected properties
var unexpectedProps = _.difference(Object.keys(embeds), ["ref_id", "data"]);
if (unexpectedProps.length > 0) {
return new Error('`'+unexpectedProps[0] + '` property is unexpected in `embeds` block');
}
// ref id property must be set and value type must be string
if (!embeds.ref_id) return new Error('`embeds` block is missing `ref_id` property');
if ("string" !== typeof embeds.ref_id) return new Error('`embeds.ref_id` value type is invalid. Expects string value');
// ref id must be equal to stone id
if (embeds.ref_id !== metaID) return new Error('`embeds.ref_id` not equal to `meta.id`');
// data property must be set and value type must be an array of objects
if (!embeds.data) return new Error('`embeds` block is missing `data` property');
if (!_.isArray(embeds.data) || !this.isArrayOfObjects(embeds.data)) return new Error("`embeds.data` value type is invalid. Expects an array of JSON objects");
var embeds = embeds.data;
var _embeds = null;
for (var i = 0; i < embeds.length; i++) {
// clone embeds info if available
if (embeds[i].embeds) {
_embeds = _.cloneDeep(embeds[i].embeds);
embeds[i].embeds = null;
}
var result = this.validate(embeds[i]);
if (result instanceof Error) {
return new Error('unable to validate embed at index '+i+'. Reason: ' + result.message);
}
// reinitialize embeds if held in _embeds
if (_embeds)
embeds[i].embeds = _embeds;
_embeds = null;
}
return null
}
/**
* Given a json object, it checks whether the object conforms to the standards an attributes block.
* Rules:
* * Must not contain unknown properties
* * A valid attributes block must contain ref_id and data properties.
* * ref_id property must be a string
*
* @param {object} attributes an object that represents a stone's attributes information
* @param {string} metaID id property in meta block
* @return {object|null} will return Error if validation fails or null if no error
*/
Validator.validateAttributesBlock = function (attributes, metaID) {
// expect a json object
if (!_.isPlainObject(attributes)) return new Error("ownership` block value type is invalid. Expects a JSON object");
// must reject unexpected properties
var unexpectedProps = _.difference(Object.keys(attributes), ["ref_id", "data"]);
if (unexpectedProps.length > 0) return new Error('`'+unexpectedProps[0] + '` property is unexpected in `attributes` block');
// ref id property must be set and it's value type must be string
if (!attributes.ref_id) return new Error('`attributes` block is missing `ref_id` property');
if ("string" !== typeof attributes.ref_id) return new Error('`attributes.ref_id` value type is invalid. Expects string value');
// ref id must be equal to stone id
if (attributes.ref_id !== metaID) return new Error('`attributes.ref_id` not equal to `meta.id`');
// since ref id is set, then data property is required
if (!attributes.data) return new Error('`attributes` block is missing `data` property');
return null
}
/**
* Given a json object, it checks whether the object conforms to the standards a valid ownership block.
* Rules:
* * Must not contain unknown properties
* * Must contain ref id and it must be a string
* * A valid ownership block can only contain ref_id, type, sole and status properties.
* * ownership.ref_id must be set and value type must be string
* * ref_id must be equal to the meta id
* * ownership.type must be set, value type must be a string and value must be known
* - if ownership.type is 'sole':
* - ownership.sole must be set to an object
* - ownership.sole.address_id must be set and it must be a string
* * ownership.status is optional, but if set
* - ownership.status must be a string value. The value must also be known
*
* @param {object} ownership an object that represents a stone's ownership information
* @param {string} metaID id property in meta block
* @return {null|Error} will return Error if validation fails or null if no error
*/
Validator.validateOwnershipBlock = function (ownership, metaID) {
// expect a json object
if (!_.isPlainObject(ownership)) return new Error("`ownership` block value type is invalid. Expects a JSON object");
// must reject unexpected properties
var unexpectedProps = _.difference(Object.keys(ownership), ["ref_id", "type", "sole", "status"]);
if (unexpectedProps.length > 0) return new Error('`'+unexpectedProps[0] + '` property is unexpected in `ownership` block');
// ref id property must be set and it's value type must be string
if (!ownership.ref_id) return new Error('`ownership` block is missing `ref_id` property');
if ("string" !== typeof ownership.ref_id) return new Error('`ownership.ref_id` value type is invalid. Expects string value');
// ref id must be equal to stone id
if (ownership.ref_id !== metaID) return new Error('`ownership.ref_id` not equal to `meta.id`');
// type property must be set and it's value type must be string
if (!ownership.type) return new Error('`ownership` block is missing `type` property');
if ("string" !== typeof ownership.type) return new Error('`ownership.type` value type is invalid. Expects string value');
// ownership.type must contain a known value
if (_.indexOf(["sole"], ownership.type) === -1) return new Error('`ownership.type` property has unexpected value');
// when ownership.type is `sole`, `sole` property is required.
// `ownership.sole` must be a json object. and must have `address_id` property.
// `ownership.sole.address_id value type must be string.`
if (ownership.type === 'sole' && !ownership.sole) return new Error('`ownership` block is missing `sole` property');
if (!_.isPlainObject(ownership.sole)) return new Error('`ownership.sole` value type is invalid. Expects a JSON object');
if (!ownership.sole.address_id) return new Error('`ownership.sole` property is missing `address_id` property');
if ("string" !== typeof ownership.sole.address_id) return new Error('`ownership.sole.address_id` value type is invalid. Expects string value');
// if `status` property is set, it's value type must be string
// and it's value must be a known value
if (ownership.status) {
if ("string" !== typeof ownership.status) return new Error('`ownership.status` value type is invalid. Expects string value');
if (_.indexOf(["transferred"], ownership.status) === -1) return new Error('`ownership.status` property has unexpected value');
}
return null
}