@lucaspaganini/value-objects
Version:
TypeScript first validation and class creation library
155 lines (142 loc) • 4.94 kB
text/typescript
import { isDefined, isLeft, isNil } from '../utils'
import { MinSizeError, NotIntegerError, RawTypeError, VOError } from './errors'
import { makeFromRawInit } from './functions'
import { ValueObjectContructor, ValueObjectWorkAround, VOCRaw, VOCRawInit } from './value-object'
export interface VOObjectOptions {
/**
* Maximum inclusive errors to acumulate before throwing.
* Can't be less than zero.
* @default 1
*/
maxErrors?: number
}
type VOObjectSchema<Schema> = { [P in keyof Schema]: ValueObjectContructor }
type VOObjectRawInitSchema<Schema extends VOObjectSchema<Schema>> = {
[P in keyof Schema]: Schema[P] extends ValueObjectContructor ? VOCRawInit<Schema[P]> : never
}
type VOObjectRawSchema<Schema extends VOObjectSchema<Schema>> = {
[P in keyof Schema]: Schema[P] extends ValueObjectContructor ? VOCRaw<Schema[P]> : never
}
export type VOObjectInstance<Schema extends VOObjectSchema<Schema>> = ValueObjectWorkAround<VOObjectRawSchema<Schema>> &
{ [P in keyof Schema]: InstanceType<Schema[P]> }
export interface VOObjectConstructor<Schema extends VOObjectSchema<Schema>> {
new (rawInit: VOObjectRawInitSchema<Schema>): VOObjectInstance<Schema>
}
/**
* Function to create an object wrapper over a given map of value
* object constructors. Useful if you have different classes and
* want to aggregate them.
*
* @template Schema Object mapping to value object constructors.
* @param schema Object mapping to value object constructors.
* @param options Customizations for the returned class constructor.
* @return Class constructor that accepts an object mapping it's keys
* and values to what the inner value object constructors expect.
* Calling {@link VOObjectInstance.valueOf} calls `valueOf()` for all it's inner instances
* and returns them in an object.
*
* @example
* ```typescript
* class Email extends VOString({ ... }) {
* getHost(): string { ... }
* }
*
* class Name extends VOString({ ... }) {}
*
* class Password extends VOString({ ... }) {}
*
* class User extends VOObject({
* name: Name,
* email: Email,
* password: Password
* }) {}
*
* new User({
* name: 'Lucas',
* email: 'me@lucaspaganini.com',
* password: 'Secret123'
* }); // OK
*
* new User({
* name: 'Lucas',
* email: 123,
* password: 'Secret123'
* }); // Compilation error: `.email` expects a string
*
* new User({
* name: 'Lucas',
* email: 'lucaspaganini.com',
* password: 'Secret123'
* }); // Runtime error: `.email` Value doesn't match pattern
*
* const user = new User({
* name: 'Lucas',
* email: 'me@lucaspaganini.com',
* password: 'Secret123'
* });
*
* user.valueOf(); // { name: 'Lucas', email: 'me@lucaspaganini.com', password: 'Secret123' }
* user.email.getHost(); // lucaspaganini.com
* ```
*
* @example
* ```typescript
* class Test {
* constructor(shouldThrow: boolean) {
* if (shouldThrow) throw Error('I was instructed to throw');
* }
* }
* new Test(false); // OK
* new Test(true); // Runtime error: I was instructed to throw
*
* class TestsObject extends VoObject({
* aaa: Test,
* bbb: Test,
* ccc: Test
* }, { maxErrors: 2 }) {}
* new TestsArray({ aaa: false, bbb: false, ccc: false }); // OK
* new TestsArray({ aaa: true, bbb: true, ccc: true }); // Runtime error: ["I was instructed to throw", "I was instructed to throw"]
* ```
*/
export const VOObject = <Schema extends VOObjectSchema<Schema>>(
schema: Schema,
options: VOObjectOptions = {},
): VOObjectConstructor<Schema> => {
if (isDefined(options.maxErrors)) {
if (typeof options.maxErrors !== 'number')
throw new RawTypeError('number', typeof options.maxErrors, 'options.maxErrors')
if (!Number.isInteger(options.maxErrors)) throw new NotIntegerError(options.maxErrors, 'options.maxErrors')
if (options.maxErrors < 0) throw new MinSizeError(options.maxErrors, 0)
}
const maxErrors = options.maxErrors ?? 1
return <any>class {
constructor(raw: VOObjectRawInitSchema<Schema>) {
if (isNil(raw)) throw new RawTypeError('object', typeof raw, 'raw')
const errors: Array<Error> = []
for (const [prop, VO] of Object.entries(schema)) {
const fromRaw = makeFromRawInit(<any>VO)
const either = fromRaw((<any>raw)[prop])
if (isLeft(either)) {
const errorsWithProp = either.left.map(e => {
if (VOError.is(e)) e.path.push(prop)
return e
})
errors.push(...errorsWithProp)
if (errors.length >= maxErrors) throw errors
} else {
;(<any>this)[prop] = either.right
}
}
if (errors.length > 0) throw errors
}
valueOf(): VOObjectRawSchema<Schema> {
return Object.keys(schema).reduce<any>((acc, key) => {
acc[key] = (<any>this)[key].valueOf()
return acc
}, {})
}
toRaw(): VOObjectRawSchema<Schema> {
return this.valueOf()
}
}
}