gcanti / io-ts

Runtime type system for IO decoding/encoding
https://gcanti.github.io/io-ts/
MIT License
6.7k stars 328 forks source link

helper to map input after validation #203

Open aruediger opened 6 years ago

aruediger commented 6 years ago

I'd like to use something similar to mapOutput but want to transform the input during validation. Use case would be

const wrapArray = <RTS extends t.Type<any, any, t.mixed>>(type: RTS, name?: string) =>
  t.union([t.array(type), mapInput(type, x => [x])], name)

so that my input could either be an array of type T or an object of type T but after validation I'd always get an array. I know that this makes serialisation/deserialisation non-roundtrippable but that would be ok.

Are there maybe any higher order function I did miss? Or do I have to create a completely new type?

aruediger commented 6 years ago

FYI: this seems to work for now:

import * as t from 'io-ts'
import { either } from 'ramda'

export class ReadonlyArrayWrapperType<RT extends t.Any, A = any, O = A, I = t.mixed> extends t.Type<
  A,
  O,
  I
> {
  readonly _tag: 'ReadonlyArrayType' = 'ReadonlyArrayType'
  constructor(
    name: string,
    is: ReadonlyArrayWrapperType<RT, A, O, I>['is'],
    validate: ReadonlyArrayWrapperType<RT, A, O, I>['validate'],
    serialize: ReadonlyArrayWrapperType<RT, A, O, I>['encode'],
    readonly type: RT,
  ) {
    super(name, is, validate, serialize)
  }
}

export const readonlyArrayWrapper = <RT extends t.Mixed>(
  type: RT,
  name: string = `ReadonlyArray<${type.name}>`,
): ReadonlyArrayWrapperType<
  RT,
  ReadonlyArray<t.TypeOf<RT>>,
  ReadonlyArray<t.OutputOf<RT>>,
  t.mixed
> => {
  const arrayType = t.array(type)
  return new ReadonlyArrayWrapperType(
    name,
    either(arrayType.is, type.is),
    (m, c) =>
      (Array.isArray(m) ? arrayType.validate(m, c) : type.validate(m, c).map(x => [x])).map(x => {
        if (process.env.NODE_ENV !== 'production') {
          return Object.freeze(x)
        } else {
          return x
        }
      }),
    arrayType.encode as any,
    type,
  )
}
gcanti commented 6 years ago

(Array.isArray(m) ? arrayType.validate(m, c) : type.validate(m, c).map(x => [x]))

@2beaucoup this is problematic if type is in turn an array. I'd do something like

export class ReadonlyArrayWrapperType<RT extends t.Any, A = any, O = A, I = t.mixed> extends t.Type<A, O, I> {
  readonly _tag: 'ReadonlyArrayWrapperType' = 'ReadonlyArrayWrapperType'
  constructor(
    name: string,
    is: ReadonlyArrayWrapperType<RT, A, O, I>['is'],
    validate: ReadonlyArrayWrapperType<RT, A, O, I>['validate'],
    encode: ReadonlyArrayWrapperType<RT, A, O, I>['encode'],
    readonly type: RT
  ) {
    super(name, is, validate, encode)
  }
}

export const readonlyArrayWrapper = <RT extends t.Mixed>(
  type: RT,
  name: string = `ReadonlyArrayWrapperType<${type.name}>`
): ReadonlyArrayWrapperType<RT, ReadonlyArray<t.TypeOf<RT>>, ReadonlyArray<t.OutputOf<RT>>, t.mixed> => {
  const arrayType = t.readonlyArray(type)
  return new ReadonlyArrayWrapperType(
    name,
    arrayType.is,
    (m, c) =>
      arrayType.validate(m, t.appendContext(c, '0', arrayType)).orElse(arrayTypeErrors =>
        type.validate(m, t.appendContext(c, '1', type)).bimap(
          typeErrors => arrayTypeErrors.concat(typeErrors),
          x => {
            if (process.env.NODE_ENV !== 'production') {
              return Object.freeze([x])
            } else {
              return [x]
            }
          }
        )
      ),
    arrayType.encode,
    type
  )
}

Usage

import { PathReporter } from 'io-ts/lib/PathReporter'

const T = readonlyArrayWrapper(t.string)
console.log(PathReporter.report(T.decode('a')))
console.log(PathReporter.report(T.decode(undefined)))
console.log(PathReporter.report(T.decode([1])))
console.log(PathReporter.report(T.decode(1)))

/*
Output:
[ 'No errors!' ]
[ 'Invalid value undefined supplied to : ReadonlyArrayWrapperType<string>/0: ReadonlyArray<string>',
  'Invalid value undefined supplied to : ReadonlyArrayWrapperType<string>/1: string' ]
[ 'Invalid value 1 supplied to : ReadonlyArrayWrapperType<string>/0: ReadonlyArray<string>/0: string',
  'Invalid value [1] supplied to : ReadonlyArrayWrapperType<string>/1: string' ]
[ 'Invalid value 1 supplied to : ReadonlyArrayWrapperType<string>/0: ReadonlyArray<string>',
  'Invalid value 1 supplied to : ReadonlyArrayWrapperType<string>/1: string' ]
*/
aruediger commented 6 years ago

Awesome, thanks a lot @gcanti!

So no higher order function that does the trick I guess.

gcanti commented 6 years ago

do you mean mapInput? I'm not sure how would be its signature

aruediger commented 6 years ago

i'm not sure. i guess it would expect a function that receives the original validator and returns a new, adjusted one.