diff --git a/js-fp3/application/accountApp.js b/js-fp3/application/accountApp.js index ffe5090..c07a345 100644 --- a/js-fp3/application/accountApp.js +++ b/js-fp3/application/accountApp.js @@ -1,14 +1,8 @@ -import { bind, ok, tap, tapError, pipe } from '../library/result.js'; -import { output, outputError } from '../library/logging.js'; +import { bind, ok, pipe } from '../library/result.js'; +import { log, logWith, formatMoney } from '../library/logging.js'; import { withdraw } from '../domain/account.js'; import { createMoney } from '../domain/money.js'; -const log = (src, msg) => (result) => - tap((x) => output(src, msg)(x))(tapError((e) => outputError(src, e))(result)); - -const logWith = (src, renderText) => (result) => - tap((x) => output(src, renderText(x))(x))(tapError((e) => outputError(src, e))(result)); - export const createWithdrawMoney = (repo) => ok((accountId, amount) => pipe( @@ -16,7 +10,7 @@ export const createWithdrawMoney = (repo) => log('App load', 'Account loaded'), log('App exec wdrwl', `[App] Executing withdrawal of ${amount.toFixed(2)}...`), bind((account) => withdraw(account, createMoney(amount))), - logWith('App wdrwl done', (a) => `Withdrawal applied. New balance: ${a.balance.amount.toFixed(2)}`), + logWith('App wdrwl done', (a) => `Withdrawal applied. New balance: ${formatMoney(a.balance)}`), bind(repo.saveAccount), log('App acc svd', '[App] Account persisted.') ) diff --git a/js-fp3/domain/account.js b/js-fp3/domain/account.js index f55073e..688f0a8 100644 --- a/js-fp3/domain/account.js +++ b/js-fp3/domain/account.js @@ -1,4 +1,5 @@ import { ok, fail } from '../library/result.js'; +import { match, when, any } from '../library/patterns.js'; export const openingBalanceMustBeNonNegative = { type: 'OpeningBalanceMustBeNonNegative' }; @@ -20,15 +21,17 @@ export const openAccount = (id, openingBalance) => ? fail(openingBalanceMustBeNonNegative) : ok({ id, balance: openingBalance }); -export const withdraw = (account, amount) => { - if (amount.amount <= 0) { - return fail(amountMustBePositive); - } - if (account.balance.amount < amount.amount) { - return fail(insufficientBalance(account.balance, amount)); - } - return ok({ - ...account, - balance: { amount: account.balance.amount - amount.amount }, - }); -}; +export const withdraw = (account, amount) => + match( + when(({ amount: a }) => a.amount <= 0, () => fail(amountMustBePositive)), + when( + ({ account: acc, amount: amt }) => acc.balance.amount < amt.amount, + ({ account: acc, amount: amt }) => fail(insufficientBalance(acc.balance, amt)) + ), + when(any, ({ account: acc, amount: amt }) => + ok({ + ...acc, + balance: { amount: acc.balance.amount - amt.amount }, + }) + ) + )({ account, amount }); diff --git a/js-fp3/index.js b/js-fp3/index.js index 96cd4db..2270c35 100644 --- a/js-fp3/index.js +++ b/js-fp3/index.js @@ -1,17 +1,11 @@ import { randomUUID } from 'crypto'; -import { catchResult, bind, tap, tapError, mapError, pipe } from './library/result.js'; -import { output, outputError } from './library/logging.js'; +import { catchResult, bind, mapError, pipe } from './library/result.js'; +import { log, logWith, formatMoney } from './library/logging.js'; import { createInMemoryRepository, innerAccountError } from './infrastructure/repository.js'; import { openAccount } from './domain/account.js'; import { createMoney } from './domain/money.js'; import { createWithdrawMoney } from './application/accountApp.js'; -const log = (src, msg) => (result) => - tap((x) => output(src, msg)(x))(tapError((e) => outputError(src, e))(result)); - -const logWith = (src, renderText) => (result) => - tap((x) => output(src, renderText(x))(x))(tapError((e) => outputError(src, e))(result)); - console.log('[js-fp3] Starting withdraw money demo...'); const accountId = randomUUID(); @@ -30,11 +24,11 @@ pipe( bind((withdrawMoney) => pipe( openAccount(accountId, createMoney(200)), - logWith('js-fp3 seed', (account) => `Seeding account ${account.id} with opening balance ${account.balance.amount.toFixed(2)}`), + logWith('js-fp3 seed', (account) => `Seeding account ${account.id} with opening balance ${formatMoney(account.balance)}`), bind(repo.saveAccount), log('js-fp3 exec', `Executing withdrawal ${withdrawalAmount.toFixed(2)} from account ${accountId}`), bind(() => withdrawMoney(accountId, withdrawalAmount)), - logWith('js-fp3 new balance', (account) => `New balance is ${account.balance.amount.toFixed(2)}`), + logWith('js-fp3 new balance', (account) => `New balance is ${formatMoney(account.balance)}`), mapError((ae) => innerAccountError(ae)) ) ) diff --git a/js-fp3/infrastructure/repository.js b/js-fp3/infrastructure/repository.js index 2332539..d734502 100644 --- a/js-fp3/infrastructure/repository.js +++ b/js-fp3/infrastructure/repository.js @@ -1,5 +1,5 @@ import { ok, fail } from '../library/result.js'; -import { logReturn } from '../library/logging.js'; +import { logReturn, formatMoney } from '../library/logging.js'; import { accountNotFound } from '../domain/account.js'; export const repositoryCreationFailed = (message) => ({ @@ -29,7 +29,7 @@ export const createInMemoryRepository = () => { const save = (account) => { store.set(account.id, account); return logReturn( - `[Repo] Saved account ${account.id} with balance ${account.balance.amount.toFixed(2)}`, + `[Repo] Saved account ${account.id} with balance ${formatMoney(account.balance)}`, ok(account) ); }; diff --git a/js-fp3/library/logging.d.ts b/js-fp3/library/logging.d.ts index e3e9630..0c7f3bd 100644 --- a/js-fp3/library/logging.d.ts +++ b/js-fp3/library/logging.d.ts @@ -9,6 +9,17 @@ export declare const outputWith: ( export declare const outputError: (src: string, error: E) => void; +export declare const logSuccess: (src: string, message: string) => ( + result: import('./result.js').Result +) => import('./result.js').Result; + +export declare const logSuccessWith: ( + src: string, + renderText: (x: T) => string +) => (result: import('./result.js').Result) => import('./result.js').Result; + +export declare const formatMoney: (money: { amount: number }) => string; + export declare const log: (src: string, message: string) => ( result: import('./result.js').Result ) => import('./result.js').Result; diff --git a/js-fp3/library/logging.js b/js-fp3/library/logging.js index 2e5ab7c..53131c3 100644 --- a/js-fp3/library/logging.js +++ b/js-fp3/library/logging.js @@ -1,4 +1,7 @@ -import { tap, tapError } from './result.js'; +import { tap, tapError, pipe } from './result.js'; +import { formatMoney as formatMoneyFromDomain } from '../domain/money.js'; + +export { formatMoneyFromDomain as formatMoney }; export const logReturn = (message, r) => { console.log(message); @@ -19,6 +22,12 @@ export const outputError = (src, error) => { console.error(`\x1b[1;31m[${src} ERROR]\x1b[0m ${value}`); }; -export const log = (src, message) => tap(output(src, message)); +export const logSuccess = (src, message) => tap(output(src, message)); -export const logWith = (src, renderText) => tap(outputWith(src, renderText)); +export const logSuccessWith = (src, renderText) => tap(outputWith(src, renderText)); + +export const log = (src, message) => (result) => + pipe(result, tap(output(src, message)), tapError((e) => outputError(src, e))); + +export const logWith = (src, renderText) => (result) => + pipe(result, tap((x) => output(src, renderText(x))(x)), tapError((e) => outputError(src, e))); diff --git a/js-fp3/library/patterns.d.ts b/js-fp3/library/patterns.d.ts new file mode 100644 index 0000000..2d3f1ca --- /dev/null +++ b/js-fp3/library/patterns.d.ts @@ -0,0 +1,9 @@ +export type Predicate = (value: T) => boolean; +export type Handler = (value: T) => R; +export type Branch = [Predicate, Handler]; + +export declare const match: (...branches: Branch[]) => (value: T) => R; + +export declare const when: (predicate: Predicate, handler: Handler) => Branch; + +export declare const any: Predicate; diff --git a/js-fp3/library/patterns.js b/js-fp3/library/patterns.js new file mode 100644 index 0000000..1467e11 --- /dev/null +++ b/js-fp3/library/patterns.js @@ -0,0 +1,10 @@ +export const match = (...branches) => (value) => { + for (const [predicate, handler] of branches) { + if (predicate(value)) return handler(value); + } + throw new Error('No matching pattern'); +}; + +export const when = (predicate, handler) => [predicate, handler]; + +export const any = () => true;