fluent-results
Version:
Tiny, dependency-free TypeScript implementation of the Fluent Results pattern for railway-oriented programming.
287 lines (286 loc) • 10.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Result = exports.AResult = void 0;
const AError_1 = require("./AError");
const ExceptionalError_1 = require("./ExceptionalError");
const PromiseRejection_1 = require("./PromiseRejection");
class AResult {
/** `true` when *no* {@link AError} has been recorded. */
get isSuccess() {
return !this.isFailed;
}
/** `true` when **at least one** {@link AError} exists. */
get isFailed() {
return this._reasons.some((r) => r instanceof AError_1.AError);
}
/**
* A descriptive name for the routine, useful for logging or debugging.
*/
get routineName() {
return this._routineName;
}
/**
* The parent result that created this result (usually as to execute a contingent routine)
*/
get parent() {
return this._parent;
}
/**
* A child that was created as a result of execution of a contingent path
*/
get child() {
return this._child;
}
set child(child) {
this._child = child;
}
/**
* @param routineName A descriptive name for the routine, useful for logging or debugging.
* @param [parent] The parent result that created this result (usually as to execute a contingent routine)
*/
constructor(routineName, parent) {
/** Informational messages *and* errors gathered so far. */
this._reasons = [];
/** Single‑slot cache for the latest successful value. */
this.stateCache = [];
this._routineName = "";
this._routineName = routineName;
this._parent = parent;
}
}
exports.AResult = AResult;
/**
* `Result` represents the outcome **and** the flowing state of a pipeline that can
* short‑circuit on the first error ("railway‑oriented programming").
*
* A `Result` starts out *successful* and accumulates {@link AReason | reasons};
* any {@link AError | error} automatically flips the result into the *failed* state.
*
*/
class Result extends AResult {
/**
* The most recent value produced by the pipeline.
* @throws {Error} If no value has been cached yet (typically because the pipeline only ran parameter‑less steps).
*/
get currentState() {
if (this.stateCache.length === 1) {
return this.stateCache[0];
}
throw new Error('No state present. Ensure a previous delegate returns a value before attempting to read currentState.');
}
/** Immutable copy of informational reasons **and** errors. */
get reasons() {
return this._reasons.slice();
}
/** Convenience subset of {@link reasons} limited to errors. */
get errors() {
return this._reasons.filter((r) => r instanceof AError_1.AError);
}
/**
* Executes `action` and wraps its outcome into a new **root** `Result`.
*
* • If `action` throws, the exception is captured as an {@link ExceptionalError}.
* • If it completes successfully, the return value is stored as {@link currentState}.
*
* @param action A synchronous delegate that may return a value and/or throw.
* @param routineName A descriptive name for the routine, useful for logging or debugging.
*/
static try(action, routineName) {
const result = new Result(routineName);
try {
result.cacheState(action());
}
catch (e) {
result._reasons.push(new ExceptionalError_1.ExceptionalError(e));
}
finally {
return result;
}
}
/**
* Executes `action` and wraps its awaited outcome into a new **root** `Result`.
*
* • If `action` throws, the exception is captured as an {@link ExceptionalError}.
* • If it completes successfully, the awaited outcome is stored as {@link currentState}.
*
* @param action A synchronous delegate that may return a value and/or throw.
* @param routineName A descriptive name for the routine, useful for logging or debugging.
*/
static async tryAsync(action, routineName) {
const result = new Result(routineName);
try {
await action().then(value => result.cacheState(value)).catch(reason => result._reasons.push(new PromiseRejection_1.PromiseRejection(reason)));
}
catch (e) {
result._reasons.push(new ExceptionalError_1.ExceptionalError(e));
}
finally {
return result;
}
}
/**
* Chains another synchronous function into the pipeline.
*
* @param func Delegate to execute.
* • If the previous step succeeded, its return value becomes the input when `func` has an arity of **1**.
* • If the previous step failed, `func` is **skipped**.
*
* @returns **this** so that calls can be fluently chained.
*/
bind(func) {
if (this.isSuccess) {
try {
const out = func.length === 0
? func()
: func(this.currentState);
this.cacheState(out);
}
catch (e) {
this._reasons.push(new ExceptionalError_1.ExceptionalError(e));
}
}
return this;
}
/**
* Chains another synchronous function into the pipeline and captures its awaited outcome as {@link currentState}.
*
* @param func A delegate returning a promise.
* • If the previous step succeeded, its return value becomes the input when `func` has an arity of **1**.
* • If the previous step failed, `func` is **skipped**.
*
* @returns **this** so that calls can be fluently chained.
*/
async bindAsync(func) {
if (this.isSuccess) {
try {
const promise = func.length === 0
? func()
: func(this.currentState);
await promise.then(value => this.cacheState(value)).catch(reason => this._reasons.push(new PromiseRejection_1.PromiseRejection(reason)));
}
catch (e) {
this._reasons.push(new ExceptionalError_1.ExceptionalError(e));
}
}
return this;
}
/**
* Keeps the pipeline successful **only if** the `predicate` evaluates to `true`.
*
* @param predicate Condition to evaluate (optionally with `currentState` input).
* @param error Error instance to push when the predicate fails.
*
* @returns **this** for chaining.
*/
okIf(predicate, error) {
if (this.isSuccess) {
try {
const pass = predicate.length === 0
? predicate()
: predicate(this.currentState);
if (!pass)
this._reasons.push(error);
}
catch (e) {
this._reasons.push(new ExceptionalError_1.ExceptionalError(e));
}
}
return this;
}
/**
* Keeps the pipeline successful **only if** the `predicate` evaluates to `true`.
*
* @param predicate A function returning promise that returns boolean when awaited (optionally with `currentState` input).
* @param error Error instance to push when the predicate fails.
*
* @returns **this** for chaining.
*/
async okIfAsync(predicate, error) {
if (this.isSuccess) {
try {
const pass = predicate.length === 0
? await predicate()
: await predicate(this.currentState);
if (!pass)
this._reasons.push(error);
}
catch (e) {
this._reasons.push(new ExceptionalError_1.ExceptionalError(e));
}
}
return this;
}
/**
* Fails the pipeline **only if** the `predicate` evaluates to `true`.
*
* @param predicate Condition to evaluate (optionally with `currentState` input).
* @param error Error instance to push when the predicate **passes**.
* @param [contingency] - Optional object defining a contingent route.
* @param next.func - A function to be executed if {@link predicate} evaluates to false
* @param next.routineName - A descriptive name for the routine, useful for logging or debugging.
*
* @returns **this** for chaining.
*/
failIf(predicate, error, contingency) {
if (this.isSuccess) {
try {
const fail = predicate.length === 0
? predicate()
: predicate(this.currentState);
if (fail) {
this._reasons.push(error);
if (contingency) {
let child = new Result(contingency.routineName, this);
this.child = child;
contingency.func(child);
}
}
}
catch (e) {
this._reasons.push(new ExceptionalError_1.ExceptionalError(e));
}
}
return this;
}
/**
* Fails the pipeline **only if** the `predicate` evaluates to `true`.
*
* @param predicate A function returning promise that returns boolean when awaited (optionally with `currentState` input).
* @param error Error instance to push when the predicate **passes**.
* @param [contingency] - Optional object defining a contingent route.
* @param next.func - A function to be executed if {@link predicate} evaluates to false
* @param next.routineName - A descriptive name for the routine, useful for logging or debugging.
*
* @returns **this** for chaining.
*/
async failIfAsync(predicate, error, contingency) {
if (this.isSuccess) {
try {
const fail = predicate.length === 0
? await predicate()
: await predicate(this.currentState);
if (fail) {
this._reasons.push(error);
if (contingency) {
let child = new Result(contingency.routineName, this);
this.child = child;
contingency.func(child);
}
}
}
catch (e) {
this._reasons.push(new ExceptionalError_1.ExceptionalError(e));
}
}
return this;
}
/**
* Internal helper—overwrites the single‑slot {@link stateCache}.
* Not exposed publicly on purpose.
*/
cacheState(value) {
this.stateCache.length = 0;
this.stateCache.push(value);
}
}
exports.Result = Result;