Skip to content

Commit

Permalink
feat: add ArraySchema
Browse files Browse the repository at this point in the history
  • Loading branch information
ASafaeirad committed Dec 27, 2023
1 parent 524a935 commit dd51142
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 7 deletions.
4 changes: 4 additions & 0 deletions src/Config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,21 @@ 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();

expect(config).toEqual({
s: 's',
n: 0,
foo: { foo1: 'foo1', foo2: { foo3: false } },
arr: ['a', 'b'],
});

// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand All @@ -32,6 +35,7 @@ describe('Config', () => {
s: string | undefined;
n: number;
foo: { foo1: string; foo2: { foo3: boolean | undefined } };
arr: string[];
}
>
>;
Expand Down
7 changes: 7 additions & 0 deletions src/Config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Schema, SchemaOptions, SchemaWithDefaultOptions } from './Schema';
import {
ArraySchema,
BooleanSchema,
NumberSchema,
ObjectSchema,
Expand Down Expand Up @@ -55,6 +56,12 @@ export class Config<TSchema extends Record<string, Schema<any, any, boolean>>> {
return new ObjectSchema(schema) as any;
}

static array<T extends Schema<any, any, boolean>>(
schema: T,
): RequiredSchema<ArraySchema<T>> {
return new ArraySchema(schema) as any;
}

public get<TKey extends keyof TSchema>(key: TKey) {
return this.value[key] as Prettify<InferSchema<TSchema>[TKey]>;
}
Expand Down
67 changes: 67 additions & 0 deletions src/Schema/ArraySchema.spec.ts
Original file line number Diff line number Diff line change
@@ -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',
),
);
});
});
32 changes: 32 additions & 0 deletions src/Schema/ArraySchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Guard } from '../Guard';
import { Schema } from './Schema';
import { TypeGuard } from './TypeGuard';

class ArrayGuard implements Guard<any[]> {
constructor(private schema: Schema<any, any, boolean>) {}

validate(input: unknown[], key: string) {
input.forEach((v, i) => {
this.schema.key = `${key}.${i}`;
this.schema.setValue(v);
this.schema.validate();
});
}
}

export class ArraySchema<TInput = any> extends Schema<TInput, TInput, boolean> {
#type = 'array'; // eslint-disable-line no-unused-private-class-members

constructor(schema: Schema<any, any, boolean>) {
super({
typeConstructor: x => x,
initialGuards: [
new TypeGuard('array', x => Array.isArray(x)),
new ArrayGuard(schema),
],
});
schema.options.coerce ??= false;
schema.required();
this.required();
}
}
8 changes: 5 additions & 3 deletions src/Schema/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,25 @@ export class Schema<
#isRequired: TRequired; // eslint-disable-line no-unused-private-class-members

constructor(public options: SchemaOptions<TInput, TValue>) {
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;
}
Expand Down
1 change: 1 addition & 0 deletions src/Schema/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './ArraySchema';
export * from './BooleanSchema';
export * from './NumberSchema';
export * from './ObjectSchema';
Expand Down
12 changes: 8 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Schema } from './Schema';
import type { ArraySchema, Schema } from './Schema';
import type { ObjectSchema } from './Schema/ObjectSchema';

export type Prettify<T> = {
Expand All @@ -18,9 +18,13 @@ export type InferSchema<
> = {
[K in keyof T]: T[K] extends ObjectSchema
? InferObjectSchema<T[K]>
: T[K] extends RequiredSchema<T[K]>
? NonNullable<T[K]['value']>
: T[K]['value'];
: T[K] extends ArraySchema<infer TArrSchema>
? TArrSchema extends Schema
? NonNullable<TArrSchema['value']>[]
: never
: T[K] extends RequiredSchema<T[K]>
? NonNullable<T[K]['value']>
: T[K]['value'];
};

export type Expect<T extends true> = T;
Expand Down

0 comments on commit dd51142

Please sign in to comment.