Skip to content

Instantly share code, notes, and snippets.

@mattdiamond
Last active February 7, 2022 18:34
Show Gist options
  • Save mattdiamond/161ac08a2364c9fb5471949c99c54742 to your computer and use it in GitHub Desktop.
Save mattdiamond/161ac08a2364c9fb5471949c99c54742 to your computer and use it in GitHub Desktop.
Error handling kata
import { traverse as _traverse } from 'fp-ts/Array';
import { Task, chainIOK } from 'fp-ts/Task';
import {
TaskEither,
fromOption,
fromPredicate,
ApplicativePar,
right,
left,
chain,
match,
map
} from 'fp-ts/TaskEither';
import { pipe } from 'fp-ts/function';
import { log } from 'fp-ts/Console';
import { isEmpty } from 'fp-ts/string';
import { findById, Pet } from './repository/pet';
import { SetRequired } from 'type-fest';
interface Request {
body: string;
}
interface Response {
statusCode: number;
data?: {
weight: number; // The average weight
};
error?: string; // An error message if an error occurred
}
export interface AppError {
message: string;
statusCode: number;
}
const traverse = _traverse(ApplicativePar);
function handleRequest (request: Request): Task<void> {
return pipe(
createRequestTask(request.body),
chainIOK(log)
);
}
// map: (a -> b) -> m a -> m b
// chain: (a -> m b) -> m a -> m b
// traverse: (a -> m b) -> t a -> m (t b)
// traverse is kind of like the Promise.all(arr.map(x => ...)) pattern
function createRequestTask (requestBody: string): Task<Response> {
return pipe(
parseInput(requestBody), // TaskEither<AppError, Pet['id'][]>
chain(traverse(fetchPet)), // TaskEither<AppError, Pet[]>
chain(traverse(validatePet)), // TaskEither<AppError, ValidPet[]>
map(getAverageWeight), // TaskEither<AppError, number>
match( // Task<Response>
createErrorResponse,
createSuccessResponse
)
)
}
// Note that the use of "map" above is *not* Array.map!
// It's mapping over the result of the TaskEither computation
function parseInput (input: string): TaskEither<AppError, Pet['id'][]> {
const list = input.split(',');
if (!list.length || list.some(isEmpty)) {
return left({
message: 'Bad input',
statusCode: 400
});
}
return right(list as Pet['id'][]);
}
function fetchPet (id: Pet['id']): TaskEither<AppError, Pet> {
return pipe(
findById(id),
chain(fromOption(() => ({
message: `Could not find pet with ID ${id}`,
statusCode: 404
})))
);
}
type ValidPet = SetRequired<Pet, 'weight'>;
function isValid (pet: Pet): pet is ValidPet {
return pet.weight != null;
}
function handleInvalidPet (pet: Pet): AppError {
return {
message: `Pet is missing weight: ${pet.id}`,
statusCode: 400
};
}
// (pet: Pet) => TaskEither<AppError, ValidPet>
const validatePet = fromPredicate(isValid, handleInvalidPet);
function getAverageWeight (pets: ValidPet[]): number {
return pets.reduce((sum, pet) => sum + pet.weight, 0) / pets.length;
}
function createSuccessResponse (averageWeight: number): Response {
return {
statusCode: 200,
data: {
weight: averageWeight
}
};
}
function createErrorResponse (error: AppError): Response {
return {
statusCode: error.statusCode,
error: error.message
};
}
// main :: Task ()
const main = handleRequest({
body: '1,2,4'
});
// Our program synchronously assembled an asynchronous computation.
// Now we run that computation.
main();
import { Opaque } from 'type-fest';
import { Option, fromNullable } from 'fp-ts/Option';
import { TaskEither, tryCatch } from 'fp-ts/TaskEither';
import type { AppError } from '../app';
export interface Pet {
id: Opaque<string, Pet>;
weight?: number;
}
const testPet = {
id: '1234' as Pet['id'],
weight: 100
};
export function findById(id: string): TaskEither<AppError, Option<Pet>> {
return tryCatch(
() => fetchPetQuery(id).then(fromNullable),
e => ({
message: `DB query failure: ${e}`,
statusCode: 500
})
);
}
function fetchPetQuery (id: string): Promise<Pet | null> {
return Promise.resolve(testPet);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment