mirror of
https://github.com/zoriya/typebox.git
synced 2026-06-12 06:41:07 +00:00
Additional Record Constraints (#363)
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sinclair/typebox",
|
||||
"version": "0.26.6",
|
||||
"version": "0.26.7",
|
||||
"description": "JSONSchema Type Builder with Static Type Resolution for TypeScript",
|
||||
"keywords": [
|
||||
"typescript",
|
||||
|
||||
@@ -287,9 +287,10 @@ export namespace TypeCompiler {
|
||||
function* Promise(schema: Types.TPromise<any>, references: Types.TSchema[], value: string): IterableIterator<string> {
|
||||
yield `(typeof value === 'object' && typeof ${value}.then === 'function')`
|
||||
}
|
||||
|
||||
function* Record(schema: Types.TRecord<any, any>, references: Types.TSchema[], value: string): IterableIterator<string> {
|
||||
yield IsRecordCheck(value)
|
||||
if (IsNumber(schema.minProperties)) yield `Object.getOwnPropertyNames(${value}).length >= ${schema.minProperties}`
|
||||
if (IsNumber(schema.maxProperties)) yield `Object.getOwnPropertyNames(${value}).length <= ${schema.maxProperties}`
|
||||
const [keyPattern, valueSchema] = globalThis.Object.entries(schema.patternProperties)[0]
|
||||
const local = PushLocal(`new RegExp(/${keyPattern}/)`)
|
||||
yield `(Object.getOwnPropertyNames(${value}).every(key => ${local}.test(key)))`
|
||||
|
||||
@@ -390,6 +390,12 @@ export namespace ValueErrors {
|
||||
if (!IsRecordObject(value)) {
|
||||
return yield { type: ValueErrorType.Object, schema, path, value, message: `Expected record object` }
|
||||
}
|
||||
if (IsDefined<number>(schema.minProperties) && !(globalThis.Object.getOwnPropertyNames(value).length >= schema.minProperties)) {
|
||||
yield { type: ValueErrorType.ObjectMinProperties, schema, path, value, message: `Expected object to have at least ${schema.minProperties} properties` }
|
||||
}
|
||||
if (IsDefined<number>(schema.maxProperties) && !(globalThis.Object.getOwnPropertyNames(value).length <= schema.maxProperties)) {
|
||||
yield { type: ValueErrorType.ObjectMaxProperties, schema, path, value, message: `Expected object to have less than ${schema.minProperties} properties` }
|
||||
}
|
||||
const [keyPattern, valueSchema] = globalThis.Object.entries(schema.patternProperties)[0]
|
||||
const regex = new RegExp(keyPattern)
|
||||
if (!globalThis.Object.getOwnPropertyNames(value).every((key) => regex.test(key))) {
|
||||
|
||||
@@ -270,6 +270,12 @@ export namespace ValueCheck {
|
||||
if (!IsRecordObject(value)) {
|
||||
return false
|
||||
}
|
||||
if (IsDefined<number>(schema.minProperties) && !(globalThis.Object.getOwnPropertyNames(value).length >= schema.minProperties)) {
|
||||
return false
|
||||
}
|
||||
if (IsDefined<number>(schema.maxProperties) && !(globalThis.Object.getOwnPropertyNames(value).length <= schema.maxProperties)) {
|
||||
return false
|
||||
}
|
||||
const [keyPattern, valueSchema] = globalThis.Object.entries(schema.patternProperties)[0]
|
||||
const regex = new RegExp(keyPattern)
|
||||
if (!globalThis.Object.getOwnPropertyNames(value).every((key) => regex.test(key))) {
|
||||
|
||||
@@ -6,24 +6,36 @@ describe('type/compiler/Record', () => {
|
||||
const T = Type.Record(Type.String(), Type.Number())
|
||||
Ok(T, { a: 1, b: 2, c: 3 })
|
||||
})
|
||||
|
||||
it('Should validate when all property keys are strings', () => {
|
||||
const T = Type.Record(Type.String(), Type.Number())
|
||||
Ok(T, { a: 1, b: 2, c: 3, '0': 4 })
|
||||
})
|
||||
|
||||
it('Should not validate when below minProperties', () => {
|
||||
const T = Type.Record(Type.String(), Type.Number(), { minProperties: 4 })
|
||||
Ok(T, { a: 1, b: 2, c: 3, d: 4 })
|
||||
Fail(T, { a: 1, b: 2, c: 3 })
|
||||
})
|
||||
it('Should not validate when above maxProperties', () => {
|
||||
const T = Type.Record(Type.String(), Type.Number(), { maxProperties: 4 })
|
||||
Ok(T, { a: 1, b: 2, c: 3, d: 4 })
|
||||
Fail(T, { a: 1, b: 2, c: 3, d: 4, e: 5 })
|
||||
})
|
||||
it('Should not validate with illogical minProperties | maxProperties', () => {
|
||||
const T = Type.Record(Type.String(), Type.Number(), { minProperties: 5, maxProperties: 4 })
|
||||
Fail(T, { a: 1, b: 2, c: 3 })
|
||||
Fail(T, { a: 1, b: 2, c: 3, d: 4 })
|
||||
Fail(T, { a: 1, b: 2, c: 3, d: 4, e: 5 })
|
||||
})
|
||||
it('Should validate when specifying string union literals when additionalProperties is true', () => {
|
||||
const K = Type.Union([Type.Literal('a'), Type.Literal('b'), Type.Literal('c')])
|
||||
const T = Type.Record(K, Type.Number())
|
||||
Ok(T, { a: 1, b: 2, c: 3, d: 'hello' })
|
||||
})
|
||||
|
||||
it('Should not validate when specifying string union literals when additionalProperties is false', () => {
|
||||
const K = Type.Union([Type.Literal('a'), Type.Literal('b'), Type.Literal('c')])
|
||||
const T = Type.Record(K, Type.Number(), { additionalProperties: false })
|
||||
Fail(T, { a: 1, b: 2, c: 3, d: 'hello' })
|
||||
})
|
||||
|
||||
it('Should validate for keyof records', () => {
|
||||
const T = Type.Object({
|
||||
a: Type.String(),
|
||||
@@ -33,7 +45,6 @@ describe('type/compiler/Record', () => {
|
||||
const R = Type.Record(Type.KeyOf(T), Type.Number())
|
||||
Ok(R, { a: 1, b: 2, c: 3 })
|
||||
})
|
||||
|
||||
it('Should not validate for unknown key via keyof', () => {
|
||||
const T = Type.Object({
|
||||
a: Type.String(),
|
||||
@@ -43,7 +54,6 @@ describe('type/compiler/Record', () => {
|
||||
const R = Type.Record(Type.KeyOf(T), Type.Number(), { additionalProperties: false })
|
||||
Fail(R, { a: 1, b: 2, c: 3, d: 4 })
|
||||
})
|
||||
|
||||
it('Should should validate when specifying regular expressions', () => {
|
||||
const K = Type.RegEx(/^op_.*$/)
|
||||
const T = Type.Record(K, Type.Number())
|
||||
@@ -53,7 +63,6 @@ describe('type/compiler/Record', () => {
|
||||
op_c: 3,
|
||||
})
|
||||
})
|
||||
|
||||
it('Should should not validate when specifying regular expressions and passing invalid property', () => {
|
||||
const K = Type.RegEx(/^op_.*$/)
|
||||
const T = Type.Record(K, Type.Number())
|
||||
@@ -63,21 +72,17 @@ describe('type/compiler/Record', () => {
|
||||
aop_c: 3,
|
||||
})
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Integer Keys
|
||||
// ------------------------------------------------------------
|
||||
|
||||
it('Should validate when all property keys are integers', () => {
|
||||
const T = Type.Record(Type.Integer(), Type.Number())
|
||||
Ok(T, { '0': 1, '1': 2, '2': 3, '3': 4 })
|
||||
})
|
||||
|
||||
it('Should validate when all property keys are integers, but one property is a string with varying type', () => {
|
||||
const T = Type.Record(Type.Integer(), Type.Number())
|
||||
Fail(T, { '0': 1, '1': 2, '2': 3, '3': 4, a: 'hello' })
|
||||
})
|
||||
|
||||
it('Should not validate if passing a leading zeros for integers keys', () => {
|
||||
const T = Type.Record(Type.Integer(), Type.Number())
|
||||
Fail(T, {
|
||||
@@ -87,7 +92,6 @@ describe('type/compiler/Record', () => {
|
||||
'03': 4,
|
||||
})
|
||||
})
|
||||
|
||||
it('Should not validate if passing a signed integers keys', () => {
|
||||
const T = Type.Record(Type.Integer(), Type.Number())
|
||||
Fail(T, {
|
||||
@@ -97,21 +101,17 @@ describe('type/compiler/Record', () => {
|
||||
'-3': 4,
|
||||
})
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Number Keys
|
||||
// ------------------------------------------------------------
|
||||
|
||||
it('Should validate when all property keys are numbers', () => {
|
||||
const T = Type.Record(Type.Number(), Type.Number())
|
||||
Ok(T, { '0': 1, '1': 2, '2': 3, '3': 4 })
|
||||
})
|
||||
|
||||
it('Should validate when all property keys are numbers, but one property is a string with varying type', () => {
|
||||
const T = Type.Record(Type.Number(), Type.Number())
|
||||
Fail(T, { '0': 1, '1': 2, '2': 3, '3': 4, a: 'hello' })
|
||||
})
|
||||
|
||||
it('Should not validate if passing a leading zeros for numeric keys', () => {
|
||||
const T = Type.Record(Type.Number(), Type.Number())
|
||||
Fail(T, {
|
||||
@@ -121,7 +121,6 @@ describe('type/compiler/Record', () => {
|
||||
'03': 4,
|
||||
})
|
||||
})
|
||||
|
||||
it('Should not validate if passing a signed numeric keys', () => {
|
||||
const T = Type.Record(Type.Number(), Type.Number())
|
||||
Fail(T, {
|
||||
@@ -131,7 +130,6 @@ describe('type/compiler/Record', () => {
|
||||
'-3': 4,
|
||||
})
|
||||
})
|
||||
|
||||
it('Should not validate when all property keys are numbers, but one property is a string with varying type', () => {
|
||||
const T = Type.Record(Type.Number(), Type.Number())
|
||||
Fail(T, { '0': 1, '1': 2, '2': 3, '3': 4, a: 'hello' })
|
||||
|
||||
@@ -6,24 +6,36 @@ describe('type/schema/Record', () => {
|
||||
const T = Type.Record(Type.String(), Type.Number())
|
||||
Ok(T, { a: 1, b: 2, c: 3 })
|
||||
})
|
||||
|
||||
it('Should validate when all property keys are strings', () => {
|
||||
const T = Type.Record(Type.String(), Type.Number())
|
||||
Ok(T, { a: 1, b: 2, c: 3, '0': 4 })
|
||||
})
|
||||
|
||||
it('Should not validate when below minProperties', () => {
|
||||
const T = Type.Record(Type.String(), Type.Number(), { minProperties: 4 })
|
||||
Ok(T, { a: 1, b: 2, c: 3, d: 4 })
|
||||
Fail(T, { a: 1, b: 2, c: 3 })
|
||||
})
|
||||
it('Should not validate when above maxProperties', () => {
|
||||
const T = Type.Record(Type.String(), Type.Number(), { maxProperties: 4 })
|
||||
Ok(T, { a: 1, b: 2, c: 3, d: 4 })
|
||||
Fail(T, { a: 1, b: 2, c: 3, d: 4, e: 5 })
|
||||
})
|
||||
it('Should not validate with illogical minProperties | maxProperties', () => {
|
||||
const T = Type.Record(Type.String(), Type.Number(), { minProperties: 5, maxProperties: 4 })
|
||||
Fail(T, { a: 1, b: 2, c: 3 })
|
||||
Fail(T, { a: 1, b: 2, c: 3, d: 4 })
|
||||
Fail(T, { a: 1, b: 2, c: 3, d: 4, e: 5 })
|
||||
})
|
||||
it('Should validate when specifying string union literals when additionalProperties is true', () => {
|
||||
const K = Type.Union([Type.Literal('a'), Type.Literal('b'), Type.Literal('c')])
|
||||
const T = Type.Record(K, Type.Number())
|
||||
Ok(T, { a: 1, b: 2, c: 3, d: 'hello' })
|
||||
})
|
||||
|
||||
it('Should not validate when specifying string union literals when additionalProperties is false', () => {
|
||||
const K = Type.Union([Type.Literal('a'), Type.Literal('b'), Type.Literal('c')])
|
||||
const T = Type.Record(K, Type.Number(), { additionalProperties: false })
|
||||
Fail(T, { a: 1, b: 2, c: 3, d: 'hello' })
|
||||
})
|
||||
|
||||
it('Should validate for keyof records', () => {
|
||||
const T = Type.Object({
|
||||
a: Type.String(),
|
||||
@@ -33,7 +45,6 @@ describe('type/schema/Record', () => {
|
||||
const R = Type.Record(Type.KeyOf(T), Type.Number())
|
||||
Ok(R, { a: 1, b: 2, c: 3 })
|
||||
})
|
||||
|
||||
it('Should not validate for unknown key via keyof', () => {
|
||||
const T = Type.Object({
|
||||
a: Type.String(),
|
||||
@@ -43,7 +54,6 @@ describe('type/schema/Record', () => {
|
||||
const R = Type.Record(Type.KeyOf(T), Type.Number(), { additionalProperties: false })
|
||||
Fail(R, { a: 1, b: 2, c: 3, d: 4 })
|
||||
})
|
||||
|
||||
it('Should should validate when specifying regular expressions', () => {
|
||||
const K = Type.RegEx(/^op_.*$/)
|
||||
const T = Type.Record(K, Type.Number())
|
||||
@@ -53,7 +63,6 @@ describe('type/schema/Record', () => {
|
||||
op_c: 3,
|
||||
})
|
||||
})
|
||||
|
||||
it('Should should not validate when specifying regular expressions and passing invalid property', () => {
|
||||
const K = Type.RegEx(/^op_.*$/)
|
||||
const T = Type.Record(K, Type.Number())
|
||||
@@ -63,21 +72,17 @@ describe('type/schema/Record', () => {
|
||||
aop_c: 3,
|
||||
})
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Integer Keys
|
||||
// ------------------------------------------------------------
|
||||
|
||||
it('Should validate when all property keys are integers', () => {
|
||||
const T = Type.Record(Type.Integer(), Type.Number())
|
||||
Ok(T, { '0': 1, '1': 2, '2': 3, '3': 4 })
|
||||
})
|
||||
|
||||
it('Should validate when all property keys are integers, but one property is a string with varying type', () => {
|
||||
const T = Type.Record(Type.Integer(), Type.Number())
|
||||
Fail(T, { '0': 1, '1': 2, '2': 3, '3': 4, a: 'hello' })
|
||||
})
|
||||
|
||||
it('Should not validate if passing a leading zeros for integers keys', () => {
|
||||
const T = Type.Record(Type.Integer(), Type.Number())
|
||||
Fail(T, {
|
||||
@@ -87,7 +92,6 @@ describe('type/schema/Record', () => {
|
||||
'03': 4,
|
||||
})
|
||||
})
|
||||
|
||||
it('Should not validate if passing a signed integers keys', () => {
|
||||
const T = Type.Record(Type.Integer(), Type.Number())
|
||||
Fail(T, {
|
||||
@@ -97,21 +101,17 @@ describe('type/schema/Record', () => {
|
||||
'-3': 4,
|
||||
})
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Number Keys
|
||||
// ------------------------------------------------------------
|
||||
|
||||
it('Should validate when all property keys are numbers', () => {
|
||||
const T = Type.Record(Type.Number(), Type.Number())
|
||||
Ok(T, { '0': 1, '1': 2, '2': 3, '3': 4 })
|
||||
})
|
||||
|
||||
it('Should validate when all property keys are numbers, but one property is a string with varying type', () => {
|
||||
const T = Type.Record(Type.Number(), Type.Number())
|
||||
Fail(T, { '0': 1, '1': 2, '2': 3, '3': 4, a: 'hello' })
|
||||
})
|
||||
|
||||
it('Should not validate if passing a leading zeros for numeric keys', () => {
|
||||
const T = Type.Record(Type.Number(), Type.Number())
|
||||
Fail(T, {
|
||||
@@ -121,7 +121,6 @@ describe('type/schema/Record', () => {
|
||||
'03': 4,
|
||||
})
|
||||
})
|
||||
|
||||
it('Should not validate if passing a signed numeric keys', () => {
|
||||
const T = Type.Record(Type.Number(), Type.Number())
|
||||
Fail(T, {
|
||||
@@ -131,7 +130,6 @@ describe('type/schema/Record', () => {
|
||||
'-3': 4,
|
||||
})
|
||||
})
|
||||
|
||||
it('Should not validate when all property keys are numbers, but one property is a string with varying type', () => {
|
||||
const T = Type.Record(Type.Number(), Type.Number())
|
||||
Fail(T, { '0': 1, '1': 2, '2': 3, '3': 4, a: 'hello' })
|
||||
|
||||
@@ -22,6 +22,22 @@ describe('value/check/Record', () => {
|
||||
const result = Value.Check(T, value)
|
||||
Assert.equal(result, true)
|
||||
})
|
||||
it('Should fail when below minProperties', () => {
|
||||
const T = Type.Record(Type.String(), Type.Number(), { minProperties: 4 })
|
||||
Assert.equal(Value.Check(T, { a: 1, b: 2, c: 3, d: 4 }), true)
|
||||
Assert.equal(Value.Check(T, { a: 1, b: 2, c: 3 }), false)
|
||||
})
|
||||
it('Should fail when above maxProperties', () => {
|
||||
const T = Type.Record(Type.String(), Type.Number(), { maxProperties: 4 })
|
||||
Assert.equal(Value.Check(T, { a: 1, b: 2, c: 3, d: 4 }), true)
|
||||
Assert.equal(Value.Check(T, { a: 1, b: 2, c: 3, d: 4, e: 5 }), false)
|
||||
})
|
||||
it('Should fail with illogical minProperties | maxProperties', () => {
|
||||
const T = Type.Record(Type.String(), Type.Number(), { minProperties: 5, maxProperties: 4 })
|
||||
Assert.equal(Value.Check(T, { a: 1, b: 2, c: 3 }), false)
|
||||
Assert.equal(Value.Check(T, { a: 1, b: 2, c: 3, d: 4 }), false)
|
||||
Assert.equal(Value.Check(T, { a: 1, b: 2, c: 3, d: 4, e: 5 }), false)
|
||||
})
|
||||
it('Should fail record with Date', () => {
|
||||
const T = Type.Record(Type.String(), Type.String())
|
||||
const result = Value.Check(T, new Date())
|
||||
@@ -50,7 +66,6 @@ describe('value/check/Record', () => {
|
||||
const result = Value.Check(T, value)
|
||||
Assert.equal(result, false)
|
||||
})
|
||||
|
||||
it('Should fail record with invalid property', () => {
|
||||
const T = Type.Record(
|
||||
Type.String(),
|
||||
@@ -70,7 +85,6 @@ describe('value/check/Record', () => {
|
||||
const result = Value.Check(T, value)
|
||||
Assert.equal(result, false)
|
||||
})
|
||||
|
||||
it('Should pass record with optional property', () => {
|
||||
const T = Type.Record(
|
||||
Type.String(),
|
||||
@@ -89,7 +103,6 @@ describe('value/check/Record', () => {
|
||||
const result = Value.Check(T, value)
|
||||
Assert.equal(result, true)
|
||||
})
|
||||
|
||||
it('Should pass record with optional property', () => {
|
||||
const T = Type.Record(
|
||||
Type.String(),
|
||||
@@ -108,11 +121,9 @@ describe('value/check/Record', () => {
|
||||
const result = Value.Check(T, value)
|
||||
Assert.equal(result, true)
|
||||
})
|
||||
|
||||
// -------------------------------------------------
|
||||
// Number Key
|
||||
// -------------------------------------------------
|
||||
|
||||
it('Should pass record with number key', () => {
|
||||
const T = Type.Record(Type.Number(), Type.String())
|
||||
const value = {
|
||||
@@ -134,11 +145,9 @@ describe('value/check/Record', () => {
|
||||
const result = Value.Check(T, value)
|
||||
Assert.equal(result, false)
|
||||
})
|
||||
|
||||
// -------------------------------------------------
|
||||
// Integer Key
|
||||
// -------------------------------------------------
|
||||
|
||||
it('Should pass record with integer key', () => {
|
||||
const T = Type.Record(Type.Integer(), Type.String())
|
||||
const value = {
|
||||
@@ -149,7 +158,6 @@ describe('value/check/Record', () => {
|
||||
const result = Value.Check(T, value)
|
||||
Assert.equal(result, true)
|
||||
})
|
||||
|
||||
it('Should not pass record with invalid integer key', () => {
|
||||
const T = Type.Record(Type.Integer(), Type.String())
|
||||
const value = {
|
||||
|
||||
Reference in New Issue
Block a user