Finalizers
Finalizers are only needed when aspects of the output data cannot be computed until after all children have been transformed.
import { Schema, SchemaResolver } from '@versionzero/schema';
const resolver = new SchemaResolver();
const sum = a => a.reduce((acc, item) => acc + item, 0);
const schema = await resolver.compile(
new Schema('object')
.property('items', new Schema('array')
.property('*', new Schema('number').validator('$positive'))
.validator({$length: {min: 1}})
)
.property('sum', new Schema('number'))
.finalizer(v => {
v.sum = sum(v.items);
return v;
})
.validator(v => {
const computed = sum(v.items);
if (computed !== v.sum) { throw new Error(`incorrect sum ${v.sum}, should be ${computed}!`)}
return v;
})
);
const result = await schema.process({items: [1, 2, 3]});
console.log(result); // -> { items: [ 1, 2, 3 ], sum: 6 }
await schema.validate({items: [1, 2, 3], sum: 5}) // -> throws ValidationError
It is often possible to accomplish the same effects as a finalizer with careful choreography, but it can add complexity. Here's a schema that does the same thing, but implemented without a finalizer:
import { Schema, SchemaResolver } from '@versionzero/schema';
const resolver = new SchemaResolver();
const sum = a => a.reduce((acc, item) => acc + item, 0);
const schema = await resolver.compile(
new Schema('object')
.property('items', new Schema('array')
.opaque() // wait for all child items before saving the array to the parent object
.property('*', new Schema('number').validator('$positive'))
.validator({$length: {min: 1}})
)
.property('sum', new Schema('number').default(0)
.condition({$reference: '^.items'}) // wait until items exists
.transformer([{$reference: '^.items'}, sum]) // then retrieve and transform
)
.validator(v => {
const computed = sum(v.items);
if (computed !== v.sum) { throw new Error(`incorrect sum ${v.sum}, should be ${computed}!`)}
return v;
})
);
const result = await schema.process({items: [1, 2, 3]});
console.log(result); // -> { items: [ 1, 2, 3 ], sum: 6 }
await schema.validate({items: [1, 2, 3], sum: 5}) // -> throws ValidationError
Transformers and opaque schemas can often achieve the same effect as using a finalizer, but finalizers solve the cases where using an opaque schema would interfere with peer dependencies.
Opaque schemas are best reserved for when the output value does not match the schema definition. While they do have the property of deferring final value construction, finalizers are usually a more effective approach to explicitly denote "process only after children are done".