diff --git a/src/exo6/application/index.ts b/src/exo6/application/index.ts new file mode 100644 index 0000000..1f87985 --- /dev/null +++ b/src/exo6/application/index.ts @@ -0,0 +1 @@ +export * as Application from './services'; diff --git a/src/exo6/application/services/NodeTimeService.ts b/src/exo6/application/services/NodeTimeService.ts new file mode 100644 index 0000000..3a75b43 --- /dev/null +++ b/src/exo6/application/services/NodeTimeService.ts @@ -0,0 +1,7 @@ +import { TimeService } from './TimeService'; + +export class NodeTimeService implements TimeService { + public thisYear() { + return new Date().getFullYear(); + } +} diff --git a/src/exo6/application/services/TimeService.ts b/src/exo6/application/services/TimeService.ts new file mode 100644 index 0000000..fe04d37 --- /dev/null +++ b/src/exo6/application/services/TimeService.ts @@ -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, +); diff --git a/src/exo6/application/services/index.ts b/src/exo6/application/services/index.ts new file mode 100644 index 0000000..04ed54e --- /dev/null +++ b/src/exo6/application/services/index.ts @@ -0,0 +1,2 @@ +export * as NodeTimeService from './NodeTimeService'; +export * as TimeService from './TimeService'; diff --git a/src/exo6/domain/User/Repository/InMemoryUserRepository.ts b/src/exo6/domain/User/Repository/InMemoryUserRepository.ts new file mode 100644 index 0000000..b23181c --- /dev/null +++ b/src/exo6/domain/User/Repository/InMemoryUserRepository.ts @@ -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; + + constructor(aggregates: User[]) { + this.aggregates = new Map(aggregates.map(user => [user.id, user])); + } + + getById(userId: string): TaskEither { + return pipe( + this.aggregates.get(userId), + taskEither.fromNullable(new UserNotFoundError()), + ); + } +} diff --git a/src/exo6/domain/User/Repository/Repository.ts b/src/exo6/domain/User/Repository/Repository.ts new file mode 100644 index 0000000..a33e599 --- /dev/null +++ b/src/exo6/domain/User/Repository/Repository.ts @@ -0,0 +1,2 @@ +export * from './InMemoryUserRepository'; +export * from './readerMethods'; diff --git a/src/exo6/domain/User/Repository/index.ts b/src/exo6/domain/User/Repository/index.ts new file mode 100644 index 0000000..e188eff --- /dev/null +++ b/src/exo6/domain/User/Repository/index.ts @@ -0,0 +1 @@ +export * as Repository from './Repository'; diff --git a/src/exo6/domain/User/Repository/readerMethods.ts b/src/exo6/domain/User/Repository/readerMethods.ts new file mode 100644 index 0000000..974fd12 --- /dev/null +++ b/src/exo6/domain/User/Repository/readerMethods.ts @@ -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 => + pipe( + reader.ask(), + reader.map(({ userRepository }) => userRepository.getById(userId)), + ); + +export class UserNotFoundError extends Error { + constructor() { + super('User not found'); + } +} diff --git a/src/exo6/domain/User/User.ts b/src/exo6/domain/User/User.ts new file mode 100644 index 0000000..f38ad3b --- /dev/null +++ b/src/exo6/domain/User/User.ts @@ -0,0 +1,2 @@ +export * from './Repository'; +export * from './type'; diff --git a/src/exo6/domain/User/index.ts b/src/exo6/domain/User/index.ts new file mode 100644 index 0000000..1f94c77 --- /dev/null +++ b/src/exo6/domain/User/index.ts @@ -0,0 +1 @@ +export * as User from './User'; diff --git a/src/exo6/domain/User/type.ts b/src/exo6/domain/User/type.ts new file mode 100644 index 0000000..4779d2c --- /dev/null +++ b/src/exo6/domain/User/type.ts @@ -0,0 +1,5 @@ +export type User = { + id: string; + name: string; + bestFriendId: string; +}; diff --git a/src/exo6/domain/index.ts b/src/exo6/domain/index.ts new file mode 100644 index 0000000..f6b9f36 --- /dev/null +++ b/src/exo6/domain/index.ts @@ -0,0 +1 @@ +export * from './User'; diff --git a/src/exo6/exo6.test.ts b/src/exo6/exo6.test.ts new file mode 100644 index 0000000..10c3879 --- /dev/null +++ b/src/exo6/exo6.test.ts @@ -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()}`)); + }); +}); diff --git a/src/exo6/exo6.ts b/src/exo6/exo6.ts new file mode 100644 index 0000000..690c657 --- /dev/null +++ b/src/exo6/exo6.ts @@ -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 = Reader>> +// +// 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; diff --git a/src/exo6/index.ts b/src/exo6/index.ts new file mode 100644 index 0000000..28ccfc2 --- /dev/null +++ b/src/exo6/index.ts @@ -0,0 +1 @@ +export * from './exo6'; diff --git a/src/readerTaskEither.ts b/src/readerTaskEither.ts new file mode 100644 index 0000000..f18a428 --- /dev/null +++ b/src/readerTaskEither.ts @@ -0,0 +1 @@ +export { readerTaskEither as rte } from 'fp-ts'; diff --git a/src/utils.ts b/src/utils.ts index 6662260..5f42e99 100644 --- a/src/utils.ts +++ b/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 = + , R>( + getMethod: (access: Access) => (...a: A) => R, + ) => + (...a: A): Reader => + pipe( + reader.ask(), + reader.map(access => getMethod(access)(...a)), + );