Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix mapper does not support nested mapped types #248

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions packages/io-ts-extra/src/__tests__/mapper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,35 @@ describe('mapper', () => {
expect(BoolToStringArray.encode(['f', 'a', 'l', 's', 'e'])).toEqual(false)
})

it('map decodes nested mapped types', () => {
const Bool_Number = mapper(
t.boolean,
t.number,
b => (b ? 1 : 0),
n => n === 1
)

const Bool_Number_String = mapper(t.string, Bool_Number, JSON.parse, JSON.stringify)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The naming here is confusing. For consistency this should be called String_Bool_Number


expect(Bool_Number_String.decode('true')).toMatchInlineSnapshot(`
_tag: Right
right: 1
`)
})

it('unmap encodes nested mapped types', () => {
const Bool_Number = mapper(
t.boolean,
t.number,
b => (b ? 1 : 0),
n => n === 1
)

const Bool_Number_String = mapper(t.string, Bool_Number, JSON.parse, JSON.stringify)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.


expect(Bool_Number_String.encode(1)).toEqual('true')
})

it('throws helpfully when unmap not implemented', () => {
const BoolToStringArray = mapper(t.boolean, t.array(t.string), b => b.toString().split(''))
expect(() => (BoolToStringArray as any).encode(['f', 'a', 'l', 's', 'e'])).toThrowErrorMatchingInlineSnapshot(`
Expand Down
58 changes: 41 additions & 17 deletions packages/io-ts-extra/src/mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,29 @@ import * as t from 'io-ts'
import {RichError, funcLabel} from './util'
import * as Either from 'fp-ts/lib/Either'
import {pipe} from 'fp-ts/lib/pipeable'
import { flow } from 'fp-ts/lib/function'

export type Decoder<A, O = A, I = unknown> = Omit<t.Type<A, O, I>, 'encode'>
// prettier-ignore
interface Mapper {
<From, ToO, ToA extends ToO | t.Branded<ToO, any>>(from: t.Type<From>, to: t.Type<ToA, ToO>, map: (f: From) => ToO): Decoder<ToA, From> & {from: From; to: ToA};
<From, ToO, ToA extends ToO | t.Branded<ToO, any>>(from: t.Type<From>, to: t.Type<ToA, ToO>, map: (f: From) => ToO, unmap: (t: ToA) => From): t.Type<ToA, From> & {from: From; to: ToA};
<
TFrom extends t.Any,
TTo extends t.Any,
>(
from: TFrom,
to: TTo,
map: (f: t.TypeOf<TFrom>) => t.OutputOf<TTo>,
unmap: (t: t.TypeOf<TTo>) => t.TypeOf<TFrom>
): t.Type<t.TypeOf<TTo>, t.TypeOf<TFrom>> & {from: t.TypeOf<TFrom>; to: t.TypeOf<TTo>};

<
TFrom extends t.Any,
TTo extends t.Any
>(
from: TFrom,
to: TTo,
map: (f: t.TypeOf<TFrom>) => t.OutputOf<TTo>
): Decoder<t.TypeOf<TTo>, t.TypeOf<TFrom>> & {from: t.TypeOf<TFrom>; to: t.TypeOf<TTo>};
}

/**
Expand All @@ -30,14 +47,17 @@ interface Mapper {
* @param map transform (decode) a `from` type to a `to` type
* @param unmap transfrom a `to` type back to a `from` type
*/
export const mapper: Mapper = <From, To>(
from: t.Type<From>,
to: t.Type<To>,
map: (f: From) => To,
unmap: (t: To) => From = RichError.thrower('unmapper/encoder not implemented')
export const mapper: Mapper = <TFrom extends t.Any, TTo extends t.Any>(
from: TFrom,
to: TTo,
map: (f: t.TypeOf<TFrom>) => t.OutputOf<TTo>,
unmap: (t: t.TypeOf<TTo>) => t.TypeOf<TFrom> = RichError.thrower('unmapper/encoder not implemented')
) => {
type From = t.TypeOf<typeof from>;
type To = t.TypeOf<typeof to>;

const fail = (s: From, c: t.Context, info: string) =>
t.failure<To>(s, c.concat([{key: `decoder [${funcLabel(map)}]: ${info}`, type: to}]))
t.failure<To>(s, c.concat([{key: `decoder [${funcLabel(map)}]: ${info}`, type: to}]));
const piped = from.pipe(
new t.Type<To, From, From>(
to.name,
Expand All @@ -53,12 +73,15 @@ export const mapper: Mapper = <From, To>(
value => to.validate(value, c)
)
),
unmap
flow(
to.encode,
unmap as any
)
),
`${from.name} |> ${funcLabel(map)} |> ${to.name}`
) as any
return Object.assign(piped, {from, to})
}
) as any;
return Object.assign(piped, {from, to});
};

/**
* A helper for parsing strings into other types. A wrapper around `mapper` where the `from` type is `t.string`.
Expand All @@ -75,8 +98,9 @@ export const mapper: Mapper = <From, To>(
* @param decode transform a string into the target type
* @param encode transform the target type back into a string
*/
export const parser = <ToO, ToA extends ToO | t.Branded<ToO, any>>(
type: t.Type<ToA, ToO>,
decode: (value: string) => ToO,
encode: (value: ToA) => string = String
): t.Type<ToA, string> & {from: string; to: ToA} => mapper(t.string, type, decode, encode)
export const parser = <TTo extends t.Any>(
type: TTo,
decode: (value: string) => t.OutputOf<TTo>,
encode: (value: t.TypeOf<TTo>) => string = String
): t.Type<t.TypeOf<TTo>, string> & {from: string; to: t.TypeOf<TTo>} => mapper(t.string, type, decode, encode)