diff --git a/.gitignore b/.gitignore index 323e2c5..d0ccd2c 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,5 @@ dist .pnp.* package-lock.json +.vscode +test-storage \ No newline at end of file diff --git a/example.js b/example.js index f61911f..f8b4c18 100644 --- a/example.js +++ b/example.js @@ -2,6 +2,8 @@ const Hyperschema = require('.') const schema = Hyperschema.from('./spec') const ns1 = schema.namespace('namespace-1') +ns1.require('./helpers.js') + const ns2 = schema.namespace('namespace-2') ns2.register({ @@ -60,6 +62,7 @@ ns1.register({ ns1.register({ name: 'basic-struct', flagsPosition: 0, + validator: 'basicStructValidator', fields: [ { name: 'id', diff --git a/helpers.js b/helpers.js new file mode 100644 index 0000000..61a51bd --- /dev/null +++ b/helpers.js @@ -0,0 +1,4 @@ +exports.basicStructValidator = (state, m) => { + if (!m.id) throw new Error('id is required') + if (!m.basicString) throw new Error('basicString is required') +} diff --git a/index.js b/index.js index cb347b2..dd2738a 100644 --- a/index.js +++ b/index.js @@ -131,6 +131,7 @@ class Struct extends ResolvedType { super(hyperschema, fqn, description, existing) this.isStruct = true this.default = null + this.validator = description.validator this.fields = [] this.fieldsByName = new Map() @@ -188,6 +189,10 @@ class Struct extends ResolvedType { } } + getNamespace () { + return this.hyperschema.namespaces.get(this.namespace) + } + toJSON () { return { name: this.name, @@ -203,6 +208,12 @@ class HyperschemaNamespace { constructor (hyperschema, name) { this.hyperschema = hyperschema this.name = name + this.helpers = null + this.id = hyperschema.namespaces.size + } + + require (filename) { + this.helpers = p.resolve(filename) } register (description) { diff --git a/lib/codegen.js b/lib/codegen.js index 94ebca1..981b4eb 100644 --- a/lib/codegen.js +++ b/lib/codegen.js @@ -1,5 +1,6 @@ const gen = require('generate-object-property') const s = require('generate-string') +const p = require('path') module.exports = function generateSchema (hyperschema) { const structs = [] @@ -33,6 +34,13 @@ module.exports = function generateSchema (hyperschema) { str += 'let version = VERSION\n' str += '\n' + for (const ns of hyperschema.namespaces.values()) { + if (!ns.helpers) continue + const helpers = p.relative(p.resolve(hyperschema.dir), ns.helpers).replaceAll('\\', '/') + str += `const helpers${ns.id} = require('${helpers}')\n` + str += '\n' + } + for (let i = 0; i < structs.length; i++) { const struct = structs[i] const { encoder } = structsByName.get(struct.fqn) @@ -102,7 +110,13 @@ module.exports = function generateSchema (hyperschema) { str += '\n' } - const preencode = generateEncode(struct, { preencode: true }) + if (struct.validator) { + str += `// ${struct.fqn}.validator\n` + str += `const ${id}_validator = ${gen('helpers' + struct.getNamespace().id, struct.validator)}\n` + str += '\n' + } + + const preencode = generateEncode(struct, { id, preencode: true }) const encode = generateEncode(struct) const decode = generateDecode(struct) str += `// ${struct.fqn}\n` @@ -119,10 +133,12 @@ module.exports = function generateSchema (hyperschema) { str += '}\n' return str - function generateEncode (struct, { preencode = false } = {}) { + function generateEncode (struct, { id, preencode = false } = {}) { const fn = preencode ? 'preencode' : 'encode' let str = '' + str = preencode && struct.validator ? ` ${id}_validator(state, m)\n` : '' + if (struct.optionals.length >= 1) str += ' let flags = 0\n' for (const field of struct.optionals) { str += ` ${vPrefix(field.version, gen('m', field.name))} flags |= ${field.flag}` diff --git a/test/basic.js b/test/basic.js index 8cf76ca..a6c31e4 100644 --- a/test/basic.js +++ b/test/basic.js @@ -283,6 +283,40 @@ test('basic required field missing', async t => { } }) +test('basic required field missing with validation', async t => { + const schema = await createTestSchema(t) + + await schema.rebuild(schema => { + const ns = schema.namespace('test') + ns.require('test/helpers/index.js') + ns.register({ + name: 'test-struct', + validator: 'testValidator', + fields: [ + { + name: 'field1', + type: 'string', + required: true + } + ] + }) + }) + + t.is(schema.json.version, 1) + t.is(schema.module.version, 1) + + { + const enc = schema.module.resolveStruct('@test/test-struct') + const missingRequired = { field2: 'badField' } + try { + c.encode(enc, missingRequired) + t.fail('expected error') + } catch (e) { + t.is(e.message, 'field1 is required') + } + } +}) + test('basic nested struct, version bump', async t => { const schema = await createTestSchema(t) diff --git a/test/helpers/index.js b/test/helpers/index.js index f273f1e..d268c4f 100644 --- a/test/helpers/index.js +++ b/test/helpers/index.js @@ -48,6 +48,11 @@ async function createTestSchema (t) { return new TestBuilder(dir, t) } +function testValidator (state, m) { + if (!m.field1) throw new Error('field1 is required') +} + module.exports = { - createTestSchema + createTestSchema, + testValidator }