diff --git a/src/Config.spec.ts b/src/Config.spec.ts index 2220ec6..51f931f 100644 --- a/src/Config.spec.ts +++ b/src/Config.spec.ts @@ -10,11 +10,13 @@ describe('Config', () => { foo1: Config.string().required(), foo2: Config.object({ foo3: Config.boolean() }), }), + arr: Config.array(Config.string()), }) .parse({ s: 's', n: 0, foo: { foo1: 'foo1', foo2: { foo3: false } }, + arr: ['a', 'b'], }) .getAll(); @@ -22,6 +24,7 @@ describe('Config', () => { s: 's', n: 0, foo: { foo1: 'foo1', foo2: { foo3: false } }, + arr: ['a', 'b'], }); // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -32,6 +35,7 @@ describe('Config', () => { s: string | undefined; n: number; foo: { foo1: string; foo2: { foo3: boolean | undefined } }; + arr: string[]; } > >; diff --git a/src/Config.ts b/src/Config.ts index 53e60f7..9c4761e 100644 --- a/src/Config.ts +++ b/src/Config.ts @@ -1,5 +1,6 @@ import type { Schema, SchemaOptions, SchemaWithDefaultOptions } from './Schema'; import { + ArraySchema, BooleanSchema, NumberSchema, ObjectSchema, @@ -55,6 +56,12 @@ export class Config>> { return new ObjectSchema(schema) as any; } + static array>( + schema: T, + ): RequiredSchema> { + return new ArraySchema(schema) as any; + } + public get(key: TKey) { return this.value[key] as Prettify[TKey]>; } diff --git a/src/Schema/ArraySchema.spec.ts b/src/Schema/ArraySchema.spec.ts new file mode 100644 index 0000000..d9212c7 --- /dev/null +++ b/src/Schema/ArraySchema.spec.ts @@ -0,0 +1,67 @@ +import { ArraySchema } from './ArraySchema'; +import { NumberSchema } from './NumberSchema'; + +describe('Array Schema', () => { + it('should throw error when there is an error in object keys', () => { + const schema = new ArraySchema(new NumberSchema()).setValue('3000'); + schema.key = 'key'; + + expect(() => schema.validate()).toThrow( + new TypeError( + 'Invalid configuration: The "key" expected to be "array" but a "string" was provided', + ), + ); + }); + + it('should not throw with empty array', () => { + const schema = new ArraySchema(new NumberSchema()).setValue([]); + schema.key = 'key'; + + expect(() => schema.validate()).not.toThrow(); + }); + + it('should not throw for valid values', () => { + const schema = new ArraySchema(new NumberSchema({ coerce: true })).setValue( + [3000, '3000'], + ); + schema.key = 'key'; + + expect(() => schema.validate()).not.toThrow(); + }); + + it('should not accept "undefined" or "null"', () => { + const schema = new ArraySchema(new NumberSchema({ coerce: true })).setValue( + [undefined], + ); + schema.key = 'key'; + + const schema2 = new ArraySchema( + new NumberSchema({ coerce: true }), + ).setValue([null]); + schema2.key = 'key'; + + expect(() => schema.validate()).toThrow( + new Error( + 'Invalid configuration: The "key.0" is required but the given value is "undefined"', + ), + ); + expect(() => schema2.validate()).toThrow( + new Error( + 'Invalid configuration: The "key.0" is required but the given value is "null"', + ), + ); + }); + + it('should validate values', () => { + const schema = new ArraySchema( + new NumberSchema({ coerce: false }), + ).setValue([3000, 'string']); + schema.key = 'key'; + + expect(() => schema.validate()).toThrow( + new Error( + 'Invalid configuration: The "key.1" expected to be "number" but a "string" was provided', + ), + ); + }); +}); diff --git a/src/Schema/ArraySchema.ts b/src/Schema/ArraySchema.ts new file mode 100644 index 0000000..3a1c76c --- /dev/null +++ b/src/Schema/ArraySchema.ts @@ -0,0 +1,32 @@ +import type { Guard } from '../Guard'; +import { Schema } from './Schema'; +import { TypeGuard } from './TypeGuard'; + +class ArrayGuard implements Guard { + constructor(private schema: Schema) {} + + validate(input: unknown[], key: string) { + input.forEach((v, i) => { + this.schema.key = `${key}.${i}`; + this.schema.setValue(v); + this.schema.validate(); + }); + } +} + +export class ArraySchema extends Schema { + #type = 'array'; // eslint-disable-line no-unused-private-class-members + + constructor(schema: Schema) { + super({ + typeConstructor: x => x, + initialGuards: [ + new TypeGuard('array', x => Array.isArray(x)), + new ArrayGuard(schema), + ], + }); + schema.options.coerce ??= false; + schema.required(); + this.required(); + } +} diff --git a/src/Schema/Schema.ts b/src/Schema/Schema.ts index a5e3a4d..4d6e66b 100644 --- a/src/Schema/Schema.ts +++ b/src/Schema/Schema.ts @@ -30,23 +30,25 @@ export class Schema< #isRequired: TRequired; // eslint-disable-line no-unused-private-class-members constructor(public options: SchemaOptions) { - this.options.coerce ??= true; this.guards = options.initialGuards; } public setValue(input?: TInput) { this.input = input; + const coerce = this.options.coerce ?? true; const shouldCoerce = typeof input !== typeof this.options.typeConstructor(input!); - if (this.options.coerce && shouldCoerce) + // @ts-expect-error There's a runtime check to ensure + if (this.options.default == null && input === null) this.value = input; + else if (coerce && shouldCoerce) this.value = input != null ? this.options.typeConstructor(input) : this.options.default; // @ts-expect-error There's a runtime check to ensure - else this.value = input ?? this.options.default; + else this.value = input ?? this.options.default ?? input; return this; } diff --git a/src/Schema/index.ts b/src/Schema/index.ts index 60569ac..34b8c13 100644 --- a/src/Schema/index.ts +++ b/src/Schema/index.ts @@ -1,3 +1,4 @@ +export * from './ArraySchema'; export * from './BooleanSchema'; export * from './NumberSchema'; export * from './ObjectSchema'; diff --git a/src/types.ts b/src/types.ts index e31b69c..aecf7f0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import type { Schema } from './Schema'; +import type { ArraySchema, Schema } from './Schema'; import type { ObjectSchema } from './Schema/ObjectSchema'; export type Prettify = { @@ -18,9 +18,13 @@ export type InferSchema< > = { [K in keyof T]: T[K] extends ObjectSchema ? InferObjectSchema - : T[K] extends RequiredSchema - ? NonNullable - : T[K]['value']; + : T[K] extends ArraySchema + ? TArrSchema extends Schema + ? NonNullable[] + : never + : T[K] extends RequiredSchema + ? NonNullable + : T[K]['value']; }; export type Expect = T;