import { ErrorObject , LiskValidationError , validator , liskSchemaIdentifier , } from '@liskhq/lisk-validator' ; import { objects as objectUtils } from '@liskhq/lisk-utils' ; import { generateKey } from './utils' ; import { readObject , writeObject } from './collection' ; import { Schema , CompiledSchema , CompiledSchemas , CompiledSchemasArray , GenericObject , IteratableGenericObject , ValidatedSchema , SchemaProps , } from './types' ; import { iterator , recursiveTypeCast } from './json_wrapper' ; export const validateSchema = ( schema ) : boolean => { validator . removeSchema ( schema . $id ) ; const schemaToValidate = { ... schema , $schema : schema . $schema ?? liskSchemaIdentifier , } ; const errors = validator . validateSchema ( schemaToValidate ) ; if ( errors . length ) { throw new LiskValidationError ( [ ... errors ] ) ; } try { validator . compile ( schemaToValidate ) ; } finally { validator . removeSchema ( schema . $id ) ; } return true ; } ; export class Codec { private _compileSchemas : CompiledSchemas = { } ; public addSchema ( schema ) { validateSchema ( schema ) ; const schemaName = schema . $id ; this . _compileSchemas [ schemaName ] = this . _compileSchema ( schema as ValidatedSchema , [ ] , [ ] ) ; return true ; } public encode ( schema , message ) { if ( this . _compileSchemas [ schema . $id ] === undefined ) { this . addSchema ( schema ) ; } const compiledSchema = this . _compileSchemas [ schema . $id ] ; const res = writeObject ( compiledSchema , message as GenericObject , [ ] ) ; return Buffer . concat ( res [ 0 ] ) ; } public decode < T > ( schema , message ) { if ( this . _compileSchemas [ schema . $id ] === undefined ) { this . addSchema ( schema ) ; } const compiledSchema = this . _compileSchemas [ schema . $id ] ; const [ res ] = readObject ( message , 0 , compiledSchema , message . length ) ; return ( res as unknown ) as T ; } public decodeJSON < T > ( schema , message ) { const decodedMessage = this . decode ( schema , message ) ; const jsonMessageAsObject = this . toJSON ( schema , decodedMessage ) ; return ( jsonMessageAsObject as unknown ) as T ; } public encodeJSON ( schema , message ) { const objectFromJson = this . fromJSON ( schema , message ) ; return this . encode ( schema , objectFromJson ) ; } public toJSON < T = object > ( schema , message ) { const messageCopy = objectUtils . cloneDeep ( message ) ; ( messageCopy as IteratableGenericObject ) [ Symbol . iterator ] = iterator ; recursiveTypeCast ( 'toJSON' , messageCopy as IteratableGenericObject , ( schema as unknown ) as SchemaProps , [ ] ) ; return ( messageCopy as unknown ) as T ; } public fromJSON < T = object > ( schema , message ) { const messageCopy = objectUtils . cloneDeep ( message ) ; ( messageCopy as IteratableGenericObject ) [ Symbol . iterator ] = iterator ; recursiveTypeCast ( 'fromJSON' , messageCopy as IteratableGenericObject , ( schema as unknown ) as SchemaProps , [ ] ) ; return ( messageCopy as unknown ) as T ; } public clearCache ( ) { this . _compileSchemas = { } ; } private _compileSchema ( schema , compiledSchema , dataPath ) { if ( schema . type === 'object' ) { const { properties } = schema ; if ( properties === undefined ) { throw new Error ( 'Invalid schema. Missing "properties" property' ) ; } for ( const property of Object . values ( properties ) ) { if ( ! ( 'fieldNumber' in property ) ) { throw new Error ( 'Invalid schema. Missing "fieldNumber" in properties' ) ; } } const currentDepthSchema = Object . entries ( properties ) . sort ( ( a , b ) => a [ 1 ] . fieldNumber - b [ 1 ] . fieldNumber ) ; for ( let i = 0 ; i < currentDepthSchema . length ; i += 1 ) { const [ schemaPropertyName , schemaPropertyValue ] = currentDepthSchema [ i ] ; if ( schemaPropertyValue . type === 'object' ) { if ( ! ( 'fieldNumber' in schemaPropertyValue ) ) { throw new Error ( 'Invalid schema. Missing "fieldNumber" in properties' ) ; } dataPath . push ( schemaPropertyName ) ; const nestedSchema = [ { propertyName : schemaPropertyName , schemaProp : { type : schemaPropertyValue . type , fieldNumber : schemaPropertyValue . fieldNumber , } , dataPath : [ ... dataPath ] , binaryKey : generateKey ( schemaPropertyValue ) , } , ] ; const res = this . _compileSchema ( schemaPropertyValue , nestedSchema , dataPath ) ; compiledSchema . push ( res as CompiledSchema [ ] ) ; dataPath . pop ( ) ; } else if ( schemaPropertyValue . type === 'array' ) { if ( schemaPropertyValue . items === undefined ) { throw new Error ( 'Invalid schema. Missing "items" property for Array schema' ) ; } if ( ! ( 'fieldNumber' in schemaPropertyValue ) ) { throw new Error ( 'Invalid schema. Missing "fieldNumber" in properties' ) ; } dataPath . push ( schemaPropertyName ) ; if ( schemaPropertyValue . items . type === 'object' ) { const nestedSchema = [ { propertyName : schemaPropertyName , schemaProp : { type : 'object' , fieldNumber : schemaPropertyValue . fieldNumber , } , dataPath : [ ... dataPath ] , binaryKey : generateKey ( schemaPropertyValue ) , } , ] ; const res = this . _compileSchema ( schemaPropertyValue . items , nestedSchema , dataPath ) ; compiledSchema . push ( [ { propertyName : schemaPropertyName , schemaProp : { type : schemaPropertyValue . type , fieldNumber : schemaPropertyValue . fieldNumber , } , dataPath : [ ... dataPath ] , binaryKey : generateKey ( schemaPropertyValue ) , } , ( res as unknown ) as CompiledSchema , ] ) ; dataPath . pop ( ) ; } else { compiledSchema . push ( [ { propertyName : schemaPropertyName , schemaProp : { type : schemaPropertyValue . type , fieldNumber : schemaPropertyValue . fieldNumber , } , dataPath : [ ... dataPath ] , binaryKey : generateKey ( schemaPropertyValue ) , } , { propertyName : schemaPropertyName , schemaProp : { dataType : schemaPropertyValue . items . dataType , fieldNumber : schemaPropertyValue . fieldNumber , } , dataPath : [ ... dataPath ] , binaryKey : generateKey ( schemaPropertyValue ) , } , ] ) ; dataPath . pop ( ) ; } } else { compiledSchema . push ( { propertyName : schemaPropertyName , schemaProp : schemaPropertyValue , dataPath : [ ... dataPath ] , binaryKey : generateKey ( schemaPropertyValue ) , } ) ; } } } return compiledSchema ; } } export const codec = new Codec ( ) ;