architex-js 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "architex-js",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "main": "src/index.js",
5
5
  "exports": {
6
6
  ".": "./src/index.js",
@@ -8,7 +8,9 @@
8
8
  "./mixins": "./src/mixins/index.js",
9
9
  "./microkernel": "./src/microkernel/index.js",
10
10
  "./exceptions": "./src/exceptions/index.js",
11
- "./pipeline": "./src/pipeline/index.js"
11
+ "./pipeline": "./src/pipeline/index.js",
12
+ "./result": "./src/result/index.js",
13
+ "./guards": "./src/guards/index.js"
12
14
  },
13
15
  "description": "Architectural Toolbox for JavaScript - Providing high-level building blocks for robust systems.",
14
16
  "author": {
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Defensive programming utilities.
3
+ * Guard methods throw descriptive errors when assertions fail,
4
+ * keeping domain invariants clean and readable.
5
+ *
6
+ * @example
7
+ * Guard.notNull(userId, 'userId');
8
+ * Guard.minLength(username, 3, 'username');
9
+ * Guard.positiveNumber(price, 'price');
10
+ */
11
+ class Guard {
12
+ /**
13
+ * @param {string} fieldName
14
+ * @param {string} message
15
+ */
16
+ static #fail(fieldName, message) {
17
+ throw new Error(`[Guard] ${fieldName}: ${message}`);
18
+ }
19
+
20
+ /**
21
+ * Asserts the value is not null or undefined.
22
+ * @param {*} value
23
+ * @param {string} fieldName
24
+ */
25
+ static notNull(value, fieldName = 'value') {
26
+ if (value === null || value === undefined) {
27
+ Guard.#fail(fieldName, 'must not be null or undefined');
28
+ }
29
+ return value;
30
+ }
31
+
32
+ /**
33
+ * Asserts the string is non-empty after trimming.
34
+ * @param {string} value
35
+ * @param {string} fieldName
36
+ */
37
+ static notEmpty(value, fieldName = 'value') {
38
+ Guard.notNull(value, fieldName);
39
+ if (typeof value !== 'string' || value.trim().length === 0) {
40
+ Guard.#fail(fieldName, 'must not be an empty string');
41
+ }
42
+ return value;
43
+ }
44
+
45
+ /**
46
+ * Asserts the string has at least `min` characters.
47
+ * @param {string} value
48
+ * @param {number} min
49
+ * @param {string} fieldName
50
+ */
51
+ static minLength(value, min, fieldName = 'value') {
52
+ Guard.notNull(value, fieldName);
53
+ if (typeof value !== 'string' || value.length < min) {
54
+ Guard.#fail(fieldName, `must have at least ${min} character(s), got ${value?.length ?? 0}`);
55
+ }
56
+ return value;
57
+ }
58
+
59
+ /**
60
+ * Asserts the string has at most `max` characters.
61
+ * @param {string} value
62
+ * @param {number} max
63
+ * @param {string} fieldName
64
+ */
65
+ static maxLength(value, max, fieldName = 'value') {
66
+ Guard.notNull(value, fieldName);
67
+ if (typeof value !== 'string' || value.length > max) {
68
+ Guard.#fail(fieldName, `must have at most ${max} character(s), got ${value?.length ?? 0}`);
69
+ }
70
+ return value;
71
+ }
72
+
73
+ /**
74
+ * Asserts the number is greater than zero (no zero allowed).
75
+ * @param {number} value
76
+ * @param {string} fieldName
77
+ */
78
+ static positiveNumber(value, fieldName = 'value') {
79
+ Guard.notNull(value, fieldName);
80
+ if (typeof value !== 'number' || value <= 0) {
81
+ Guard.#fail(fieldName, `must be a positive number, got ${value}`);
82
+ }
83
+ return value;
84
+ }
85
+
86
+ /**
87
+ * Asserts the number is zero or greater.
88
+ * @param {number} value
89
+ * @param {string} fieldName
90
+ */
91
+ static nonNegativeNumber(value, fieldName = 'value') {
92
+ Guard.notNull(value, fieldName);
93
+ if (typeof value !== 'number' || value < 0) {
94
+ Guard.#fail(fieldName, `must be >= 0, got ${value}`);
95
+ }
96
+ return value;
97
+ }
98
+
99
+ /**
100
+ * Asserts the value falls within a numeric range [min, max].
101
+ * @param {number} value
102
+ * @param {number} min
103
+ * @param {number} max
104
+ * @param {string} fieldName
105
+ */
106
+ static inRange(value, min, max, fieldName = 'value') {
107
+ Guard.notNull(value, fieldName);
108
+ if (typeof value !== 'number' || value < min || value > max) {
109
+ Guard.#fail(fieldName, `must be between ${min} and ${max}, got ${value}`);
110
+ }
111
+ return value;
112
+ }
113
+
114
+ /**
115
+ * Asserts the value matches a regular expression.
116
+ * @param {string} value
117
+ * @param {RegExp} pattern
118
+ * @param {string} fieldName
119
+ */
120
+ static matches(value, pattern, fieldName = 'value') {
121
+ Guard.notNull(value, fieldName);
122
+ if (typeof value !== 'string' || !pattern.test(value)) {
123
+ Guard.#fail(fieldName, `must match pattern ${pattern}`);
124
+ }
125
+ return value;
126
+ }
127
+
128
+ /**
129
+ * Asserts the value is a valid email address.
130
+ * @param {string} value
131
+ * @param {string} fieldName
132
+ */
133
+ static isEmail(value, fieldName = 'value') {
134
+ return Guard.matches(value, /^[^\s@]+@[^\s@]+\.[^\s@]+$/, fieldName);
135
+ }
136
+
137
+ /**
138
+ * Asserts the value is one of the allowed options.
139
+ * @param {*} value
140
+ * @param {Array} options
141
+ * @param {string} fieldName
142
+ */
143
+ static oneOf(value, options, fieldName = 'value') {
144
+ Guard.notNull(value, fieldName);
145
+ if (!options.includes(value)) {
146
+ Guard.#fail(fieldName, `must be one of [${options.join(', ')}], got "${value}"`);
147
+ }
148
+ return value;
149
+ }
150
+
151
+ /**
152
+ * Asserts the value is an Array with at least one element.
153
+ * @param {Array} value
154
+ * @param {string} fieldName
155
+ */
156
+ static notEmptyArray(value, fieldName = 'value') {
157
+ Guard.notNull(value, fieldName);
158
+ if (!Array.isArray(value) || value.length === 0) {
159
+ Guard.#fail(fieldName, 'must be a non-empty array');
160
+ }
161
+ return value;
162
+ }
163
+ }
164
+
165
+ export { Guard };
@@ -0,0 +1 @@
1
+ export * from "./Guard.js";
package/src/index.js CHANGED
@@ -2,4 +2,6 @@ export * from "./abc/index.js";
2
2
  export * from "./mixins/index.js";
