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:
parent
0f7c7941fe
commit
a06d6b2573
17 changed files with 245 additions and 1 deletions
1
src/exo6/application/index.ts
Normal file
1
src/exo6/application/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * as Application from './services';
|
7
src/exo6/application/services/NodeTimeService.ts
Normal file
7
src/exo6/application/services/NodeTimeService.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { TimeService } from './TimeService';
|
||||||
|
|
||||||
|
export class NodeTimeService implements TimeService {
|
||||||
|
public thisYear() {
|
||||||
|
return new Date().getFullYear();
|
||||||
|
}
|
||||||
|
}
|
13
src/exo6/application/services/TimeService.ts
Normal file
13
src/exo6/application/services/TimeService.ts
Normal 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,
|
||||||
|
);
|
2
src/exo6/application/services/index.ts
Normal file
2
src/exo6/application/services/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * as NodeTimeService from './NodeTimeService';
|
||||||
|
export * as TimeService from './TimeService';
|
20
src/exo6/domain/User/Repository/InMemoryUserRepository.ts
Normal file
20
src/exo6/domain/User/Repository/InMemoryUserRepository.ts
Normal 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()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
2
src/exo6/domain/User/Repository/Repository.ts
Normal file
2
src/exo6/domain/User/Repository/Repository.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './InMemoryUserRepository';
|
||||||
|
export * from './readerMethods';
|
1
src/exo6/domain/User/Repository/index.ts
Normal file
1
src/exo6/domain/User/Repository/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * as Repository from './Repository';
|
23
src/exo6/domain/User/Repository/readerMethods.ts
Normal file
23
src/exo6/domain/User/Repository/readerMethods.ts
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
2
src/exo6/domain/User/User.ts
Normal file
2
src/exo6/domain/User/User.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './Repository';
|
||||||
|
export * from './type';
|
1
src/exo6/domain/User/index.ts
Normal file
1
src/exo6/domain/User/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * as User from './User';
|
5
src/exo6/domain/User/type.ts
Normal file
5
src/exo6/domain/User/type.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export type User = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
bestFriendId: string;
|
||||||
|
};
|
1
src/exo6/domain/index.ts
Normal file
1
src/exo6/domain/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './User';
|
73
src/exo6/exo6.test.ts
Normal file
73
src/exo6/exo6.test.ts
Normal 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
77
src/exo6/exo6.ts
Normal 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
1
src/exo6/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './exo6';
|
1
src/readerTaskEither.ts
Normal file
1
src/readerTaskEither.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { readerTaskEither as rte } from 'fp-ts';
|
16
src/utils.ts
16
src/utils.ts
|
@ -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 unimplemented = () => undefined as any;
|
||||||
export const unimplementedAsync = () => () => undefined as any;
|
export const unimplementedAsync = () => () => undefined as any;
|
||||||
|
|
||||||
export function sleep(ms: number) {
|
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)),
|
||||||
|
);
|
||||||
|
|
Reference in a new issue