cleanup, patterns

This commit is contained in:
Oli Sturm
2026-04-27 13:08:40 +01:00
parent d0c9466170
commit 3194231879
8 changed files with 66 additions and 36 deletions
+3 -9
View File
@@ -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.')
)
+15 -12
View File
@@ -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 });
+4 -10
View File
@@ -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))
)
)
+2 -2
View File
@@ -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)
);
};
+11
View File
@@ -9,6 +9,17 @@ export declare const outputWith: <T>(
export declare const outputError: <E>(src: string, error: E) => void;
export declare const logSuccess: <T, E>(src: string, message: string) => (
result: import('./result.js').Result<T, E>
) => import('./result.js').Result<T, E>;
export declare const logSuccessWith: <T, E>(
src: string,
renderText: (x: T) => string
) => (result: import('./result.js').Result<T, E>) => import('./result.js').Result<T, E>;
export declare const formatMoney: (money: { amount: number }) => string;
export declare const log: <T, E>(src: string, message: string) => (
result: import('./result.js').Result<T, E>
) => import('./result.js').Result<T, E>;
+12 -3
View File
@@ -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)));
+9
View File
@@ -0,0 +1,9 @@
export type Predicate<T> = (value: T) => boolean;
export type Handler<T, R> = (value: T) => R;
export type Branch<T, R> = [Predicate<T>, Handler<T, R>];
export declare const match: <T, R>(...branches: Branch<T, R>[]) => (value: T) => R;
export declare const when: <T, R>(predicate: Predicate<T>, handler: Handler<T, R>) => Branch<T, R>;
export declare const any: Predicate<any>;
+10
View File
@@ -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;