UNPKG

fluent-json-schema

Version:
1,026 lines (946 loc) 27.1 kB
'use strict' const { describe, it } = require('node:test') const assert = require('node:assert/strict') const { ObjectSchema } = require('./ObjectSchema') const S = require('./FluentJSONSchema') describe('ObjectSchema', () => { it('defined', () => { assert.notStrictEqual(ObjectSchema, undefined) }) it('Expose symbol', () => { assert.notStrictEqual( ObjectSchema()[Symbol.for('fluent-schema-object')], undefined ) }) describe('constructor', () => { it('without params', () => { assert.deepStrictEqual(ObjectSchema().valueOf(), { // $schema: 'http://json-schema.org/draft-07/schema#', type: 'object' }) }) describe('generatedIds', () => { describe('properties', () => { it('true', () => { assert.deepStrictEqual( ObjectSchema({ generateIds: true }) .prop('prop', S.string()) .valueOf(), { properties: { prop: { $id: '#properties/prop', type: 'string' } }, type: 'object' } ) }) it('false', () => { assert.deepStrictEqual(ObjectSchema().prop('prop').valueOf(), { properties: { prop: {} }, type: 'object' }) }) describe('nested', () => { it('true', () => { assert.deepStrictEqual( ObjectSchema({ generateIds: true }) .prop('foo', ObjectSchema().prop('bar').required()) .valueOf(), { properties: { foo: { $id: '#properties/foo', properties: { bar: { $id: '#properties/foo/properties/bar' } }, required: ['bar'], type: 'object' } }, type: 'object' } ) }) it('false', () => { const id = 'myId' assert.deepStrictEqual( ObjectSchema() .prop('foo', ObjectSchema().prop('bar', S.id(id)).required()) .valueOf(), { properties: { foo: { properties: { bar: { $id: 'myId' } }, required: ['bar'], type: 'object' } }, type: 'object' } ) }) it('invalid', () => { assert.throws( () => ObjectSchema().id(''), (err) => err instanceof S.FluentSchemaError && err.message === 'id should not be an empty fragment <#> or an empty string <> (e.g. #myId)' ) }) }) }) describe('definitions', () => { it('true', () => { assert.deepStrictEqual( ObjectSchema({ generateIds: true }) .definition('entity', ObjectSchema().prop('foo').prop('bar')) .prop('prop', S.ref('entity')) .valueOf(), { definitions: { entity: { $id: '#definitions/entity', properties: { bar: {}, foo: {} }, type: 'object' } }, properties: { prop: { $ref: 'entity' } }, type: 'object' } ) }) it('false', () => { assert.deepStrictEqual( ObjectSchema({ generateIds: false }) .definition('entity', ObjectSchema().id('myCustomId').prop('foo')) .prop('prop', S.ref('entity')) .valueOf(), { definitions: { entity: { $id: 'myCustomId', properties: { foo: {} }, type: 'object' } }, properties: { prop: { $ref: 'entity' } }, type: 'object' } ) }) it('nested', () => { const id = 'myId' assert.deepStrictEqual( ObjectSchema() .prop( 'foo', ObjectSchema().prop('bar', S.string().id(id)).required() ) .valueOf(), { properties: { foo: { properties: { bar: { $id: 'myId', type: 'string' } }, required: ['bar'], type: 'object' } }, type: 'object' } ) }) }) }) }) it('from S', () => { assert.deepStrictEqual(S.object().valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', type: 'object' }) }) it('sets a type object to the prop', () => { assert.strictEqual( ObjectSchema().prop('prop', S.object()).valueOf().properties.prop.type, 'object' ) }) it('valueOf', () => { assert.deepStrictEqual(ObjectSchema().prop('foo', S.string()).valueOf(), { properties: { foo: { type: 'string' } }, type: 'object' }) }) describe('keywords:', () => { describe('id', () => { it('valid', () => { const id = 'myId' assert.deepStrictEqual(ObjectSchema().prop('prop').id(id).valueOf(), { $id: id, properties: { prop: {} }, type: 'object' }) }) describe('nested', () => { it('object', () => { const id = 'myId' assert.deepStrictEqual( ObjectSchema().prop('foo', S.string().id(id)).valueOf().properties .foo, { type: 'string', $id: id } ) }) it('string', () => { assert.deepStrictEqual( ObjectSchema().prop('foo', S.string().title('Foo')).valueOf() .properties, { foo: { type: 'string', title: 'Foo' } } ) }) }) }) describe('properties', () => { it('string', () => { assert.deepStrictEqual( ObjectSchema().prop('prop', S.string()).valueOf().properties, { prop: { type: 'string' } } ) }) describe('nested', () => { it('object', () => { assert.deepStrictEqual( ObjectSchema().prop('foo', ObjectSchema().prop('bar')).valueOf() .properties.foo.properties, { bar: { $id: undefined } } ) }) it('string', () => { assert.deepStrictEqual( ObjectSchema().prop('foo', S.string().title('Foo')).valueOf() .properties, { foo: { type: 'string', title: 'Foo' } } ) }) }) describe('invalid', () => { it('throws an error passing a string as value', () => { assert.throws( () => ObjectSchema().prop('prop', 'invalid'), (err) => err instanceof S.FluentSchemaError && err.message === "'prop' doesn't support value '\"invalid\"'. Pass a FluentSchema object" ) }) it('throws an error passing a number as value', () => { assert.throws( () => ObjectSchema().prop('prop', 555), (err) => err instanceof S.FluentSchemaError && err.message === "'prop' doesn't support value '555'. Pass a FluentSchema object" ) }) it('throws an error passing an array as value', () => { assert.throws( () => ObjectSchema().prop('prop', []), (err) => err instanceof S.FluentSchemaError && err.message === "'prop' doesn't support value '[]'. Pass a FluentSchema object" ) }) }) }) describe('additionalProperties', () => { it('true', () => { const value = true assert.deepStrictEqual( ObjectSchema().additionalProperties(value).prop('prop').valueOf() .additionalProperties, value ) }) it('false', () => { const value = false assert.deepStrictEqual( ObjectSchema().additionalProperties(value).prop('prop').valueOf() .additionalProperties, value ) }) it('object', () => { assert.deepStrictEqual( ObjectSchema().additionalProperties(S.string()).prop('prop').valueOf() .additionalProperties, { type: 'string' } ) }) it('invalid', () => { const value = 'invalid' assert.throws( () => assert.strictEqual( ObjectSchema().prop('prop').additionalProperties(value), value ), (err) => err instanceof S.FluentSchemaError && err.message === "'additionalProperties' must be a boolean or a S" ) }) }) describe('maxProperties', () => { it('valid', () => { const value = 2 assert.deepStrictEqual( ObjectSchema().maxProperties(value).prop('prop').valueOf() .maxProperties, value ) }) it('invalid', () => { const value = 'invalid' assert.throws( () => assert.strictEqual( ObjectSchema().prop('prop').maxProperties(value), value ), (err) => err instanceof S.FluentSchemaError && err.message === "'maxProperties' must be a Integer" ) }) }) describe('minProperties', () => { it('valid', () => { const value = 2 assert.deepStrictEqual( ObjectSchema().minProperties(value).prop('prop').valueOf() .minProperties, value ) }) it('invalid', () => { const value = 'invalid' assert.throws( () => assert.strictEqual( ObjectSchema().prop('prop').minProperties(value), value ), (err) => err instanceof S.FluentSchemaError && err.message === "'minProperties' must be a Integer" ) }) }) describe('patternProperties', () => { it('valid', () => { assert.deepStrictEqual( ObjectSchema() .patternProperties({ '^fo.*$': S.string() }) .prop('foo') .valueOf(), { patternProperties: { '^fo.*$': { type: 'string' } }, properties: { foo: {} }, type: 'object' } ) }) it('invalid', () => { const value = 'invalid' assert.throws( () => assert.strictEqual( ObjectSchema().prop('prop').patternProperties(value), value ), (err) => err instanceof S.FluentSchemaError && err.message === "'patternProperties' invalid options. Provide a valid map e.g. { '^fo.*$': S.string() }" ) }) }) describe('dependencies', () => { it('map of array', () => { assert.deepStrictEqual( ObjectSchema() .dependencies({ foo: ['bar'] }) .prop('foo') .prop('bar') .valueOf(), { dependencies: { foo: ['bar'] }, properties: { bar: {}, foo: {} }, type: 'object' } ) }) it('object', () => { assert.deepStrictEqual( ObjectSchema() .dependencies({ foo: ObjectSchema().prop('bar', S.string()) }) .prop('foo') .valueOf(), { dependencies: { foo: { properties: { bar: { type: 'string' } } } }, properties: { foo: {} }, type: 'object' } ) }) it('invalid', () => { const value = 'invalid' assert.throws( () => assert.strictEqual( ObjectSchema().prop('prop').dependencies(value), value ), (err) => err instanceof S.FluentSchemaError && err.message === "'dependencies' invalid options. Provide a valid map e.g. { 'foo': ['bar'] } or { 'foo': S.string() }" ) }) }) describe('dependentRequired', () => { it('valid', () => { assert.deepStrictEqual( ObjectSchema() .dependentRequired({ foo: ['bar'] }) .prop('foo') .prop('bar') .valueOf(), { type: 'object', dependentRequired: { foo: ['bar'] }, properties: { foo: {}, bar: {} } } ) }) it('invalid', () => { const value = { foo: ObjectSchema().prop('bar', S.string()) } assert.throws( () => { assert.deepStrictEqual( ObjectSchema().dependentRequired(value).prop('foo'), value ) }, (err) => err instanceof S.FluentSchemaError && err.message === "'dependentRequired' invalid options. Provide a valid array e.g. { 'foo': ['bar'] }" ) }) }) describe('dependentSchemas', () => { it('valid', () => { assert.deepStrictEqual( ObjectSchema() .dependentSchemas({ foo: ObjectSchema().prop('bar', S.string()) }) .prop('foo') .valueOf(), { dependentSchemas: { foo: { properties: { bar: { type: 'string' } } } }, properties: { foo: {} }, type: 'object' } ) }) it('invalid', () => { const value = { foo: ['bar'] } assert.throws( () => { assert.deepStrictEqual( ObjectSchema().dependentSchemas(value).prop('foo'), value ) }, (err) => err instanceof S.FluentSchemaError && err.message === "'dependentSchemas' invalid options. Provide a valid schema e.g. { 'foo': S.string() }" ) }) }) describe('propertyNames', () => { it('valid', () => { assert.deepStrictEqual( ObjectSchema() .propertyNames(S.string().format(S.FORMATS.EMAIL)) .prop('foo@bar.com') .valueOf().propertyNames, { format: 'email', type: 'string' } ) }) it('invalid', () => { const value = 'invalid' assert.throws( () => assert.strictEqual( ObjectSchema().prop('prop').propertyNames(value), value ), (err) => err instanceof S.FluentSchemaError && err.message === "'propertyNames' must be a S" ) }) }) }) describe('null', () => { it('sets a type object from the root', () => { assert.strictEqual(S.null().valueOf().type, 'null') }) it('sets a type object from the prop', () => { assert.strictEqual( ObjectSchema().prop('value', S.null()).valueOf().properties.value.type, 'null' ) }) }) describe('definition', () => { it('add', () => { assert.deepStrictEqual( ObjectSchema() .definition('foo', ObjectSchema().prop('foo').prop('bar')) .valueOf().definitions, { foo: { type: 'object', properties: { foo: {}, bar: {} } } } ) }) it('empty props', () => { assert.deepStrictEqual( ObjectSchema().definition('foo').valueOf().definitions, { foo: {} } ) }) it('with id', () => { assert.deepStrictEqual( ObjectSchema() .definition( 'foo', ObjectSchema().id('myDefId').prop('foo').prop('bar') ) .valueOf().definitions, { foo: { $id: 'myDefId', type: 'object', properties: { foo: {}, bar: {} } } } ) }) }) describe('extend', () => { it('extends a simple schema', () => { const base = S.object() .id('base') .title('base') .additionalProperties(false) .prop('foo', S.string().minLength(5).required(true)) const extended = S.object() .id('extended') .title('extended') .prop('bar', S.string().required()) .extend(base) assert.deepStrictEqual(extended.valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', $id: 'extended', title: 'extended', additionalProperties: false, properties: { foo: { type: 'string', minLength: 5 }, bar: { type: 'string' } }, required: ['foo', 'bar'], type: 'object' }) }) it('extends a nested schema', () => { const base = S.object() .id('base') .additionalProperties(false) .prop( 'foo', S.object().prop('id', S.string().format('uuid').required()) ) .prop('str', S.string().required()) .prop('bol', S.boolean().required()) .prop('num', S.integer().required()) const extended = S.object() .id('extended') .prop('bar', S.number()) .extend(base) assert.deepStrictEqual(extended.valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', $id: 'extended', additionalProperties: false, properties: { foo: { type: 'object', properties: { id: { $id: undefined, type: 'string', format: 'uuid' } }, required: ['id'] }, bar: { type: 'number' }, str: { type: 'string' }, bol: { type: 'boolean' }, num: { type: 'integer' } }, required: ['str', 'bol', 'num'], type: 'object' }) }) it('extends a schema with definitions', () => { const base = S.object() .id('base') .additionalProperties(false) .definition('def1', S.object().prop('some')) .definition('def2', S.object().prop('somethingElse')) .prop( 'foo', S.object().prop('id', S.string().format('uuid').required()) ) .prop('str', S.string().required()) .prop('bol', S.boolean().required()) .prop('num', S.integer().required()) const extended = S.object() .id('extended') .definition('def1', S.object().prop('someExtended')) .prop('bar', S.number()) .extend(base) assert.deepStrictEqual(extended.valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', definitions: { def1: { type: 'object', properties: { some: {}, someExtended: {} } }, def2: { type: 'object', properties: { somethingElse: {} } } }, type: 'object', $id: 'extended', additionalProperties: false, properties: { foo: { type: 'object', properties: { id: { $id: undefined, type: 'string', format: 'uuid' } }, required: ['id'] }, str: { type: 'string' }, bol: { type: 'boolean' }, num: { type: 'integer' }, bar: { type: 'number' } }, required: ['str', 'bol', 'num'] }) }) it('extends a schema overriding the props', () => { const base = S.object().prop('reason', S.string().title('title')) const extended = S.object() .prop('other') .prop('reason', S.string().minLength(1)) .extend(base) assert.deepStrictEqual(extended.valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', type: 'object', properties: { other: {}, reason: { title: 'title', type: 'string', minLength: 1 } } }) }) it('extends a chain of schemas overriding the props', () => { const base = S.object().prop('reason', S.string().title('title')) const extended = S.object() .prop('other') .prop('reason', S.string().minLength(1)) .extend(base) const extendedAgain = S.object() .prop('again') .prop('reason', S.string().minLength(2)) .extend(extended) .extend(S.object().prop('multiple')) assert.deepStrictEqual(extendedAgain.valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', type: 'object', properties: { other: {}, again: {}, multiple: {}, reason: { title: 'title', type: 'string', minLength: 2 } } }) }) it('throws an error if a schema is not provided', () => { assert.throws( () => S.object().extend(), (err) => err instanceof S.FluentSchemaError && err.message === "Schema can't be null or undefined" ) }) it('throws an error if a schema is invalid', () => { assert.throws( () => S.object().extend('boom!'), (err) => err instanceof S.FluentSchemaError && err.message === "Schema isn't FluentSchema type" ) }) it('throws an error if you append a new prop after extend', () => { assert.throws( () => { const base = S.object() S.object().extend(base).prop('foo') }, (err) => // err instanceof S.FluentSchemaError && err instanceof TypeError && err.message === 'S.object(...).extend(...).prop is not a function' ) }) }) describe('only', () => { it('returns a subset of the object', () => { const base = S.object() .id('base') .title('base') .prop('foo', S.string()) .prop('bar', S.string()) .prop('baz', S.string()) .prop( 'children', S.object().prop('alpha', S.string()).prop('beta', S.string()) ) const only = base.only(['foo']) assert.deepStrictEqual(only.valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', title: 'base', properties: { foo: { type: 'string' } }, type: 'object' }) }) it('works correctly with required properties', () => { const base = S.object() .id('base') .title('base') .prop('foo', S.string().required()) .prop('bar', S.string()) .prop('baz', S.string().required()) .prop('qux', S.string()) const only = base.only(['foo', 'bar']) assert.deepStrictEqual(only.valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', title: 'base', properties: { foo: { type: 'string' }, bar: { type: 'string' } }, required: ['foo'], type: 'object' }) }) }) describe('without', () => { it('returns a subset of the object', () => { const base = S.object() .id('base') .title('base') .prop('foo', S.string()) .prop('bar', S.string()) .prop('baz', S.string()) .prop( 'children', S.object().prop('alpha', S.string()).prop('beta', S.string()) ) const without = base.without(['foo', 'children']) assert.deepStrictEqual(without.valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', title: 'base', properties: { bar: { type: 'string' }, baz: { type: 'string' } }, type: 'object' }) }) it('works correctly with required properties', () => { const base = S.object() .id('base') .title('base') .prop('foo', S.string().required()) .prop('bar', S.string()) .prop('baz', S.string().required()) .prop('qux', S.string()) const without = base.without(['foo', 'bar']) assert.deepStrictEqual(without.valueOf(), { $schema: 'http://json-schema.org/draft-07/schema#', title: 'base', properties: { baz: { type: 'string' }, qux: { type: 'string' } }, required: ['baz'], type: 'object' }) }) }) describe('raw', () => { it('allows to add a custom attribute', () => { const schema = ObjectSchema().raw({ customKeyword: true }).valueOf() assert.deepStrictEqual(schema, { type: 'object', customKeyword: true }) }) it('Carry raw properties', () => { const schema = S.object() .prop('test', S.ref('foo').raw({ test: true })) .valueOf() assert.deepStrictEqual(schema, { $schema: 'http://json-schema.org/draft-07/schema#', type: 'object', properties: { test: { $ref: 'foo', test: true } } }) }) it('Carry raw properties multiple props', () => { const schema = S.object() .prop('a', S.string()) .prop('test', S.ref('foo').raw({ test: true })) .valueOf() assert.deepStrictEqual(schema, { $schema: 'http://json-schema.org/draft-07/schema#', type: 'object', properties: { a: { type: 'string' }, test: { $ref: 'foo', test: true } } }) }) }) })