import {ValidationError} from "../../../utils/errors"; import {TokenPropertyPermissionObject} from "../../../substrate/extrinsics/unique/types"; import { BoxedNumberWithDefault, InfixOrUrlOrCidAndHash, LocalizedStringWithDefault, URL_TEMPLATE_INFIX, UrlTemplateString } from "../../types"; import {getKeys} from "../../../tsUtils"; import {Semver} from "../../../utils/semver"; import {LANG_REGEX} from "./constants"; export const isPlainObject = (obj: any, varName: string): obj is Object => { if (typeof obj !== 'object') throw new ValidationError(`${varName} is not an object, got ${typeof obj}: ${obj}`) if (obj === null) throw new ValidationError(`${varName} is a null, should be valid object`) if (obj instanceof Map) throw new ValidationError(`${varName} is a Map, should be plain object`) if (obj instanceof Set) throw new ValidationError(`${varName} is a Set, should be plain object`) if (Array.isArray(obj)) throw new ValidationError(`${varName} is an array, should be plain object`) return true } export const validateNumber = (num: any, shouldBeInteger: boolean, varName: string): num is number => { if (typeof num !== 'number' || isNaN(num)) { throw new ValidationError(`${varName} is not a valid number, got ${num}`) } if (shouldBeInteger && num !== Math.round(num)) { throw new ValidationError(`${varName} is not an integer number, got ${num}`) } return true } export const validateAttributeKey = (num: string | number | symbol, varName: string): boolean => { let isOk = false if (typeof num === 'number') { isOk = num === Math.round(num); } else if (typeof num === 'string') { const parsed = parseFloat(num); isOk = !isNaN(parsed) && parsed === Math.round(parsed); } if (!isOk) { throw new ValidationError(`${varName}["${String(num)}"] is not a valid number key, got ${String(num)}`) } return true } const validateLangCode = (key: string | number | symbol, varName: string): boolean => { if (typeof key !== 'string') { throw new ValidationError(`${varName}: key ${String(key)} should be a string`) } if (!key.match(LANG_REGEX)) { throw new ValidationError(`${varName} should be a valid Language code string (like 'co' or 'ca-ES'), got ${key}`) } return true } export const validateURL = (url: string, varName: string): boolean => { if (typeof url !== 'string') { throw new ValidationError(`${varName} should be a string`) } try { new URL(url) return true } catch (err) { throw new ValidationError(`${varName} should be a valid URL, got ${url}`) } } export const validateAndParseSemverString = (str: string, varName: string): Semver => { if (!Semver.isValid(str)) throw new ValidationError(`${varName} is not a valid semver string (passed ${str})`) return Semver.fromString(str) } export const validateLocalizedStringWithDefault = (dict: any, canHaveLocalization: boolean, varName: string): dict is LocalizedStringWithDefault => { isPlainObject(dict, varName) const keys = getKeys(dict) if (keys.length === 0) { throw new ValidationError(`${varName} is an empty object, should have at least one key`) } if (!dict.hasOwnProperty('_')) { throw new ValidationError(`${varName} is doesn't contain field "_"`) } if (typeof dict._ !== 'string') { throw new ValidationError(`${varName}._ is not a string`) } if (!canHaveLocalization && keys.length !== 1) { throw new ValidationError(`${varName} cannot have localization strings, got object with keys ["${keys.join('", "')}"]`) } for (const key in dict) { if (key === '_') continue validateLangCode(key, `${varName}["${key}"]`) if (typeof dict[key] !== 'string') { throw new ValidationError(`${varName}["${key}"] should be a string, got ${typeof key}: ${key}`) } } return true } export const validateBoxedNumberWithDefault = (dict: BoxedNumberWithDefault, shouldBeInteger: boolean, varName: string): dict is BoxedNumberWithDefault => { isPlainObject(dict, varName) if (getKeys(dict).length === 0) { throw new ValidationError(`${varName} is an empty object, should have at least one key`) } if (!dict.hasOwnProperty('_')) { throw new ValidationError(`${varName} is doesn't contain field "_"`) } validateNumber(dict._, shouldBeInteger, `${varName}._`) for (const key in dict) { if (key === '_') continue } return true } export const validateUrlTemplateString = (str: any, varName: string): str is UrlTemplateString => { const prefix = `TemplateUrlString is not valid, ${varName}` if (typeof str !== 'string') throw new ValidationError(`${prefix} is not a string, got ${str}`) if (str.indexOf(URL_TEMPLATE_INFIX) < 0) throw new ValidationError(`${prefix} doesn't contain "${URL_TEMPLATE_INFIX}", got ${str}`) return true } export const validateUrlWithHashObject = (obj: any, varName: string): obj is InfixOrUrlOrCidAndHash => { isPlainObject(obj, varName) const keysAmount = ['urlInfix', 'url', 'ipfsCid'] .map(field => Number(typeof obj[field] === 'string')) .reduce((prev, curr) => { return prev + curr }, 0) if (keysAmount !== 1) { throw new ValidationError(`${varName} should have one and only one of "urlInfix" or "url" or "ipfsCid" string fields, got ${JSON.stringify(obj)}`) } if (typeof obj.url === 'string') { validateURL(obj.url, `${varName}.url`) } if (obj.hasOwnProperty('hash')) validateFieldByType(obj, 'hash', 'string', false, varName) return true } export const validateFieldByType = (obj: T, key: keyof T, type: string, optional: boolean, varName: string): boolean => { isPlainObject(obj, varName) if (optional) { if (obj.hasOwnProperty(key) && typeof obj[key] !== type) { throw new ValidationError(`${varName}.${String(key)} is passed and not a ${type}, got ${typeof obj[key]}: ${obj[key]}`) } } else { if (!obj.hasOwnProperty(key)) { throw new ValidationError(`${varName}.${String(key)} not found in ${varName}`) } if (typeof obj[key] !== type) { throw new ValidationError(`${varName}.${String(key)} should be a ${type}, got ${typeof obj[key]}: ${obj[key]}`) } } return true } export const validateSingleTokenPropertyPermission = (tpp: any, varName: string): tpp is TokenPropertyPermissionObject => { isPlainObject(tpp, varName) validateFieldByType(tpp, 'key', 'string', false, varName) const permissionVarName = `${varName}.permission` isPlainObject(tpp.permission, permissionVarName) validateFieldByType(tpp.permission, 'mutable', 'boolean', false, permissionVarName) validateFieldByType(tpp.permission, 'collectionAdmin', 'boolean', false, permissionVarName) validateFieldByType(tpp.permission, 'tokenOwner', 'boolean', false, permissionVarName) return true } export const checkSafeFactory = any>(fn: T) => { const returnFn = (...params: Parameters) => { try { return fn(...params as any) } catch { return false as ReturnType } } return returnFn as T } export const validateUrlTemplateStringSafe = checkSafeFactory(validateUrlTemplateString) export const validateURLSafe = checkSafeFactory(validateURL) export const validateLocalizedStringWithDefaultSafe = checkSafeFactory(validateLocalizedStringWithDefault)