-
Notifications
You must be signed in to change notification settings - Fork 9
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
Feedback #3
Comments
Thank you @gcanti for this proposal! I will play with your approach a bit and see if it suits my initial design goals. I was thinking of the following scenarios when designed this package:
fetcher
.handle(200, cb1, codec1)
.handle(204, cb2, codec2)
.discardRest(() => something) As far as I can see right now, your approach requires full coverage of possible status codes in |
Another approach I was thinking of – make I was playing a bit with similar approach when explored possibility of session types implementation in TS: https://gist.github.com/YBogomolov/c5675b788a7046b6edf9e76ef7337af7. I really like the idea of discoverable APIs, when the compiler guides you towards correct consumption of the API. |
You can handle as many status codes as you wish (full or partial coverage) //
// README example
//
import * as t from 'io-ts'
const User = t.type({ name: t.string })
const Users = t.array(User)
const FourTwoTwo = t.type({ code: t.number, correlationId: t.string })
interface User extends t.TypeOf<typeof User> {}
interface FourTwoTwo extends t.TypeOf<typeof FourTwoTwo> {}
type GetUserResult =
| { code: 200; payload: User[] }
| { code: 400; payload: Error }
| { code: 401; payload: [Error, string] }
| { code: 422; payload: FourTwoTwo }
// v-- ensures total coverage missing handlers ---v
const fetcher1: Fetcher<GetUserResult['code'], string, GetUserResult> = make('myurl', {}, () =>
E.left<string, GetUserResult>('unexpected error')
)
// ^--- error: Type 'Fetcher<never, string, GetUserResult>' is not assignable to type 'Fetcher<200 | 400 | 401 | 422, string, GetUserResult>'
import { flow } from 'fp-ts/lib/function'
import { failure } from 'io-ts/lib/PathReporter'
const decodeError = (errors: t.Errors): string => failure(errors).join('\n')
const handleUsers = flow(
Users.decode,
E.bimap(decodeError, payload => ({ code: 200 as const, payload }))
)
const handleFourTwoTwo = flow(
FourTwoTwo.decode,
E.bimap(decodeError, payload => ({ code: 422 as const, payload }))
)
// partial coverage, fetcher2 is inferred as: Fetcher<200 | 422, string, GetUserResult>
const fetcher2 = make(
'myurl',
{
200: handleUsers,
422: handleFourTwoTwo
},
() => E.left<string, GetUserResult>('unexpected error')
)
// you could also specify which statuses should be handled explicitly using
// a type annotation
const fetcher3 = make<200 | 422 | 400, string, GetUserResult>(
'myurl',
{
200: handleUsers,
422: handleFourTwoTwo
},
() => E.left('unexpected error')
)
// ^--- error: Property '400' is missing in type ... |
I agree, chainable APIs are nice for API discoverability, but there's no need for better discoverability with an API like Isn't the discardRest(restHandler: () => To) What if I can't produce a p.s. Btw I'm not sure about |
I though of restricting |
@gcanti I would like to thank you once again for your feedback and suggested implementation. I played with it a little bit, and written the following: https://gist.github.com/YBogomolov/5de9290f6a7056f17a52b088ac298441 Main ideas:
I would be glad if you take a look at it 🙏 |
👍 much better
Looks like export interface Fetcher<S extends Status, E, A> {
...
readonly onUnexpectedError: Decoder<E, A>;
...
} (maybe should be renamed)
👍
I'd say you pass around partially built handlers export type Handlers<S extends Status, E, A> = Record<S, Decoder<E, A>>
// partially built handlers
const handlers: Handlers<400 | 100, Error, never> = {
400: () => TE.left(new Error('400')),
100: () => TE.left(new Error('100'))
}
const fetcher21 = extend(fetcher2, handlers) Nitpick: now handlers can be overwritten, the name "extend" could be misleading, what about // disallow overwrites ---v
export function extend<OldS extends Status, NewS extends Exclude<Status, OldS>, E, A>(
fetcher: Fetcher<OldS, E, A>,
handlers: Record<NewS, Decoder<E, A>>,
onUnexpectedError?: ReaderTaskEither<FetcherError, E, A>
): Fetcher<OldS | NewS, E, A> |
@gcanti it really clicks now :) I've split
I totally forgot about the
I don't like the long name, but it's quite verbose, so I'd stick with it ¯_(ツ)_/¯ |
Isn't this export function map<S extends Status, E, A, B>(fetcher: Fetcher<S, E, A>, f: (a: A) => B): Fetcher<S, E, B> {
return {
input: fetcher.input,
handlers: pipe(fetcher.handlers, R.map(RTE.map(f))) as Record<S, Decoder<E, B>>,
onUnexpectedError: pipe(fetcher.onUnexpectedError, RTE.map(f)),
init: fetcher.init
}
}
// not interesting since can be derived from `map` and `extend`
export const extendWith = <A, B extends A>(ab: (a: A) => B) => <
OldS extends Status,
NewS extends Exclude<Status, OldS>,
E
>(
fetcher: Fetcher<OldS, E, A>,
handlers: Handlers<NewS, E, B>
): Fetcher<OldS | NewS, E, B> => extend(map(fetcher, ab), handlers) |
Yep, it is. I'll add
And I will test this |
The resulting iteration: https://gist.github.com/YBogomolov/5de9290f6a7056f17a52b088ac298441 |
This is a great thread! I've been thinking about a fetcher that could take a set of decoders as input in form of a tagged union (here and here) but never got to realize my ambition 😅 . I like the change that made Bonus question: would this fetcher work with the experimental modules of io-ts? |
Based on the following (self imposed) requirements:
io-ts
cross-fetch
E.right
)I would propose an API like this:
Example
The text was updated successfully, but these errors were encountered: