import { Static, create, RuntypeBase, Codec, createValidationPlaceholder, assertRuntype, } from '../runtype'; import { hasKey } from '../util'; import show from '../show'; import { Failure } from '..'; import { expected, failure, FullError, typesAreNotCompatible, unableToAssign } from '../result'; export type RecordFields = { readonly [_: string]: RuntypeBase }; type RecordStaticType< O extends RecordFields, IsPartial extends boolean, IsReadonly extends boolean, > = IsPartial extends false ? IsReadonly extends false ? { -readonly [K in keyof O]: Static } : { readonly [K in keyof O]: Static } : IsReadonly extends false ? { -readonly [K in keyof O]?: Static } : { readonly [K in keyof O]?: Static }; export interface InternalRecord< O extends RecordFields, IsPartial extends boolean, IsReadonly extends boolean, > extends Codec> { readonly tag: 'object'; readonly fields: O; readonly isPartial: IsPartial; readonly isReadonly: IsReadonly; asPartial(): Partial; asReadonly(): IsPartial extends false ? Obj : Partial; pick( ...keys: TKeys ): InternalRecord, IsPartial, IsReadonly>; omit( ...keys: TKeys ): InternalRecord, IsPartial, IsReadonly>; } export { Obj as Object }; type Obj = InternalRecord; export type Partial = InternalRecord< O, true, IsReadonly >; export function isObjectRuntype( runtype: RuntypeBase, ): runtype is InternalRecord { return ( 'tag' in runtype && (runtype as InternalRecord).tag === 'object' ); } /** * Construct an object runtype from runtypes for its values. */ export function InternalObject( fields: O, isPartial: Part, isReadonly: RO, ): InternalRecord { assertRuntype(...Object.values(fields)); const fieldNames: ReadonlySet = new Set(Object.keys(fields)); const runtype: InternalRecord = create>( 'object', { p: (x, innerValidate, _innerValidateToPlaceholder, _getFields, sealed) => { if (x === null || x === undefined || typeof x !== 'object') { return expected(runtype, x); } if (Array.isArray(x)) { return failure(`Expected ${show(runtype)}, but was an Array`); } return createValidationPlaceholder(Object.create(null), (placeholder: any) => { let fullError: FullError | undefined = undefined; let firstError: Failure | undefined; for (const key in fields) { if (!isPartial || (hasKey(key, x) && x[key] !== undefined)) { const value = isPartial || hasKey(key, x) ? x[key] : undefined; let validated = innerValidate( fields[key], value, sealed && sealed.deep ? { deep: true } : false, ); if (!validated.success) { if (!fullError) { fullError = unableToAssign(x, runtype); } fullError.push(typesAreNotCompatible(`"${key}"`, validated)); firstError = firstError || failure(validated.message, { key: validated.key ? `${key}.${validated.key}` : key, fullError: fullError, }); } else { placeholder[key] = validated.value; } } } if (!firstError && sealed) { for (const key of Object.keys(x)) { if (!fieldNames.has(key) && !sealed.keysFromIntersect?.has(key)) { const message = `Unexpected property: ${key}`; if (!fullError) { fullError = unableToAssign(x, runtype); } fullError.push([message]); firstError = firstError || failure(message, { key: key, fullError: fullError, }); } } } return firstError; }); }, f: () => fieldNames, }, { isPartial, isReadonly, fields, asPartial, asReadonly, pick, omit, show() { const keys = Object.keys(fields); return keys.length ? `{ ${keys .map( k => `${isReadonly ? 'readonly ' : ''}${k}${isPartial ? '?' : ''}: ${show( fields[k], false, )};`, ) .join(' ')} }` : '{}'; }, }, ); return runtype; function asPartial() { return InternalObject(runtype.fields, true, runtype.isReadonly); } function asReadonly(): any { return InternalObject(runtype.fields, runtype.isPartial, true); } function pick( ...keys: TKeys ): InternalRecord, Part, RO> { const newFields: Pick = {} as any; for (const key of keys) { newFields[key] = fields[key]; } return InternalObject(newFields, isPartial, isReadonly); } function omit( ...keys: TKeys ): InternalRecord, Part, RO> { const newFields: Omit = { ...fields } as any; for (const key of keys) { if (key in newFields) delete (newFields as any)[key]; } return InternalObject(newFields, isPartial, isReadonly); } } function Obj(fields: O): Obj { return InternalObject(fields, false, false); } export function ReadonlyObject(fields: O): Obj { return InternalObject(fields, false, true); } export function Partial(fields: O): Partial { return InternalObject(fields, true, false); } export function ReadonlyPartial(fields: O): Partial { return InternalObject(fields, true, true); }