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 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)),
|
||||
);
|
||||
|
|
Reference in a new issue