Add RTE exercice (#97)

*  (exo6) add RTE exercice

* 👌 (docs) add more precise doc

* 👌 (imports) add better imports

* ♻️ (imports) use modules
This commit is contained in:
vincentj 2022-11-09 12:10:28 +01:00 committed by GitHub
parent 0f7c7941fe
commit a06d6b2573
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 245 additions and 1 deletions

View File

@ -0,0 +1 @@
export * as Application from './services';

View File

@ -0,0 +1,7 @@
import { TimeService } from './TimeService';
export class NodeTimeService implements TimeService {
public thisYear() {
return new Date().getFullYear();
}
}

View File

@ -0,0 +1,13 @@
import { getReaderMethod } from '../../../utils';
export interface TimeService {
thisYear: () => number;
}
export interface Access {
timeService: TimeService;
}
export const thisYear = getReaderMethod(
({ timeService }: Access) => timeService.thisYear,
);

View File

@ -0,0 +1,2 @@
export * as NodeTimeService from './NodeTimeService';
export * as TimeService from './TimeService';

View File

@ -0,0 +1,20 @@
import { taskEither } from 'fp-ts';
import { pipe } from 'fp-ts/lib/function';
import { TaskEither } from 'fp-ts/lib/TaskEither';
import { User } from '../User';
import { UserNotFoundError } from './readerMethods';
export class InMemoryUserRepository {
protected aggregates: Map<string, User>;
constructor(aggregates: User[]) {
this.aggregates = new Map(aggregates.map(user => [user.id, user]));
}
getById(userId: string): TaskEither<UserNotFoundError, User> {
return pipe(
this.aggregates.get(userId),
taskEither.fromNullable(new UserNotFoundError()),
);
}
}

View File

@ -0,0 +1,2 @@
export * from './InMemoryUserRepository';
export * from './readerMethods';

View File

@ -0,0 +1 @@
export * as Repository from './Repository';

View File

@ -0,0 +1,23 @@
import { reader } from 'fp-ts';
import { pipe } from 'fp-ts/lib/function';
import { ReaderTaskEither } from 'fp-ts/lib/ReaderTaskEither';
import { User } from '../User';
import { InMemoryUserRepository } from './InMemoryUserRepository';
export interface Access {
userRepository: InMemoryUserRepository;
}
export const getById = (
userId: string,
): ReaderTaskEither<Access, UserNotFoundError, User> =>
pipe(
reader.ask<Access>(),
reader.map(({ userRepository }) => userRepository.getById(userId)),
);
export class UserNotFoundError extends Error {
constructor() {
super('User not found');
}
}

View File

@ -0,0 +1,2 @@
export * from './Repository';
export * from './type';

View File

@ -0,0 +1 @@
export * as User from './User';

View File

@ -0,0 +1,5 @@
export type User = {
id: string;
name: string;
bestFriendId: string;
};

1
src/exo6/domain/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './User';

73
src/exo6/exo6.test.ts Normal file
View File

@ -0,0 +1,73 @@
import { either } from 'fp-ts';
import { Application } from './application';
import { User } from './domain';
import {
getCapitalizedUserName,
getConcatenationOfTheBestFriendNameAndUserName,
getConcatenationOfTheTwoUserNames,
getConcatenationOfUserNameAndYear,
} from './exo6';
describe('exo6', () => {
it('should return the capitalized user name', async () => {
const usecase = getCapitalizedUserName({ userId: '1' })({
userRepository: new User.Repository.InMemoryUserRepository([
{ id: '1', name: 'rob', bestFriendId: '' },
]),
});
const result = await usecase();
expect(result).toEqual(either.right('Rob'));
});
it('should return the concatenation of the two capitalized user names', async () => {
const usecase = getConcatenationOfTheTwoUserNames({
userIdOne: '1',
userIdTwo: '2',
})({
userRepository: new User.Repository.InMemoryUserRepository([
{ id: '1', name: 'rob', bestFriendId: '' },
{ id: '2', name: 'scott', bestFriendId: '' },
]),
});
const result = await usecase();
expect(result).toEqual(either.right('robscott'));
});
it('should return the concatenation of the two capitalized user names based on the best friend relation', async () => {
const usecase = getConcatenationOfTheBestFriendNameAndUserName({
userIdOne: '1',
})({
userRepository: new User.Repository.InMemoryUserRepository([
{ id: '1', name: 'rob', bestFriendId: '2' },
{ id: '2', name: 'scott', bestFriendId: '1' },
]),
});
const result = await usecase();
expect(result).toEqual(either.right('robscott'));
});
it('should return the concatenation of the two capitalized user names based on the best friend relation', async () => {
const timeservice = new Application.NodeTimeService.NodeTimeService();
const usecase = getConcatenationOfUserNameAndYear({
userIdOne: '1',
})({
userRepository: new User.Repository.InMemoryUserRepository([
{ id: '1', name: 'rob', bestFriendId: '2' },
{ id: '2', name: 'scott', bestFriendId: '1' },
]),
timeService: timeservice,
});
const result = await usecase();
expect(result).toEqual(either.right(`rob${timeservice.thisYear()}`));
});
});

77
src/exo6/exo6.ts Normal file
View File

@ -0,0 +1,77 @@
// `fp-ts` training Exercise 6
// Introduction to `ReaderTaskEither`
import { ReaderTaskEither } from 'fp-ts/lib/ReaderTaskEither';
import { unimplemented } from '../utils';
import { Application } from './application';
import { User } from './domain';
// In real world applications you will mostly manipulate `ReaderTaskEither` aka `rte` in the use-cases of the application.
// `Reader` -> For dependency injection
// `Task` -> For async operation
// `Either` -> For computations that may fail
//
// Keep in Mind, A ReaderTaskEither is nothing more than a Reader of a Task of an Either
// type ReaderTaskEither<Env, Error, Value> = Reader<Env, Task<Either<Error, Value>>>
//
// The ReaderTaskEither module from fp-ts gives us some useful methods to manipulate it.
// You will learn the usage of the most common in the following usecases.
// In the following usecase, you will learn the usage of `rte.map()`.
// `rte.map()` allows you to perform an operation on the values stored in the current context.
// In the following example, we need to fetch a user by its id and then we want to return its capitalized.
export const getCapitalizedUserName: (args: {
userId: string;
}) => ReaderTaskEither<
User.Repository.Access,
User.Repository.UserNotFoundError,
string
> = unimplemented;
// Sometimes you will need to get multiple data before performing an operation on them.
// In this case, it is very convenient to use the `Do` notation.
//
// The `Do` notation allows you to enrich the context step-by-step by binding the result
// of an effect (in this case a RTE) to a named variable using `rte.apS` or `rte.apSW`.
//
// For example:
// pipe(
// rte.Do,
// rte.apS('myDataOne', DataSource.getById(x)),
// ...
// )
export const getConcatenationOfTheTwoUserNames: (args: {
userIdOne: string;
userIdTwo: string;
}) => ReaderTaskEither<
User.Repository.Access,
User.Repository.UserNotFoundError,
string
> = unimplemented;
// Sometimes, you will need to feed the current context with data that you can only retrieve after performing some operations.
// For example, if you want to fetch the best friend of a user you will have to fetch the first user and then fetch its bestfriend.
// In this case, we will use `rte.bindW()` to use data of the current context (the firstly fetched user)
// to perform a second operation (fetch its bestfriend) and bind the return value to feed the context and use those data.
export const getConcatenationOfTheBestFriendNameAndUserName: (args: {
userIdOne: string;
}) => ReaderTaskEither<
User.Repository.Access,
User.Repository.UserNotFoundError,
string
> = unimplemented;
// Most of the time, you will need to use several external services.
// The challenge of this usecase is to use TimeService in the flow of our `rte`
type Dependencies = User.Repository.Access & Application.TimeService.Access;
export const getConcatenationOfUserNameAndYear: (args: {
userIdOne: string;
}) => ReaderTaskEither<
Dependencies,
User.Repository.UserNotFoundError,
string
> = unimplemented;

1
src/exo6/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './exo6';

1
src/readerTaskEither.ts Normal file
View File

@ -0,0 +1 @@
export { readerTaskEither as rte } from 'fp-ts';

View File

@ -1,6 +1,20 @@
import { reader } from 'fp-ts';
import { pipe } from 'fp-ts/lib/function';
import { Reader } from 'fp-ts/lib/Reader';
export const unimplemented = () => undefined as any;
export const unimplementedAsync = () => () => undefined as any;
export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
return new Promise(resolve => setTimeout(resolve, ms));
}
export const getReaderMethod =
<Access, A extends ReadonlyArray<any>, R>(
getMethod: (access: Access) => (...a: A) => R,
) =>
(...a: A): Reader<Access, R> =>
pipe(
reader.ask<Access>(),
reader.map(access => getMethod(access)(...a)),
);