3
3
  export * from "./microkernel/index.js";
4
4
  export * from "./exceptions/index.js";
5
- export * from "./pipeline/index.js";
5
+ export * from "./pipeline/index.js";
6
+ export * from "./result/index.js";
7
+ export * from "./guards/index.js";
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Represents the successful outcome of an operation.
3
+ * @template T
4
+ */
5
+ class Ok {
6
+ /** @param {T} value */
7
+ constructor(value) {
8
+ this._value = value;
9
+ }
10
+
11
+ /** @returns {true} */
12
+ isOk() { return true; }
13
+
14
+ /** @returns {false} */
15
+ isFail() { return false; }
16
+
17
+ /** @returns {T} */
18
+ get value() { return this._value; }
19
+
20
+ /**
21
+ * Transforms the inner value with a function.
22
+ * @template U
23
+ * @param {(value: T) => U} fn
24
+ * @returns {Ok<U>}
25
+ */
26
+ map(fn) { return new Ok(fn(this._value)); }
27
+
28
+ /**
29
+ * Chains another Result-returning function.
30
+ * @template U
31
+ * @param {(value: T) => Result<U>} fn
32
+ * @returns {Result<U>}
33
+ */
34
+ flatMap(fn) { return fn(this._value); }
35
+
36
+ /**
37
+ * Returns the value or the fallback if this is a Fail.
38
+ * @param {T} _fallback
39
+ * @returns {T}
40
+ */
41
+ getOrElse(_fallback) { return this._value; }
42
+
43
+ toString() { return `Ok(${this._value})`; }
44
+ }
45
+
46
+ /**
47
+ * Represents the failed outcome of an operation.
48
+ * @template E
49
+ */
50
+ class Fail {
51
+ /** @param {E} error */
52
+ constructor(error) {
53
+ this._error = error;
54
+ }
55
+
56
+ /** @returns {false} */
57
+ isOk() { return false; }
58
+
59
+ /** @returns {true} */
60
+ isFail() { return true; }
61
+
62
+ /** @returns {E} */
63
+ get error() { return this._error; }
64
+
65
+ /**
66
+ * Returns Fail unchanged (map is a no-op on failure).
67
+ * @returns {Fail<E>}
68
+ */
69
+ map(_fn) { return this; }
70
+
71
+ /**
72
+ * Returns Fail unchanged (flatMap is a no-op on failure).
73
+ * @returns {Fail<E>}
74
+ */
75
+ flatMap(_fn) { return this; }
76
+
77
+ /**
78
+ * Returns the fallback value since this is a failure.
79
+ * @template T
80
+ * @param {T} fallback
81
+ * @returns {T}
82
+ */
83
+ getOrElse(fallback) { return fallback; }
84
+
85
+ toString() { return `Fail(${this._error})`; }
86
+ }
87
+
88
+ /**
89
+ * Railway-Oriented Programming: wraps the result of an operation
90
+ * as either a success (Ok) or a failure (Fail), eliminating try/catch noise.
91
+ *
92
+ * @example
93
+ * const r = Result.ok(42);
94
+ * if (r.isOk()) console.log(r.value); // 42
95
+ *
96
+ * const e = Result.fail(new Error("boom"));
97
+ * if (e.isFail()) console.error(e.error);
98
+ */
99
+ class Result {
100
+ /**
101
+ * Creates a successful result.
102
+ * @template T
103
+ * @param {T} value
104
+ * @returns {Ok<T>}
105
+ */
106
+ static ok(value) {
107
+ return new Ok(value);
108
+ }
109
+
110
+ /**
111
+ * Creates a failed result.
112
+ * @template E
113
+ * @param {E} error
114
+ * @returns {Fail<E>}
115
+ */
116
+ static fail(error) {
117
+ return new Fail(error);
118
+ }
119
+
120
+ /**
121
+ * Wraps a function that may throw and returns a Result.
122
+ * @template T
123
+ * @param {() => T} fn
124
+ * @returns {Ok<T> | Fail<unknown>}
125
+ */
126
+ static try(fn) {
127
+ try {
128
+ return new Ok(fn());
129
+ } catch (e) {
130
+ return new Fail(e);
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Wraps an async function that may throw and returns a Promise<Result>.
136
+ * @template T
137
+ * @param {() => Promise<T>} fn
138
+ * @returns {Promise<Ok<T> | Fail<unknown>>}
139
+ */
140
+ static async tryAsync(fn) {
141
+ try {
142
+ return new Ok(await fn());
143
+ } catch (e) {
144
+ return new Fail(e);
145
+ }
146
+ }
147
+ }
148
+
149
+ export { Result, Ok, Fail };
@@ -0,0 +1 @@
1
+ export * from "./Result.js";
@@ -0,0 +1,94 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Guard } from '../src/guards/index.js';
3
+
4
+ describe('Guard', () => {
5
+ describe('notNull', () => {
6
+ it('should pass for a valid value', () => {
7
+ expect(() => Guard.notNull('hello', 'field')).not.toThrow();
8
+ });
9
+ it('should throw for null', () => {
10
+ expect(() => Guard.notNull(null, 'field')).toThrow('[Guard] field: must not be null or undefined');
11
+ });
12
+ it('should throw for undefined', () => {
13
+ expect(() => Guard.notNull(undefined, 'field')).toThrow();
14
+ });
15
+ });
16
+
17
+ describe('notEmpty', () => {
18
+ it('should pass for a non-empty string', () => {
19
+ expect(() => Guard.notEmpty('hello', 'name')).not.toThrow();
20
+ });
21
+ it('should throw for an empty string', () => {
22
+ expect(() => Guard.notEmpty('', 'name')).toThrow();
23
+ });
24
+ it('should throw for a whitespace-only string', () => {
25
+ expect(() => Guard.notEmpty(' ', 'name')).toThrow();
26
+ });
27
+ });
28
+
29
+ describe('minLength', () => {
30
+ it('should pass when length >= min', () => {
31
+ expect(() => Guard.minLength('abc', 3, 'field')).not.toThrow();
32
+ });
33
+ it('should throw when length < min', () => {
34
+ expect(() => Guard.minLength('ab', 3, 'field')).toThrow();
35
+ });
36
+ });
37
+
38
+ describe('maxLength', () => {
39
+ it('should pass when length <= max', () => {
40
+ expect(() => Guard.maxLength('abc', 5, 'field')).not.toThrow();
41
+ });
42
+ it('should throw when length > max', () => {
43
+ expect(() => Guard.maxLength('abcdef', 5, 'field')).toThrow();
44
+ });
45
+ });
46
+
47
+ describe('positiveNumber', () => {
48
+ it('should pass for a positive number', () => {
49
+ expect(() => Guard.positiveNumber(1, 'price')).not.toThrow();
50
+ });
51
+ it('should throw for zero', () => {
52
+ expect(() => Guard.positiveNumber(0, 'price')).toThrow();
53
+ });
54
+ it('should throw for negative', () => {
55
+ expect(() => Guard.positiveNumber(-5, 'price')).toThrow();
56
+ });
57
+ });
58
+
59
+ describe('inRange', () => {
60
+ it('should pass for value within range', () => {
61
+ expect(() => Guard.inRange(5, 1, 10, 'score')).not.toThrow();
62
+ });
63
+ it('should throw for value outside range', () => {
64
+ expect(() => Guard.inRange(11, 1, 10, 'score')).toThrow();
65
+ });
66
+ });
67
+
68
+ describe('isEmail', () => {
69
+ it('should pass for a valid email', () => {
70
+ expect(() => Guard.isEmail('user@example.com', 'email')).not.toThrow();
71
+ });
72
+ it('should throw for an invalid email', () => {
73
+ expect(() => Guard.isEmail('not-an-email', 'email')).toThrow();
74
+ });
75
+ });
76
+
77
+ describe('oneOf', () => {
78
+ it('should pass when value is in options', () => {
79
+ expect(() => Guard.oneOf('active', ['active', 'inactive'], 'status')).not.toThrow();
80
+ });
81
+ it('should throw when value is not in options', () => {
82
+ expect(() => Guard.oneOf('pending', ['active', 'inactive'], 'status')).toThrow();
83
+ });
84
+ });
85
+
86
+ describe('notEmptyArray', () => {
87
+ it('should pass for a non-empty array', () => {
88
+ expect(() => Guard.notEmptyArray([1, 2], 'items')).not.toThrow();
89
+ });
90
+ it('should throw for an empty array', () => {
91
+ expect(() => Guard.notEmptyArray([], 'items')).toThrow();
92
+ });
93
+ });
94
+ });
@@ -0,0 +1,76 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Result, Ok, Fail } from '../src/result/index.js';
3
+
4
+ describe('Result', () => {
5
+ it('Result.ok() should create an Ok instance', () => {
6
+ const r = Result.ok(42);
7
+ expect(r.isOk()).toBe(true);
8
+ expect(r.isFail()).toBe(false);
9
+ expect(r.value).toBe(42);
10
+ });
11
+
12
+ it('Result.fail() should create a Fail instance', () => {
13
+ const err = new Error('boom');
14
+ const r = Result.fail(err);
15
+ expect(r.isOk()).toBe(false);
16
+ expect(r.isFail()).toBe(true);
17
+ expect(r.error).toBe(err);
18
+ });
19
+
20
+ it('Ok.map() should transform the value', () => {
21
+ const r = Result.ok(5).map(x => x * 2);
22
+ expect(r.isOk()).toBe(true);
23
+ expect(r.value).toBe(10);
24
+ });
25
+
26
+ it('Fail.map() should be a no-op and preserve the error', () => {
27
+ const err = new Error('fail');
28
+ const r = Result.fail(err).map(x => x * 2);
29
+ expect(r.isFail()).toBe(true);
30
+ expect(r.error).toBe(err);
31
+ });
32
+
33
+ it('Ok.flatMap() should chain another Result', () => {
34
+ const r = Result.ok(5).flatMap(x => Result.ok(x + 10));
35
+ expect(r.value).toBe(15);
36
+ });
37
+
38
+ it('Fail.flatMap() should be a no-op', () => {
39
+ const err = new Error('fail');
40
+ const r = Result.fail(err).flatMap(x => Result.ok(x + 10));
41
+ expect(r.isFail()).toBe(true);
42
+ expect(r.error).toBe(err);
43
+ });
44
+
45
+ it('Ok.getOrElse() should return the value', () => {
46
+ expect(Result.ok('hello').getOrElse('fallback')).toBe('hello');
47
+ });
48
+
49
+ it('Fail.getOrElse() should return the fallback', () => {
50
+ expect(Result.fail(new Error()).getOrElse('fallback')).toBe('fallback');
51
+ });
52
+
53
+ it('Result.try() should wrap a throwing function in Fail', () => {
54
+ const r = Result.try(() => { throw new Error('oops'); });
55
+ expect(r.isFail()).toBe(true);
56
+ expect(r.error.message).toBe('oops');
57
+ });
58
+
59
+ it('Result.try() should wrap a successful function in Ok', () => {
60
+ const r = Result.try(() => 99);
61
+ expect(r.isOk()).toBe(true);
62
+ expect(r.value).toBe(99);
63
+ });
64
+
65
+ it('Result.tryAsync() should wrap a rejected promise in Fail', async () => {
66
+ const r = await Result.tryAsync(() => Promise.reject(new Error('async error')));
67
+ expect(r.isFail()).toBe(true);
68
+ expect(r.error.message).toBe('async error');
69
+ });
70
+
71
+ it('Result.tryAsync() should wrap a resolved promise in Ok', async () => {
72
+ const r = await Result.tryAsync(() => Promise.resolve('done'));
73
+ expect(r.isOk()).toBe(true);
74
+ expect(r.value).toBe('done');
75
+ });
76
+ });