Last active
February 7, 2022 18:34
-
-
Save mattdiamond/161ac08a2364c9fb5471949c99c54742 to your computer and use it in GitHub Desktop.
Error handling kata
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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