Skip to content

Commit

Permalink
feat: simpler generic types
Browse files Browse the repository at this point in the history
  • Loading branch information
IlyaSemenov committed Jan 3, 2023
1 parent f788514 commit ea3e368
Show file tree
Hide file tree
Showing 16 changed files with 82 additions and 142 deletions.
23 changes: 5 additions & 18 deletions packages/data-cleaner/src/clean/any.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ValidationError } from "../errors/ValidationError"
import { Cleaner } from "../types"
import { getMessage } from "../utils"

export type AnySchema<T, M> = {
export interface AnySchema<T, V = any> {
/** `required: false` - allow undefined values */
required?: boolean
/** `null: true` - allow null values */
Expand All @@ -13,21 +13,10 @@ export type AnySchema<T, M> = {
/** Override flat collector field label, or `null` to omit field label altogether. */
label?: string | null
/** Nested cleaner (called if the validation passes) */
clean?: Cleaner<T, M>
clean?: Cleaner<T, V>
}

/* This breaks reverse type inference in many cases:
& ([M] extends [T]
? {
clean?: Cleaner<T, M>
}
: {
clean: Cleaner<T, M>
})
*/

type WithSchema<C extends Cleaner<any>, S> = C & {
export type WithSchema<C extends Cleaner<any>, S> = C & {
schema: S
}

Expand All @@ -36,9 +25,7 @@ export function setSchema<C extends Cleaner<any>, S>(fn: C, schema: S) {
return fn as WithSchema<C extends WithSchema<infer OC, any> ? OC : C, S>
}

export function cleanAny<T = any, M = any>(
schema: AnySchema<T, M> = {} as AnySchema<T, M>
) {
export function cleanAny<T = any, V = any>(schema: AnySchema<T, V> = {}) {
if (schema.default !== undefined) {
if (schema.required === undefined) {
schema.required = false
Expand All @@ -53,7 +40,7 @@ export function cleanAny<T = any, M = any>(
throw new SchemaError("clean.any with 'default: null' needs 'null: true'")
}
}
const cleaner: Cleaner<T> = function (value, context) {
const cleaner: Cleaner<T, V> = function (value, context) {
if (value === undefined && schema.required !== false) {
throw new ValidationError(
getMessage(context, "required", "Value required.")
Expand Down
23 changes: 9 additions & 14 deletions packages/data-cleaner/src/clean/array.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { ErrorMessages, ValidationError } from "../errors/ValidationError"
import { Cleaner } from "../types"
import { getMessage, LimitTo } from "../utils"
import { getMessage } from "../utils"
import { AnySchema, cleanAny, setSchema } from "./any"

type TypeM<T, E> = LimitTo<T, E[] | null | undefined>

export interface ArraySchema<T, E, M extends TypeM<T, E> = TypeM<T, E>>
extends AnySchema<T, M> {
export interface ArraySchema<E, T> extends AnySchema<T, E[]> {
/** Individual element cleaner. */
element?: Cleaner<E>
/** Minimum allowed number of elements. */
Expand All @@ -15,17 +12,15 @@ export interface ArraySchema<T, E, M extends TypeM<T, E> = TypeM<T, E>>
max?: number
}

export function cleanArray<
E = any,
T = E[],
M extends TypeM<T, E> = TypeM<T, E>
>(schema: ArraySchema<T, E, M> = {}) {
const cleaner = cleanAny<T>({
export function cleanArray<E = any, T = E[], V = any>(
schema: ArraySchema<E, T> = {}
) {
const cleaner = cleanAny<T, V>({
required: schema.required,
default: schema.default,
null: schema.null,
async clean(value, context) {
let res: M = value
let res: any = value
if (!(res === undefined || res === null)) {
if (!Array.isArray(res)) {
throw new ValidationError(
Expand Down Expand Up @@ -68,10 +63,10 @@ export function cleanArray<
if (errors.length) {
throw new ValidationError(errors)
}
res = cleanedArray as M
res = cleanedArray
}
}
return schema.clean ? schema.clean(res, context) : (res as unknown as T)
return schema.clean ? schema.clean(res, context) : res
},
})
return setSchema(cleaner, schema)
Expand Down
23 changes: 9 additions & 14 deletions packages/data-cleaner/src/clean/boolean.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,35 @@
import { ValidationError } from "../errors/ValidationError"
import { getMessage, LimitTo } from "../utils"
import { getMessage } from "../utils"
import { AnySchema, cleanAny, setSchema } from "./any"

export type BooleanSchema<T, M extends TypeM<T> = TypeM<T>> = AnySchema<
T,
M
> & {
export interface BooleanSchema<T> extends AnySchema<T, boolean> {
/** No strict type check, convert value with `!!value` */
cast?: boolean
/** Return `undefined` for `false` */
omit?: boolean
}

type TypeM<T> = LimitTo<T, boolean | null | undefined>

export function cleanBoolean<T = boolean, M extends TypeM<T> = TypeM<T>>(
schema: BooleanSchema<T, M> = {} as BooleanSchema<T, M>
export function cleanBoolean<T = boolean, V = any>(
schema: BooleanSchema<T> = {}
) {
const cleaner = cleanAny<T>({
const cleaner = cleanAny<T, V>({
required: schema.required,
default: schema.default,
null: schema.null,
clean(value, context) {
let res: M = value
let res: any = value
if (!(res === undefined || res === null)) {
if (typeof res !== "boolean" && schema.cast !== true) {
throw new ValidationError(
getMessage(context, "invalid", "Invalid value.")
)
}
res = !!res as M // TODO: only do this if not boolean
res = !!res
if (res === false && schema.omit === true) {
res = undefined as M
res = undefined
}
}
return schema.clean ? schema.clean(res, context) : (res as unknown as T)
return schema.clean ? schema.clean(res, context) : res
},
})
return setSchema(cleaner, schema)
Expand Down
18 changes: 7 additions & 11 deletions packages/data-cleaner/src/clean/date.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { SchemaError } from "../errors/SchemaError"
import { ValidationError } from "../errors/ValidationError"
import { getMessage, LimitTo } from "../utils"
import { getMessage } from "../utils"
import { setSchema } from "./any"
import { cleanString, StringSchema } from "./string"

type TypeM<T> = LimitTo<T, string | Date | null | undefined>

export interface DateSchema<T>
extends Omit<StringSchema<T>, "cast" | "regexp"> {
/**
Expand All @@ -18,7 +16,7 @@ export interface DateSchema<T>
format?: null | "iso"
}

export function cleanDate<T = string, M extends TypeM<T> = TypeM<T>>(
export function cleanDate<T = Date | string, V = any>(
schema: DateSchema<T> = {}
) {
if (
Expand All @@ -33,12 +31,12 @@ export function cleanDate<T = string, M extends TypeM<T> = TypeM<T>>(
)
}
// TODO: don't allow weird combinations e.g. { format: undefined, blank: true }
const cleaner = cleanString<T>({
const cleaner = cleanString<T, V>({
...schema,
cast: true, // TODO: double check this
regexp: undefined,
clean(value, context) {
let res: M = value as M
let res: any = value
if (res) {
const date = new Date(res as string)
if (isNaN(date.getTime())) {
Expand All @@ -49,14 +47,12 @@ export function cleanDate<T = string, M extends TypeM<T> = TypeM<T>>(
if (schema.format === null) {
// ok
} else if (schema.format === "iso") {
res = date.toISOString() as M
res = date.toISOString()
} else {
res = date as M
res = date
}
}
return schema.clean
? schema.clean(res as any, context)
: (res as unknown as T)
return schema.clean ? schema.clean(res, context) : res
},
})
return setSchema(cleaner, schema)
Expand Down
9 changes: 4 additions & 5 deletions packages/data-cleaner/src/clean/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { cleanString, StringSchema } from "./string"

export type EmailSchema<T> = StringSchema<T>

export function cleanEmail<T = string>(schema: EmailSchema<T> = {}) {
const cleaner = cleanString<T>({
export function cleanEmail<T = string, V = any>(schema: EmailSchema<T> = {}) {
const cleaner = cleanString<T, V>({
...schema,
cast: true, // why?
clean(value, context) {
Expand All @@ -17,9 +17,8 @@ export function cleanEmail<T = string>(schema: EmailSchema<T> = {}) {
getMessage(context, "invalid_email", "Invalid e-mail address.")
)
}
return schema.clean
? schema.clean(value, context)
: (value as unknown as T)
const res: any = value
return schema.clean ? schema.clean(res, context) : res
},
})
return setSchema(cleaner, schema)
Expand Down
4 changes: 2 additions & 2 deletions packages/data-cleaner/src/clean/float.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { cleanNumber, NumberSchema } from "./number"

export type FloatSchema<T> = Omit<NumberSchema<T>, "parseNumber">

export function cleanFloat<T = number>(schema: FloatSchema<T> = {}) {
const cleaner = cleanNumber<T>({
export function cleanFloat<T = number, V = any>(schema: FloatSchema<T> = {}) {
const cleaner = cleanNumber<T, V>({
...schema,
parseNumber: parseFloat,
})
Expand Down
6 changes: 4 additions & 2 deletions packages/data-cleaner/src/clean/integer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { cleanNumber, NumberSchema } from "./number"

export type IntegerSchema<T> = Omit<NumberSchema<T>, "parseNumber">

export function cleanInteger<T = number>(schema: IntegerSchema<T> = {}) {
const cleaner = cleanNumber<T>({
export function cleanInteger<T = number, V = any>(
schema: IntegerSchema<T> = {}
) {
const cleaner = cleanNumber<T, V>({
...schema,
parseNumber: parseInt,
})
Expand Down
12 changes: 4 additions & 8 deletions packages/data-cleaner/src/clean/number.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { SchemaError } from "../errors/SchemaError"
import { ValidationError } from "../errors/ValidationError"
import { getMessage, LimitTo } from "../utils"
import { getMessage } from "../utils"
import { AnySchema, cleanAny, setSchema } from "./any"

type TypeM<T> = LimitTo<T, number | null | undefined>

export type NumberSchema<T, M extends TypeM<T> = TypeM<T>> = AnySchema<T, M> & {
export interface NumberSchema<T> extends AnySchema<T, number> {
/** No strict type check, convert value with `parseInt(value)` */
cast?: boolean
/** Minimum allowed value */
Expand All @@ -15,13 +13,11 @@ export type NumberSchema<T, M extends TypeM<T> = TypeM<T>> = AnySchema<T, M> & {
parseNumber: (value: any) => number
}

export function cleanNumber<T = number, M extends TypeM<T> = TypeM<T>>(
schema: NumberSchema<T, M>
) {
export function cleanNumber<T = number, V = any>(schema: NumberSchema<T>) {
if (!schema.parseNumber) {
throw new SchemaError("clean.number needs 'parseNumber'")
}
const cleaner = cleanAny<T>({
const cleaner = cleanAny<T, V>({
required: schema.required,
default: schema.default,
null: schema.null,
Expand Down
20 changes: 10 additions & 10 deletions packages/data-cleaner/src/clean/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type Dict = Record<string, any>

type ParseKeysOptions = boolean | ((key: string) => string[])

export type ObjectSchema<T, M> = AnySchema<T, M> & {
export interface ObjectSchema<M, T> extends AnySchema<T, M> {
/** Create nested objects from keys like `job.position`
*
* `true`: split by dots
Expand All @@ -25,16 +25,17 @@ export type ObjectSchema<T, M> = AnySchema<T, M> & {
parseKeys?: ParseKeysOptions
/** Map of field names to their respective cleaners */
fields: {
[field in keyof M]?: Cleaner<M[field]>
// TODO: properly document or remove (and fix types/tests) the `?` mark
[F in keyof M]?: Cleaner<M[F]>
}
/** Non-field errors will be grouped under this pseudo field key */
nonFieldErrorsKey?: string
/** `groupErrors: true` (default) - group field errors by field name */
groupErrors?: boolean
}

export function cleanObject<M extends Record<string, any>, T = M>(
schema: ObjectSchema<T, M>
export function cleanObject<M extends Record<string, any>, T = M, V = any>(
schema: ObjectSchema<M, T>
) {
if (!schema || typeof schema.fields !== "object") {
throw new SchemaError("clean.object schema must include fields.")
Expand All @@ -43,7 +44,7 @@ export function cleanObject<M extends Record<string, any>, T = M>(
schema.groupErrors !== undefined ? !!schema.groupErrors : true
// TODO: prevent !groupErrors && nonFieldErrorsKey

let cleaner: Cleaner<T> = cleanAny({
let cleaner: Cleaner<T, V> = cleanAny({
required: schema.required,
default: schema.default,
null: schema.null,
Expand Down Expand Up @@ -78,8 +79,9 @@ export function cleanObject<M extends Record<string, any>, T = M>(
const fieldValue = res.hasOwnProperty(field) ? res[field] : undefined
let cleanedFieldValue
try {
cleanedFieldValue = await Promise.resolve(
fieldCleaner(fieldValue, fieldCleanerContext)
cleanedFieldValue = await fieldCleaner(
fieldValue,
fieldCleanerContext
)
} catch (err) {
if (!(err instanceof ValidationError)) {
Expand Down Expand Up @@ -161,9 +163,7 @@ export function cleanObject<M extends Record<string, any>, T = M>(
for (const { field, messages, opts } of collectedErrors) {
let label = opts.label
if (label === undefined) {
const fieldCleaner: any = schema.fields[field]
label =
fieldCleaner && fieldCleaner.schema && fieldCleaner.schema.label
label = (schema.fields[field] as any)?.schema?.label
}
if (label === undefined) {
label = capitalCase(field)
Expand Down
Loading

0 comments on commit ea3e368

Please sign in to comment.