@anilkumarthakur/match
Advanced tools
| import { match } from './match'; | ||
| /** | ||
| * Create a new match expression | ||
| * | ||
| * @template TSubject The type of the value being matched | ||
| * @template TResult The return type of handler functions | ||
| * | ||
| * @param {TSubject} subject The value to match against | ||
| * @returns {Matcher<TSubject, TResult>} A Matcher instance | ||
| * | ||
| * @see {@link Matcher} For complete API documentation | ||
| */ | ||
| export { match }; | ||
| /** | ||
| * Core classes and types for the match expression library | ||
| */ | ||
| export { Matcher, UnhandledMatchError } from './Matcher'; | ||
| /** | ||
| * Type for match handler functions | ||
| * | ||
| * @template T The return type of the handler | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const handler: Handler<string> = () => 'result' | ||
| * ``` | ||
| */ | ||
| export type { Handler, MatchChain, MatcherHandler } from './types/main'; | ||
| //# sourceMappingURL=index.d.ts.map |
| {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAE/B;;;;;;;;;;GAUG;AACH,OAAO,EAAE,KAAK,EAAE,CAAA;AAEhB;;GAEG;AACH,OAAO,EAAE,OAAO,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAA;AAExD;;;;;;;;;GASG;AACH,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA"} |
| /** | ||
| * Re-export module for backwards compatibility | ||
| * | ||
| * This module maintains backwards compatibility by re-exporting | ||
| * from the main Matcher module. New code should import from | ||
| * the root index.ts or directly from Matcher.ts | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Backwards compatible (legacy) | ||
| * import { match } from './match' | ||
| * | ||
| * // Recommended | ||
| * import { match } from '@anilkumarthakur/match' | ||
| * ``` | ||
| */ | ||
| export { match } from './Matcher'; | ||
| /** | ||
| * Re-export all types from the types module | ||
| */ | ||
| export type { MatchChain, Handler, MatcherHandler } from './types/main'; | ||
| //# sourceMappingURL=match.d.ts.map |
| {"version":3,"file":"match.d.ts","sourceRoot":"","sources":["../../src/match.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAA;AAEjC;;GAEG;AACH,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA"} |
| import { MatcherHandler } from './types/main'; | ||
| /** | ||
| * Error thrown when a match expression has no matching case and no default handler | ||
| * | ||
| * @class UnhandledMatchError | ||
| * @extends Error | ||
| * | ||
| * @example | ||
| * try { | ||
| * match('foo') | ||
| * .on('bar', () => 'never matches') | ||
| * .valueOf() | ||
| * } catch (error) { | ||
| * if (error instanceof UnhandledMatchError) { | ||
| * console.error('No match found') | ||
| * } | ||
| * } | ||
| */ | ||
| declare class UnhandledMatchError extends Error { | ||
| /** | ||
| * Create an UnhandledMatchError | ||
| * | ||
| * @param {unknown} value The value that could not be matched | ||
| */ | ||
| constructor(value: unknown); | ||
| } | ||
| /** | ||
| * Matcher class implementing PHP-style match expressions for TypeScript/JavaScript | ||
| * Supports exhaustive matching with type safety and O(1) lookup performance | ||
| * | ||
| * @template TSubject The type of values being matched against | ||
| * @template TResult The return type of the match expression | ||
| * | ||
| * @class Matcher | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const result = match('foo') | ||
| * .on('foo', () => 'matched foo') | ||
| * .on('bar', () => 'matched bar') | ||
| * .otherwise(() => 'default') | ||
| * ``` | ||
| * | ||
| * @example HTTP Status Codes | ||
| * ```typescript | ||
| * const message = match(statusCode) | ||
| * .on(200, () => 'OK') | ||
| * .onAny([201, 202], () => 'Accepted') | ||
| * .on(404, () => 'Not Found') | ||
| * .otherwise(() => 'Unknown') | ||
| * ``` | ||
| */ | ||
| declare class Matcher<TSubject, TResult> { | ||
| /** | ||
| * The value being matched against | ||
| * @private | ||
| */ | ||
| private readonly subject; | ||
| /** | ||
| * Map of values to their corresponding handler functions | ||
| * Uses Map for O(1) lookup performance | ||
| * @private | ||
| */ | ||
| private readonly matches; | ||
| /** | ||
| * Default handler to execute if no cases match | ||
| * @private | ||
| */ | ||
| private defaultHandler?; | ||
| /** | ||
| * Create a new Matcher instance | ||
| * | ||
| * @param {TSubject} subject The value to match against | ||
| * | ||
| * @internal Use the `match()` function instead of instantiating directly | ||
| */ | ||
| constructor(subject: TSubject); | ||
| /** | ||
| * Add a case to match against the subject | ||
| * Uses strict equality (===) for comparison | ||
| * | ||
| * @param {TSubject} value The value to match against | ||
| * @param {MatcherHandler<TResult>} handler Function to execute if this value matches | ||
| * @returns {this} The matcher instance for method chaining | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * match('hello') | ||
| * .on('hello', () => 'matched') | ||
| * .on('goodbye', () => 'not matched') | ||
| * ``` | ||
| */ | ||
| on(value: TSubject, handler: MatcherHandler<TResult>): this; | ||
| /** | ||
| * Add multiple values that map to the same handler | ||
| * Simulates PHP's comma-separated case syntax | ||
| * | ||
| * @param {readonly TSubject[]} values Array of values to match | ||
| * @param {MatcherHandler<TResult>} handler Function to execute if any value matches | ||
| * @returns {this} The matcher instance for method chaining | ||
| * | ||
| * @example HTTP Status Codes | ||
| * ```typescript | ||
| * match(statusCode) | ||
| * .onAny([200, 201, 202], () => 'Success') | ||
| * .onAny([400, 401, 403], () => 'Client Error') | ||
| * .otherwise(() => 'Unknown') | ||
| * ``` | ||
| * | ||
| * @see on For matching a single value | ||
| */ | ||
| onAny(values: readonly TSubject[], handler: MatcherHandler<TResult>): this; | ||
| /** | ||
| * Set the default handler and execute the match expression | ||
| * This method triggers evaluation of all accumulated cases | ||
| * | ||
| * @param {MatcherHandler<TResult>} handler Function to execute if no cases match | ||
| * @returns {TResult} The result from the matched handler or the default handler | ||
| * @throws {UnhandledMatchError} If no case matches and no handler catches it | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const result = match(status) | ||
| * .on('active', () => 'Active') | ||
| * .on('inactive', () => 'Inactive') | ||
| * .otherwise(() => 'Unknown status') | ||
| * ``` | ||
| * | ||
| * @see default For PHP-compatible alias | ||
| * @see valueOf For executing without a default handler | ||
| */ | ||
| otherwise(handler: MatcherHandler<TResult>): TResult; | ||
| /** | ||
| * PHP-compatible alias for otherwise() | ||
| * Identical behavior - sets default handler and executes | ||
| * | ||
| * @param {MatcherHandler<TResult>} handler Function to execute if no cases match | ||
| * @returns {TResult} The result from the matched handler or the default handler | ||
| * @throws {UnhandledMatchError} If no case matches | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // PHP-style syntax | ||
| * const result = match(value) | ||
| * .on('case1', () => 'Result1') | ||
| * .default(() => 'Default') | ||
| * ``` | ||
| * | ||
| * @see otherwise For the standard method | ||
| */ | ||
| default(handler: MatcherHandler<TResult>): TResult; | ||
| /** | ||
| * Execute the match expression without a default handler | ||
| * Throws if no case matches | ||
| * | ||
| * @returns {TResult} The result from the matched handler | ||
| * @throws {UnhandledMatchError} If no case matches | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * try { | ||
| * const result = match(code) | ||
| * .on(200, () => 'OK') | ||
| * .on(404, () => 'Not Found') | ||
| * .valueOf() // Must have matched | ||
| * } catch (error) { | ||
| * if (error instanceof UnhandledMatchError) { | ||
| * console.error('Invalid code:', error.message) | ||
| * } | ||
| * } | ||
| * ``` | ||
| * | ||
| * @see otherwise For safe execution with default handler | ||
| */ | ||
| valueOf(): TResult; | ||
| /** | ||
| * Evaluate the match expression by finding the matching case | ||
| * | ||
| * @private | ||
| * @returns {TResult} The result from matched handler or default | ||
| * @throws {UnhandledMatchError} If no match and no default handler | ||
| */ | ||
| private evaluate; | ||
| } | ||
| /** | ||
| * Create a new PHP-style match expression | ||
| * | ||
| * @template TSubject The type of the value being matched | ||
| * @template TResult The return type of the match expression handlers | ||
| * | ||
| * @param {TSubject} subject The value to match against (any type) | ||
| * @returns {Matcher<TSubject, TResult>} A Matcher instance for method chaining | ||
| * | ||
| * @example Basic String Matching | ||
| * ```typescript | ||
| * const status = match(statusCode) | ||
| * .on(200, () => 'success') | ||
| * .on(404, () => 'not found') | ||
| * .otherwise(() => 'error') | ||
| * ``` | ||
| * | ||
| * @example HTTP Status Codes | ||
| * ```typescript | ||
| * const message = match(code) | ||
| * .onAny([200, 201, 202], () => 'Success') | ||
| * .onAny([400, 401, 403], () => 'Client Error') | ||
| * .on(500, () => 'Server Error') | ||
| * .otherwise(() => 'Unknown') | ||
| * ``` | ||
| * | ||
| * @example Conditional Logic | ||
| * ```typescript | ||
| * const result = match(true) | ||
| * .on(age < 18, () => 'Minor') | ||
| * .on(age >= 18 && age < 65, () => 'Adult') | ||
| * .on(age >= 65, () => 'Senior') | ||
| * .otherwise(() => 'Unknown') | ||
| * ``` | ||
| * | ||
| * @see Matcher For complete API documentation | ||
| */ | ||
| declare function match<TSubject, TResult>(subject: TSubject): Matcher<TSubject, TResult>; | ||
| export { match, UnhandledMatchError, Matcher }; | ||
| //# sourceMappingURL=Matcher.d.ts.map |
| {"version":3,"file":"Matcher.d.ts","sourceRoot":"","sources":["../../src/Matcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAElD;;;;;;;;;;;;;;;;GAgBG;AACH,cAAM,mBAAoB,SAAQ,KAAK;IACrC;;;;OAIG;gBACS,KAAK,EAAE,OAAO;CAI3B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,cAAM,OAAO,CAAC,QAAQ,EAAE,OAAO;IAC7B;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;IAElC;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoD;IAE5E;;;OAGG;IACH,OAAO,CAAC,cAAc,CAAC,CAAyB;IAEhD;;;;;;OAMG;gBACS,OAAO,EAAE,QAAQ;IAI7B;;;;;;;;;;;;;;OAcG;IACH,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,cAAc,CAAC,OAAO,CAAC,GAAG,IAAI;IAK3D;;;;;;;;;;;;;;;;;OAiBG;IACH,KAAK,CAAC,MAAM,EAAE,SAAS,QAAQ,EAAE,EAAE,OAAO,EAAE,cAAc,CAAC,OAAO,CAAC,GAAG,IAAI;IAK1E;;;;;;;;;;;;;;;;;;OAkBG;IACH,SAAS,CAAC,OAAO,EAAE,cAAc,CAAC,OAAO,CAAC,GAAG,OAAO;IAKpD;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,OAAO,EAAE,cAAc,CAAC,OAAO,CAAC,GAAG,OAAO;IAIlD;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,OAAO,IAAI,OAAO;IAIlB;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ;CAQjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,iBAAS,KAAK,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,CAE/E;AAED,OAAO,EAAE,KAAK,EAAE,mBAAmB,EAAE,OAAO,EAAE,CAAA"} |
| /** | ||
| * Internal handler function type for match expression results | ||
| * | ||
| * Used internally by the Matcher class to store and execute handler functions. | ||
| * This is an alias for Handler<T> with the same signature. | ||
| * | ||
| * @template T The return type of the handler function | ||
| * | ||
| * @internal Internal use only | ||
| */ | ||
| export type MatcherHandler<T> = () => T; | ||
| /** | ||
| * Handler function type for match expression results | ||
| * | ||
| * A handler is a function that takes no parameters and returns a value | ||
| * of type T. Used in match expressions to define what happens when a case matches. | ||
| * | ||
| * @template T The return type of the handler function | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const handler: Handler<string> = () => 'matched' | ||
| * const numHandler: Handler<number> = () => 42 | ||
| * ``` | ||
| */ | ||
| export type Handler<T> = () => T; | ||
| /** | ||
| * Interface representing a chainable match expression | ||
| * | ||
| * Provides the API contract for method chaining in match expressions. | ||
| * Implementations should support fluent interface patterns. | ||
| * | ||
| * @template TSubject The type of the value being matched against | ||
| * @template TResult The return type of handler functions | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * interface MatchChain<string, number> { | ||
| * on: (value: string, handler: Handler<number>) => MatchChain<string, number> | ||
| * otherwise: (handler: Handler<number>) => number | ||
| * } | ||
| * ``` | ||
| */ | ||
| export interface MatchChain<TSubject, TResult> { | ||
| /** | ||
| * Add a case to match against the subject | ||
| * | ||
| * @param {TSubject} value The value to match | ||
| * @param {Handler<TResult>} handler Function to execute if matched | ||
| * @returns {MatchChain<TSubject, TResult>} The matcher for chaining | ||
| */ | ||
| on: (value: TSubject, handler: Handler<TResult>) => MatchChain<TSubject, TResult>; | ||
| /** | ||
| * Set default handler and execute the match | ||
| * | ||
| * @param {Handler<TResult>} handler Function to execute if no cases match | ||
| * @returns {TResult} The result from matched handler or default | ||
| */ | ||
| otherwise: (handler: Handler<TResult>) => TResult; | ||
| } | ||
| //# sourceMappingURL=main.d.ts.map |
| {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../src/types/main.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI,MAAM,CAAC,CAAA;AAEvC;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,OAAO,CAAC,CAAC,IAAI,MAAM,CAAC,CAAA;AAEhC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,UAAU,CAAC,QAAQ,EAAE,OAAO;IAC3C;;;;;;OAMG;IACH,EAAE,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,UAAU,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;IAEjF;;;;;OAKG;IACH,SAAS,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO,CAAA;CAClD"} |
| export {}; | ||
| //# sourceMappingURL=comprehensive.test.d.ts.map |
| {"version":3,"file":"comprehensive.test.d.ts","sourceRoot":"","sources":["../../test/comprehensive.test.ts"],"names":[],"mappings":""} |
| export {}; | ||
| //# sourceMappingURL=coverage.test.d.ts.map |
| {"version":3,"file":"coverage.test.d.ts","sourceRoot":"","sources":["../../test/coverage.test.ts"],"names":[],"mappings":""} |
| export {}; | ||
| //# sourceMappingURL=match.test.d.ts.map |
| {"version":3,"file":"match.test.d.ts","sourceRoot":"","sources":["../../test/match.test.ts"],"names":[],"mappings":""} |
| export {}; | ||
| //# sourceMappingURL=match1.test.d.ts.map |
| {"version":3,"file":"match1.test.d.ts","sourceRoot":"","sources":["../../test/match1.test.ts"],"names":[],"mappings":""} |
| export {}; | ||
| //# sourceMappingURL=match2.test.d.ts.map |
| {"version":3,"file":"match2.test.d.ts","sourceRoot":"","sources":["../../test/match2.test.ts"],"names":[],"mappings":""} |
| import { match, UnhandledMatchError, Matcher } from '../src/Matcher' | ||
| beforeEach(() => { | ||
| jest.clearAllMocks() | ||
| }) | ||
| describe('Match Expression - Comprehensive Test Suite', () => { | ||
| // ============================================================================ | ||
| // BASIC FUNCTIONALITY TESTS | ||
| // ============================================================================ | ||
| describe('Basic Functionality', () => { | ||
| test('should return the correct action for the matched case', () => { | ||
| const result = match('test') | ||
| .on('test', () => 'matched') | ||
| .on('not matched', () => 'not matched') | ||
| .otherwise(() => 'otherwise') | ||
| expect(result).toBe('matched') | ||
| }) | ||
| test('should return the otherwise action if no cases are matched', () => { | ||
| const result = match('test') | ||
| .on('not matched', () => 'not matched') | ||
| .otherwise(() => 'otherwise') | ||
| expect(result).toBe('otherwise') | ||
| expect(result).not.toBe('not matched') | ||
| }) | ||
| test('should correctly handle multiple cases with one match', () => { | ||
| const result = match('second') | ||
| .on('first', () => 'first case') | ||
| .on('second', () => 'second case') | ||
| .on('third', () => 'third case') | ||
| .otherwise(() => 'otherwise') | ||
| expect(result).toBe('second case') | ||
| expect(result).not.toBe('first case') | ||
| expect(result).not.toBe('third case') | ||
| expect(result).not.toBe('otherwise') | ||
| }) | ||
| test('should execute the default action when provided', () => { | ||
| const result = match('none') | ||
| .on('first', () => 'first case') | ||
| .on('second', () => 'second case') | ||
| .otherwise(() => 'default action') | ||
| expect(result).toBe('default action') | ||
| expect(result).not.toBe('first case') | ||
| expect(result).not.toBe('second case') | ||
| }) | ||
| test('should correctly handle no cases with only otherwise', () => { | ||
| const result = match('none').otherwise(() => 'default action') | ||
| expect(result).toBe('default action') | ||
| expect(result).not.toBe('first case') | ||
| }) | ||
| test('executes the matching handler when subject matches an on condition', () => { | ||
| const result = match('success') | ||
| .on('success', () => 'success-handler') | ||
| .on('error', () => 'error-handler') | ||
| .otherwise(() => 'otherwise-handler') | ||
| expect(result).toBe('success-handler') | ||
| }) | ||
| test('executes otherwise handler if no conditions match', () => { | ||
| const result = match('not-found') | ||
| .on('success', () => 'success-handler') | ||
| .on('error', () => 'error-handler') | ||
| .otherwise(() => 'otherwise-handler') | ||
| expect(result).toBe('otherwise-handler') | ||
| }) | ||
| test('executes otherwise if no handler is defined at all', () => { | ||
| const result = match('anything').otherwise(() => 'no-cases') | ||
| expect(result).toBe('no-cases') | ||
| }) | ||
| }) | ||
| // ============================================================================ | ||
| // TYPE MATCHING TESTS | ||
| // ============================================================================ | ||
| describe('Type Matching', () => { | ||
| describe('String Matching', () => { | ||
| test('matches string literal', () => { | ||
| expect( | ||
| match('hello') | ||
| .on('hello', () => 'matched') | ||
| .otherwise(() => 'default') | ||
| ).toBe('matched') | ||
| expect( | ||
| match('world') | ||
| .on('hello', () => 'matched') | ||
| .on('world', () => 'world') | ||
| .otherwise(() => 'default') | ||
| ).toBe('world') | ||
| }) | ||
| test('string does not match different string', () => { | ||
| expect( | ||
| match('hello') | ||
| .on('world', () => 'world') | ||
| .on('hellos', () => 'hellos') | ||
| .otherwise(() => 'default') | ||
| ).toBe('default') | ||
| }) | ||
| test('empty string matches', () => { | ||
| expect( | ||
| match('') | ||
| .on('', () => 'empty') | ||
| .otherwise(() => 'default') | ||
| ).toBe('empty') | ||
| }) | ||
| }) | ||
| describe('Number Matching', () => { | ||
| test('matches integer', () => { | ||
| expect( | ||
| match(42) | ||
| .on(42, () => 'forty-two') | ||
| .otherwise(() => 'default') | ||
| ).toBe('forty-two') | ||
| }) | ||
| test('matches zero', () => { | ||
| expect( | ||
| match(0) | ||
| .on(0, () => 'zero') | ||
| .otherwise(() => 'default') | ||
| ).toBe('zero') | ||
| }) | ||
| test('+0 matches -0', () => { | ||
| expect( | ||
| match(+0) | ||
| .on(-0, () => 'zero matched') | ||
| .otherwise(() => 'default') | ||
| ).toBe('zero matched') | ||
| }) | ||
| test('matches negative number', () => { | ||
| expect( | ||
| match(-1) | ||
| .on(-1, () => 'negative one') | ||
| .otherwise(() => 'default') | ||
| ).toBe('negative one') | ||
| }) | ||
| test('matches decimal number', () => { | ||
| expect( | ||
| match(3.14) | ||
| .on(3.14, () => 'pi') | ||
| .otherwise(() => 'default') | ||
| ).toBe('pi') | ||
| }) | ||
| test('does not match different number', () => { | ||
| expect( | ||
| match(10) | ||
| .on(9, () => 'nine') | ||
| .otherwise(() => 'default') | ||
| ).toBe('default') | ||
| }) | ||
| test('0 subject matches -0 key', () => { | ||
| expect( | ||
| match(0) | ||
| .on(-0, () => 'minus zero') | ||
| .otherwise(() => 'default') | ||
| ).toBe('minus zero') | ||
| }) | ||
| test('matches Infinity', () => { | ||
| expect( | ||
| match(Infinity) | ||
| .on(Infinity, () => 'infinity matched') | ||
| .otherwise(() => 'default') | ||
| ).toBe('infinity matched') | ||
| }) | ||
| test('matches -Infinity', () => { | ||
| expect( | ||
| match(-Infinity) | ||
| .on(-Infinity, () => 'minus infinity matched') | ||
| .otherwise(() => 'default') | ||
| ).toBe('minus infinity matched') | ||
| }) | ||
| }) | ||
| describe('Boolean Matching', () => { | ||
| test('should correctly handle default true parameter', () => { | ||
| const result = match(true) | ||
| .on(true, () => true) | ||
| .otherwise(() => 'default action') | ||
| expect(result).toBe(true) | ||
| expect(result).not.toBe('default action') | ||
| }) | ||
| test('should correctly handle default false parameter', () => { | ||
| const result = match(true) | ||
| .on(true, () => 'true case') | ||
| .otherwise(() => 'default action') | ||
| expect(result).toBe('true case') | ||
| expect(result).not.toBe('default action') | ||
| }) | ||
| test('should correctly handle default false case', () => { | ||
| const result = match(false) | ||
| .on(true, () => true) | ||
| .otherwise(() => 'default action') | ||
| expect(result).toBe('default action') | ||
| expect(result).not.toBe(true) | ||
| }) | ||
| test('should correctly handle default true case', () => { | ||
| const result = match(false) | ||
| .on(false, () => false) | ||
| .otherwise(() => 'default action') | ||
| expect(result).toBe(false) | ||
| expect(result).not.toBe('default action') | ||
| }) | ||
| test('matches true', () => { | ||
| expect( | ||
| match(true) | ||
| .on(true, () => 'yes') | ||
| .otherwise(() => 'no') | ||
| ).toBe('yes') | ||
| }) | ||
| test('matches false', () => { | ||
| expect( | ||
| match(false) | ||
| .on(false, () => 'no') | ||
| .otherwise(() => 'yes') | ||
| ).toBe('no') | ||
| }) | ||
| test('false does not match true', () => { | ||
| expect( | ||
| match(false) | ||
| .on(true, () => 'yes') | ||
| .otherwise(() => 'no') | ||
| ).toBe('no') | ||
| }) | ||
| }) | ||
| describe('Null and Undefined Matching', () => { | ||
| test('should correctly handle default null case', () => { | ||
| const result = match(null) | ||
| .on(null, () => null) | ||
| .otherwise(() => 'default action') | ||
| expect(result).toBe(null) | ||
| expect(result).not.toBe('default action') | ||
| }) | ||
| test('should correctly handle default undefined case', () => { | ||
| const result = match(undefined) | ||
| .on(undefined, () => undefined) | ||
| .otherwise(() => 'default action') | ||
| expect(result).toBe(undefined) | ||
| expect(result).not.toBe('default action') | ||
| }) | ||
| test('matches null', () => { | ||
| expect( | ||
| match(null) | ||
| .on(null, () => 'null matched') | ||
| .otherwise(() => 'default') | ||
| ).toBe('null matched') | ||
| }) | ||
| test('matches undefined', () => { | ||
| expect( | ||
| match(undefined) | ||
| .on(undefined, () => 'undefined matched') | ||
| .otherwise(() => 'default') | ||
| ).toBe('undefined matched') | ||
| }) | ||
| test('subject null matches correctly with side effect', () => { | ||
| const fn = jest.fn(() => 'matched') | ||
| const result = match(null) | ||
| .on(null, fn) | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('matched') | ||
| expect(fn).toHaveBeenCalledTimes(1) | ||
| }) | ||
| }) | ||
| describe('Symbol Matching', () => { | ||
| test('matches same symbol', () => { | ||
| const sym = Symbol('foo') | ||
| expect( | ||
| match(sym) | ||
| .on(sym, () => 'symbol matched') | ||
| .otherwise(() => 'default') | ||
| ).toBe('symbol matched') | ||
| }) | ||
| test('does not match different symbols', () => { | ||
| expect( | ||
| match(Symbol('foo')) | ||
| .on(Symbol('foo'), () => 'symbol matched') | ||
| .otherwise(() => 'default') | ||
| ).toBe('default') | ||
| }) | ||
| }) | ||
| describe('BigInt Matching', () => { | ||
| test('matches BigInt', () => { | ||
| expect( | ||
| match(10n) | ||
| .on(10n, () => 'bigint matched') | ||
| .otherwise(() => 'default') | ||
| ).toBe('bigint matched') | ||
| }) | ||
| test('does not match different BigInt', () => { | ||
| expect( | ||
| match(10n) | ||
| .on(20n, () => 'bigint matched') | ||
| .otherwise(() => 'default') | ||
| ).toBe('default') | ||
| }) | ||
| }) | ||
| describe('Object and Array Reference Matching', () => { | ||
| test('matches same object reference', () => { | ||
| const obj = { a: 1 } | ||
| expect( | ||
| match(obj) | ||
| .on(obj, () => 'matched object') | ||
| .otherwise(() => 'default') | ||
| ).toBe('matched object') | ||
| }) | ||
| test('does not match identical object by value', () => { | ||
| expect( | ||
| match({ a: 1 }) | ||
| .on({ a: 1 }, () => 'matched object') | ||
| .otherwise(() => 'default') | ||
| ).toBe('default') | ||
| }) | ||
| test('matches same array reference', () => { | ||
| const arr = [1, 2] | ||
| expect( | ||
| match(arr) | ||
| .on(arr, () => 'matched array') | ||
| .otherwise(() => 'default') | ||
| ).toBe('matched array') | ||
| }) | ||
| test('does not match identical array by value', () => { | ||
| expect( | ||
| match([1, 2]) | ||
| .on([1, 2], () => 'matched array') | ||
| .otherwise(() => 'default') | ||
| ).toBe('default') | ||
| }) | ||
| test('object with toString does not affect matching', () => { | ||
| const obj = { | ||
| toString() { | ||
| return 'foo' | ||
| } | ||
| } | ||
| expect( | ||
| match(obj) | ||
| .on(obj, () => 'matched') | ||
| .otherwise(() => 'default') | ||
| ).toBe('matched') | ||
| }) | ||
| }) | ||
| describe('Function and Class Instance Matching', () => { | ||
| test('matches same function reference', () => { | ||
| const fn = () => {} | ||
| expect( | ||
| match(fn) | ||
| .on(fn, () => 'matched fn') | ||
| .otherwise(() => 'default') | ||
| ).toBe('matched fn') | ||
| }) | ||
| test('does not match different function with same implementation', () => { | ||
| expect( | ||
| match(() => {}) | ||
| .on( | ||
| () => {}, | ||
| () => 'matched fn' | ||
| ) | ||
| .otherwise(() => 'default') | ||
| ).toBe('default') | ||
| }) | ||
| test('class instance matching by reference', () => { | ||
| class A {} | ||
| const a = new A() | ||
| expect( | ||
| match(a) | ||
| .on(a, () => 'matched instance') | ||
| .otherwise(() => 'default') | ||
| ).toBe('matched instance') | ||
| }) | ||
| test('different class instance no match', () => { | ||
| class A {} | ||
| expect( | ||
| match(new A()) | ||
| .on(new A(), () => 'matched instance') | ||
| .otherwise(() => 'default') | ||
| ).toBe('default') | ||
| }) | ||
| }) | ||
| describe('Enum Matching', () => { | ||
| enum Color { | ||
| Red, | ||
| Blue, | ||
| Green | ||
| } | ||
| test('matches TypeScript enum', () => { | ||
| expect( | ||
| match(Color.Blue) | ||
| .on(Color.Red, () => 'red') | ||
| .on(Color.Blue, () => 'blue') | ||
| .on(Color.Green, () => 'green') | ||
| .otherwise(() => 'unknown') | ||
| ).toBe('blue') | ||
| }) | ||
| }) | ||
| }) | ||
| // ============================================================================ | ||
| // ON METHOD TESTS | ||
| // ============================================================================ | ||
| describe('on() Method', () => { | ||
| test('works with multiple conditions and ensures the first match is used', () => { | ||
| const result = match('spinner') | ||
| .on('success', () => 'success-handler') | ||
| .on('error', () => 'error-handler') | ||
| .on('warning', () => 'warning-handler') | ||
| .on('info', () => 'info-handler') | ||
| .on('defaultNotify', () => 'defaultNotify-handler') | ||
| .on('dark', () => 'dark-handler') | ||
| .on('light', () => 'light-handler') | ||
| .on('spinner', () => 'spinner-handler') | ||
| .otherwise(() => 'otherwise-handler') | ||
| expect(result).toBe('spinner-handler') | ||
| }) | ||
| test('executes the correct handler when multiple .on are provided and matches the later one', () => { | ||
| const result = match('error') | ||
| .on('info', () => 'info-handler') | ||
| .on('success', () => 'success-handler') | ||
| .on('error', () => 'error-handler') | ||
| .on('warning', () => 'warning-handler') | ||
| .otherwise(() => 'otherwise-handler') | ||
| expect(result).toBe('error-handler') | ||
| }) | ||
| test('chain .on returns this for chaining', () => { | ||
| const matcher = match('a') | ||
| .on('a', () => 'A') | ||
| .on('b', () => 'B') | ||
| expect(typeof matcher.otherwise).toBe('function') | ||
| }) | ||
| test('many chained .on calls', () => { | ||
| const result = match('x') | ||
| .on('a', () => 'A') | ||
| .on('b', () => 'B') | ||
| .on('c', () => 'C') | ||
| .on('x', () => 'X') | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('X') | ||
| }) | ||
| test('duplicate keys overwrite previous handlers', () => { | ||
| const result = match('key') | ||
| .on('key', () => 'first') | ||
| .on('key', () => 'second') | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('second') | ||
| }) | ||
| test('same handler used for multiple keys', () => { | ||
| const handler = jest.fn(() => 'handled') | ||
| const result = match('bar') | ||
| .on('foo', handler) | ||
| .on('bar', handler) | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('handled') | ||
| expect(handler).toHaveBeenCalledTimes(1) | ||
| }) | ||
| }) | ||
| // ============================================================================ | ||
| // ONANY METHOD TESTS | ||
| // ============================================================================ | ||
| describe('onAny() Method', () => { | ||
| test('onAny - matches multiple values to same handler', () => { | ||
| const result = match('a') | ||
| .onAny(['a', 'b', 'c'], () => 'matched') | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('matched') | ||
| }) | ||
| test('onAny - matches second value in array', () => { | ||
| const result = match('b') | ||
| .onAny(['a', 'b', 'c'], () => 'matched') | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('matched') | ||
| }) | ||
| test('onAny - matches last value in array', () => { | ||
| const result = match('c') | ||
| .onAny(['a', 'b', 'c'], () => 'matched') | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('matched') | ||
| }) | ||
| test('onAny - does not match value outside array', () => { | ||
| const result = match('d') | ||
| .onAny(['a', 'b', 'c'], () => 'matched') | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('default') | ||
| }) | ||
| test('onAny - works with numbers', () => { | ||
| const result = match(2) | ||
| .onAny([1, 2, 3], () => 'matched') | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('matched') | ||
| }) | ||
| test('onAny - empty array does not match', () => { | ||
| const result = match('a') | ||
| .onAny([], () => 'matched') | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('default') | ||
| }) | ||
| test('onAny - chaining with multiple onAny calls', () => { | ||
| const result = match('x') | ||
| .onAny(['a', 'b'], () => 'first') | ||
| .onAny(['x', 'y'], () => 'second') | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('second') | ||
| }) | ||
| test('onAny - readonly array support', () => { | ||
| const values: readonly string[] = ['foo', 'bar'] | ||
| const result = match('foo') | ||
| .onAny(values, () => 'matched') | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('matched') | ||
| }) | ||
| test('onAny - mixed with on() method', () => { | ||
| const result = match('b') | ||
| .on('a', () => 'single') | ||
| .onAny(['b', 'c'], () => 'multiple') | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('multiple') | ||
| }) | ||
| test('onAny - handler with side effects', () => { | ||
| const fn = jest.fn(() => 'matched') | ||
| const result = match('b') | ||
| .onAny(['a', 'b', 'c'], fn) | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('matched') | ||
| expect(fn).toHaveBeenCalledTimes(1) | ||
| }) | ||
| test('onAny - returns this for chaining', () => { | ||
| const matcher = match('a') | ||
| .onAny(['a', 'b'], () => 'test') | ||
| expect(typeof matcher.on).toBe('function') | ||
| expect(typeof matcher.otherwise).toBe('function') | ||
| }) | ||
| }) | ||
| // ============================================================================ | ||
| // OTHERWISE METHOD TESTS | ||
| // ============================================================================ | ||
| describe('otherwise() Method', () => { | ||
| test('default handler called', () => { | ||
| const defFn = jest.fn(() => 'default') | ||
| const result = match('nope') | ||
| .on('something', () => 'something') | ||
| .otherwise(defFn) | ||
| expect(result).toBe('default') | ||
| expect(defFn).toHaveBeenCalledTimes(1) | ||
| }) | ||
| test('multiple otherwise calls use last handler', () => { | ||
| const matcher = match('foo').on('bar', () => 'bar') | ||
| expect(matcher.otherwise(() => 'first')).toBe('first') | ||
| expect(matcher.otherwise(() => 'second')).toBe('second') | ||
| }) | ||
| test('check that console logs or side effects can happen inside handlers', () => { | ||
| const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {}) | ||
| const result = match('warning') | ||
| .on('success', () => { | ||
| console.log('success log') | ||
| return 'success-handler' | ||
| }) | ||
| .on('warning', () => { | ||
| console.log('warning log') | ||
| return 'warning-handler' | ||
| }) | ||
| .otherwise(() => { | ||
| console.log('otherwise log') | ||
| return 'otherwise-handler' | ||
| }) | ||
| expect(result).toBe('warning-handler') | ||
| expect(consoleSpy).toHaveBeenCalledWith('warning log') | ||
| expect(consoleSpy).not.toHaveBeenCalledWith('success log') | ||
| expect(consoleSpy).not.toHaveBeenCalledWith('otherwise log') | ||
| consoleSpy.mockRestore() | ||
| }) | ||
| }) | ||
| // ============================================================================ | ||
| // DEFAULT METHOD TESTS | ||
| // ============================================================================ | ||
| describe('default() Method - PHP Compatibility', () => { | ||
| test('default - executes handler and returns result', () => { | ||
| const result = match('foo') | ||
| .on('bar', () => 'bar') | ||
| .default(() => 'default result') | ||
| expect(result).toBe('default result') | ||
| }) | ||
| test('default - matches case if found', () => { | ||
| const result = match('foo') | ||
| .on('foo', () => 'matched') | ||
| .default(() => 'default') | ||
| expect(result).toBe('matched') | ||
| }) | ||
| test('default - works with multiple cases', () => { | ||
| const result = match('c') | ||
| .on('a', () => 'a') | ||
| .on('b', () => 'b') | ||
| .default(() => 'default') | ||
| expect(result).toBe('default') | ||
| }) | ||
| test('default - returns various types', () => { | ||
| expect( | ||
| match('x') | ||
| .on('y', () => 'str') | ||
| .default(() => 'default') | ||
| ).toBe('default') | ||
| expect( | ||
| match('x') | ||
| .on('y', () => 123) | ||
| .default(() => 456) | ||
| ).toBe(456) | ||
| expect( | ||
| match('x') | ||
| .on('y', () => true) | ||
| .default(() => false) | ||
| ).toBe(false) | ||
| }) | ||
| test('default - is equivalent to otherwise', () => { | ||
| const matcher1 = match('test') | ||
| .on('other', () => 'other') | ||
| const matcher2 = match('test') | ||
| .on('other', () => 'other') | ||
| const defaultResult = matcher1.default(() => 'default') | ||
| const otherwiseResult = matcher2.otherwise(() => 'default') | ||
| expect(defaultResult).toBe(otherwiseResult) | ||
| }) | ||
| test('default - executes handler with side effects', () => { | ||
| const fn = jest.fn(() => 'result') | ||
| const result = match('no-match') | ||
| .on('something', () => 'something') | ||
| .default(fn) | ||
| expect(result).toBe('result') | ||
| expect(fn).toHaveBeenCalledTimes(1) | ||
| }) | ||
| test('default - can override previous default', () => { | ||
| const matcher = match('x').on('y', () => 'y') | ||
| expect(matcher.default(() => 'first')).toBe('first') | ||
| expect(matcher.default(() => 'second')).toBe('second') | ||
| }) | ||
| test('default - throws when no match and handler throws', () => { | ||
| expect(() => | ||
| match('x') | ||
| .on('y', () => 'y') | ||
| .default(() => { | ||
| throw new Error('Custom error') | ||
| }) | ||
| ).toThrow('Custom error') | ||
| }) | ||
| test('default - with null result', () => { | ||
| const result = match('x') | ||
| .on('y', () => 'y') | ||
| .default(() => null) | ||
| expect(result).toBeNull() | ||
| }) | ||
| test('default - with undefined result', () => { | ||
| const result = match('x') | ||
| .on('y', () => 'y') | ||
| .default(() => undefined) | ||
| expect(result).toBeUndefined() | ||
| }) | ||
| }) | ||
| // ============================================================================ | ||
| // VALUEOF METHOD TESTS | ||
| // ============================================================================ | ||
| describe('valueOf() Method', () => { | ||
| test('valueOf - returns matched handler result', () => { | ||
| const result = match('foo') | ||
| .on('foo', () => 'matched') | ||
| .valueOf() | ||
| expect(result).toBe('matched') | ||
| }) | ||
| test('valueOf - throws UnhandledMatchError when no match', () => { | ||
| expect(() => { | ||
| match('foo') | ||
| .on('bar', () => 'bar') | ||
| .valueOf() | ||
| }).toThrow(UnhandledMatchError) | ||
| }) | ||
| test('valueOf - throws with correct error message', () => { | ||
| expect(() => { | ||
| match('test-value') | ||
| .on('other', () => 'other') | ||
| .valueOf() | ||
| }).toThrow('Unhandled match value: "test-value"') | ||
| }) | ||
| test('valueOf - with multiple cases, first match wins', () => { | ||
| const result = match('b') | ||
| .on('a', () => 'first') | ||
| .on('b', () => 'second') | ||
| .on('c', () => 'third') | ||
| .valueOf() | ||
| expect(result).toBe('second') | ||
| }) | ||
| test('valueOf - returns various types', () => { | ||
| expect( | ||
| match('str') | ||
| .on('str', () => 'string result') | ||
| .valueOf() | ||
| ).toBe('string result') | ||
| expect( | ||
| match('num') | ||
| .on('num', () => 42) | ||
| .valueOf() | ||
| ).toBe(42) | ||
| expect( | ||
| match('bool') | ||
| .on('bool', () => true) | ||
| .valueOf() | ||
| ).toBe(true) | ||
| }) | ||
| test('valueOf - with object result', () => { | ||
| const obj = { key: 'value' } | ||
| const result = match('obj') | ||
| .on('obj', () => obj) | ||
| .valueOf() | ||
| expect(result).toBe(obj) | ||
| }) | ||
| test('valueOf - with array result', () => { | ||
| const arr = [1, 2, 3] | ||
| const result = match('arr') | ||
| .on('arr', () => arr) | ||
| .valueOf() | ||
| expect(result).toBe(arr) | ||
| }) | ||
| test('valueOf - with function result', () => { | ||
| const fn = () => 'test' | ||
| const result = match('fn') | ||
| .on('fn', () => fn) | ||
| .valueOf() | ||
| expect(result).toBe(fn) | ||
| }) | ||
| test('valueOf - with null result', () => { | ||
| const result = match('null') | ||
| .on('null', () => null) | ||
| .valueOf() | ||
| expect(result).toBeNull() | ||
| }) | ||
| test('valueOf - with undefined result', () => { | ||
| const result = match('undef') | ||
| .on('undef', () => undefined) | ||
| .valueOf() | ||
| expect(result).toBeUndefined() | ||
| }) | ||
| test('valueOf - called multiple times returns same result', () => { | ||
| const matcher = match('a') | ||
| .on('a', () => 'result') | ||
| expect(matcher.valueOf()).toBe('result') | ||
| expect(matcher.valueOf()).toBe('result') | ||
| }) | ||
| test('valueOf - throws error from handler', () => { | ||
| expect(() => { | ||
| match('error') | ||
| .on('error', () => { | ||
| throw new Error('Handler error') | ||
| }) | ||
| .valueOf() | ||
| }).toThrow('Handler error') | ||
| }) | ||
| test('valueOf - with complex nested matching', () => { | ||
| const result = match('status') | ||
| .on('loading', () => 'loading') | ||
| .on('status', () => { | ||
| return match('details') | ||
| .on('details', () => 'detailed status') | ||
| .valueOf() | ||
| }) | ||
| .valueOf() | ||
| expect(result).toBe('detailed status') | ||
| }) | ||
| test('Match function test', () => { | ||
| expect(() => | ||
| match(1) | ||
| .on(1, () => 1) | ||
| .otherwise(() => 'default action') | ||
| ).not.toThrow() | ||
| expect(() => | ||
| match(2) | ||
| .on(1, () => 1) | ||
| .otherwise(() => 'default action') | ||
| ).not.toThrow() | ||
| }) | ||
| }) | ||
| // ============================================================================ | ||
| // HANDLER BEHAVIOR TESTS | ||
| // ============================================================================ | ||
| describe('Handler Behavior and Side Effects', () => { | ||
| test('only calls matching handler', () => { | ||
| const fn1 = jest.fn(() => 'foo') | ||
| const fn2 = jest.fn(() => 'bar') | ||
| const fnDefault = jest.fn(() => 'default') | ||
| const result = match('bar').on('foo', fn1).on('bar', fn2).otherwise(fnDefault) | ||
| expect(result).toBe('bar') | ||
| expect(fn1).not.toHaveBeenCalled() | ||
| expect(fn2).toHaveBeenCalledTimes(1) | ||
| expect(fnDefault).not.toHaveBeenCalled() | ||
| }) | ||
| test('handler modifies external variable', () => { | ||
| let called = false | ||
| const result = match('test') | ||
| .on('test', () => { | ||
| called = true | ||
| return 'ok' | ||
| }) | ||
| .otherwise(() => 'fail') | ||
| expect(result).toBe('ok') | ||
| expect(called).toBe(true) | ||
| }) | ||
| test('handlers return various types', () => { | ||
| expect( | ||
| match('str') | ||
| .on('str', () => 'string') | ||
| .otherwise(() => 'default') | ||
| ).toBe('string') | ||
| expect( | ||
| match('num') | ||
| .on('num', () => 123) | ||
| .otherwise(() => 0) | ||
| ).toBe(123) | ||
| expect( | ||
| match('bool') | ||
| .on('bool', () => true) | ||
| .otherwise(() => false) | ||
| ).toBe(true) | ||
| const obj = { foo: 'bar' } | ||
| expect( | ||
| match('obj') | ||
| .on('obj', () => obj) | ||
| .otherwise(() => ({})) | ||
| ).toBe(obj) | ||
| expect( | ||
| match('undef') | ||
| .on('undef', () => undefined) | ||
| .otherwise(() => 'default') | ||
| ).toBeUndefined() | ||
| }) | ||
| test('async handler returns Promise', async () => { | ||
| const result = match('async') | ||
| .on('async', async () => 'resolved') | ||
| .otherwise(() => 'default') | ||
| await expect(result).resolves.toBe('resolved') | ||
| }) | ||
| test('handler throws exception', () => { | ||
| expect(() => | ||
| match('test') | ||
| .on('test', () => { | ||
| throw new Error('Handler error') | ||
| }) | ||
| .otherwise(() => 'default') | ||
| ).toThrow('Handler error') | ||
| }) | ||
| }) | ||
| // ============================================================================ | ||
| // ERROR HANDLING TESTS | ||
| // ============================================================================ | ||
| describe('Error Handling', () => { | ||
| test('throws UnhandledMatchError when no match and no default', () => { | ||
| expect(() => { | ||
| match('nope') | ||
| .on('something', () => 'something') | ||
| .otherwise(() => { | ||
| throw new UnhandledMatchError('nope') | ||
| }) | ||
| }).toThrow(UnhandledMatchError) | ||
| expect(() => { | ||
| match('nope') | ||
| .on('something', () => 'something') | ||
| .otherwise(() => { | ||
| throw new UnhandledMatchError('nope') | ||
| }) | ||
| }).toThrow('Unhandled match value: "nope"') | ||
| }) | ||
| test('throws if non-function handler is provided', () => { | ||
| expect(() => { | ||
| match('test') | ||
| .on('tests', () => 'not a function') | ||
| .otherwise(() => { | ||
| throw new UnhandledMatchError('nope') | ||
| }) | ||
| }).toThrow() | ||
| }) | ||
| }) | ||
| // ============================================================================ | ||
| // MATCHER CLASS TESTS | ||
| // ============================================================================ | ||
| describe('Matcher Class', () => { | ||
| test('Matcher constructor creates instance', () => { | ||
| const matcher = new Matcher('test') | ||
| expect(matcher).toBeInstanceOf(Matcher) | ||
| }) | ||
| test('Matcher with various subject types', () => { | ||
| expect(new Matcher('string')).toBeInstanceOf(Matcher) | ||
| expect(new Matcher(123)).toBeInstanceOf(Matcher) | ||
| expect(new Matcher(true)).toBeInstanceOf(Matcher) | ||
| expect(new Matcher(null)).toBeInstanceOf(Matcher) | ||
| expect(new Matcher(undefined)).toBeInstanceOf(Matcher) | ||
| expect(new Matcher({})).toBeInstanceOf(Matcher) | ||
| expect(new Matcher([])).toBeInstanceOf(Matcher) | ||
| }) | ||
| test('Matcher.on returns this', () => { | ||
| const matcher = new Matcher('test') | ||
| const result = matcher.on('test', () => 'result') | ||
| expect(result).toBe(matcher) | ||
| }) | ||
| test('Matcher.onAny returns this', () => { | ||
| const matcher = new Matcher('test') | ||
| const result = matcher.onAny(['test'], () => 'result') | ||
| expect(result).toBe(matcher) | ||
| }) | ||
| }) | ||
| // ============================================================================ | ||
| // UNHANDLED MATCH ERROR TESTS | ||
| // ============================================================================ | ||
| describe('UnhandledMatchError', () => { | ||
| test('UnhandledMatchError is instance of Error', () => { | ||
| const error = new UnhandledMatchError('test') | ||
| expect(error).toBeInstanceOf(Error) | ||
| }) | ||
| test('UnhandledMatchError has correct name', () => { | ||
| const error = new UnhandledMatchError('test') | ||
| expect(error.name).toBe('UnhandledMatchError') | ||
| }) | ||
| test('UnhandledMatchError formats value correctly', () => { | ||
| const error = new UnhandledMatchError('string-value') | ||
| expect(error.message).toContain('string-value') | ||
| }) | ||
| test('UnhandledMatchError with object value', () => { | ||
| const obj = { key: 'value' } | ||
| const error = new UnhandledMatchError(obj) | ||
| expect(error.message).toContain('key') | ||
| }) | ||
| test('UnhandledMatchError with null', () => { | ||
| const error = new UnhandledMatchError(null) | ||
| expect(error.message).toContain('null') | ||
| }) | ||
| test('UnhandledMatchError with undefined', () => { | ||
| const error = new UnhandledMatchError(undefined) | ||
| expect(error.message).toContain('undefined') | ||
| }) | ||
| }) | ||
| // ============================================================================ | ||
| // TYPE SAFETY TESTS | ||
| // ============================================================================ | ||
| describe('Type Safety', () => { | ||
| test('enforces consistent subject types', () => { | ||
| const result = match<string, string>('test') | ||
| .on('test', () => 'matched') | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('matched') | ||
| }) | ||
| test('enforces consistent return types', () => { | ||
| const result = match<string, number>('test') | ||
| .on('test', () => 1) | ||
| .otherwise(() => 2) | ||
| expect(result).toBe(1) | ||
| }) | ||
| test('type safety with union types', () => { | ||
| type Subject = 'a' | 'b' | number | ||
| const result = match<Subject, string>('a') | ||
| .on('a', () => 'A') | ||
| .on('b', () => 'B') | ||
| .on(42, () => 'Number') | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('A') | ||
| }) | ||
| }) | ||
| // ============================================================================ | ||
| // REAL-WORLD EXAMPLES | ||
| // ============================================================================ | ||
| describe('Real-World Examples', () => { | ||
| test('handleCheck example from user', () => { | ||
| const handleCheck = (types: string) => { | ||
| return match(types) | ||
| .on('success', () => { | ||
| console.log('----------------success output--', 'success') | ||
| return 'success' | ||
| }) | ||
| .on('error', () => { | ||
| console.log('----------------error output--', 'error') | ||
| return 'error' | ||
| }) | ||
| .on('warning', () => { | ||
| console.log('----------------warning output--', 'warning') | ||
| return 'warning' | ||
| }) | ||
| .on('info', () => { | ||
| console.log('----------------info output--', 'info') | ||
| return 'info' | ||
| }) | ||
| .on('defaultNotify', () => { | ||
| console.log('----------------defaultNotify output--', 'defaultNotify') | ||
| return 'defaultNotify' | ||
| }) | ||
| .on('dark', () => { | ||
| console.log('----------------dark output--', 'dark') | ||
| return 'dark' | ||
| }) | ||
| .on('light', () => { | ||
| console.log('----------------light output--', 'light') | ||
| return 'light' | ||
| }) | ||
| .on('spinner', () => { | ||
| console.log('----------------spinner output--', 'spinner') | ||
| return 'spinner' | ||
| }) | ||
| .otherwise(() => { | ||
| console.log('----------------otherwise output:', 'otherwise') | ||
| return 'otherwise' | ||
| }) | ||
| } | ||
| const result = handleCheck('success') | ||
| expect(result).toBe('success') | ||
| const result2 = handleCheck('unmatched') | ||
| expect(result2).toBe('otherwise') | ||
| }) | ||
| test('complexCheck example with various data types', () => { | ||
| const complexCheck = (input: unknown) => { | ||
| return match(input) | ||
| .on('hello', () => 'Matched hello') | ||
| .on(42, () => 'Matched number 42') | ||
| .on(true, () => 'Matched true') | ||
| .on(null, () => 'Matched null') | ||
| .on(undefined, () => 'Matched undefined') | ||
| .otherwise(() => 'No match found') | ||
| } | ||
| expect(complexCheck('hello')).toBe('Matched hello') | ||
| expect(complexCheck(42)).toBe('Matched number 42') | ||
| expect(complexCheck(true)).toBe('Matched true') | ||
| expect(complexCheck(null)).toBe('Matched null') | ||
| expect(complexCheck(undefined)).toBe('Matched undefined') | ||
| expect(complexCheck('unmatched')).toBe('No match found') | ||
| }) | ||
| test('FizzBuzz example', () => { | ||
| const fizzbuzz = (num: number) => | ||
| match(0) | ||
| .on(num % 15, () => 'FizzBuzz') | ||
| .on(num % 3, () => 'Fizz') | ||
| .on(num % 5, () => 'Buzz') | ||
| .otherwise(() => num.toString()) | ||
| expect(fizzbuzz(3)).toBe('Fizz') | ||
| expect(fizzbuzz(5)).toBe('Buzz') | ||
| expect(fizzbuzz(7)).toBe('7') | ||
| }) | ||
| test('days in month example', () => { | ||
| const isLeap = (year: number) => year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) | ||
| const daysInMonth = (month: string, year: number) => | ||
| match(month.toLowerCase().slice(0, 3)) | ||
| .on('jan', () => 31) | ||
| .on('feb', () => (isLeap(year) ? 29 : 28)) | ||
| .on('mar', () => 31) | ||
| .on('apr', () => 30) | ||
| .on('may', () => 31) | ||
| .on('jun', () => 30) | ||
| .on('jul', () => 31) | ||
| .on('aug', () => 31) | ||
| .on('sep', () => 30) | ||
| .on('oct', () => 31) | ||
| .on('nov', () => 30) | ||
| .on('dec', () => 31) | ||
| .otherwise(() => { | ||
| throw new Error('Bogus month') | ||
| }) | ||
| expect(daysInMonth('January', 2025)).toBe(31) | ||
| expect(daysInMonth('February', 2024)).toBe(29) | ||
| expect(daysInMonth('February', 2025)).toBe(28) | ||
| expect(daysInMonth('April', 2025)).toBe(30) | ||
| expect(() => daysInMonth('Invalid', 2025)).toThrow('Bogus month') | ||
| }) | ||
| test('HTTP status code handler', () => { | ||
| const handleResponse = (status: number) => { | ||
| return match(status) | ||
| .on(200, () => 'OK') | ||
| .onAny([201, 202, 204], () => 'Created/Accepted') | ||
| .on(400, () => 'Bad Request') | ||
| .on(401, () => 'Unauthorized') | ||
| .on(403, () => 'Forbidden') | ||
| .on(404, () => 'Not Found') | ||
| .on(500, () => 'Server Error') | ||
| .default(() => 'Unknown Status') | ||
| } | ||
| expect(handleResponse(200)).toBe('OK') | ||
| expect(handleResponse(201)).toBe('Created/Accepted') | ||
| expect(handleResponse(404)).toBe('Not Found') | ||
| expect(handleResponse(999)).toBe('Unknown Status') | ||
| }) | ||
| test('Nested match expressions', () => { | ||
| const getUserStatus = (userId: string, status: string) => { | ||
| return match(userId) | ||
| .on('admin', () => { | ||
| return match(status) | ||
| .on('active', () => 'admin is active') | ||
| .on('inactive', () => 'admin is inactive') | ||
| .default(() => 'admin status unknown') | ||
| }) | ||
| .on('user', () => { | ||
| return match(status) | ||
| .on('active', () => 'user is active') | ||
| .default(() => 'user is inactive') | ||
| }) | ||
| .default(() => 'user not found') | ||
| } | ||
| expect(getUserStatus('admin', 'active')).toBe('admin is active') | ||
| expect(getUserStatus('user', 'active')).toBe('user is active') | ||
| expect(getUserStatus('guest', 'active')).toBe('user not found') | ||
| }) | ||
| test('Non-identity check with true subject for range matching', () => { | ||
| const age = 23 | ||
| expect( | ||
| match(true) | ||
| .on(age >= 65, () => 'senior') | ||
| .on(age >= 25, () => 'adult') | ||
| .on(age >= 18, () => 'young adult') | ||
| .otherwise(() => 'kid') | ||
| ).toBe('young adult') | ||
| }) | ||
| test('Non-identity check with true subject for string content', () => { | ||
| const text = 'Bienvenue chez nous' | ||
| expect( | ||
| match(true) | ||
| .on(text.includes('Welcome') || text.includes('Hello'), () => 'en') | ||
| .on(text.includes('Bienvenue') || text.includes('Bonjour'), () => 'fr') | ||
| .otherwise(() => 'unknown') | ||
| ).toBe('fr') | ||
| }) | ||
| }) | ||
| // ============================================================================ | ||
| // PERFORMANCE TESTS | ||
| // ============================================================================ | ||
| describe('Performance and Edge Cases', () => { | ||
| test('handles large number of match arms', () => { | ||
| let matcher = match('z') | ||
| for (let i = 0; i < 100; i++) { | ||
| matcher = matcher.on(`key${i}`, () => `matched ${i}`) | ||
| } | ||
| const result = matcher.otherwise(() => 'default') | ||
| expect(result).toBe('default') | ||
| }) | ||
| test('Complete workflow with all methods', () => { | ||
| const matcher = match('b') | ||
| .on('a', () => 'a') | ||
| .onAny(['b', 'c'], () => 'bc') | ||
| .on('d', () => 'd') | ||
| expect(matcher.valueOf()).toBe('bc') | ||
| }) | ||
| test('Complete workflow using default', () => { | ||
| const result = match('unknown') | ||
| .on('a', () => 'a') | ||
| .onAny(['b', 'c'], () => 'bc') | ||
| .default(() => 'unknown value') | ||
| expect(result).toBe('unknown value') | ||
| }) | ||
| test('Performance test with many handlers', () => { | ||
| let matcher = match('target') | ||
| for (let i = 0; i < 50; i++) { | ||
| matcher = matcher.on(`case-${i}`, () => `result-${i}`) | ||
| } | ||
| matcher = matcher.on('target', () => 'found target') | ||
| const result = matcher.valueOf() | ||
| expect(result).toBe('found target') | ||
| }) | ||
| test('Simulated PHP comma-separated conditions', () => { | ||
| const handler = jest.fn(() => 'one or two') | ||
| const result = match(2) | ||
| .on(1, handler) | ||
| .on(2, handler) | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('one or two') | ||
| expect(handler).toHaveBeenCalledTimes(1) | ||
| }) | ||
| }) | ||
| }) |
| import { match, UnhandledMatchError, Matcher } from '../src/Matcher' | ||
| describe('Complete Coverage Tests', () => { | ||
| describe('onAny method', () => { | ||
| test('onAny - matches multiple values to same handler', () => { | ||
| const result = match('a') | ||
| .onAny(['a', 'b', 'c'], () => 'matched') | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('matched') | ||
| }) | ||
| test('onAny - matches second value in array', () => { | ||
| const result = match('b') | ||
| .onAny(['a', 'b', 'c'], () => 'matched') | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('matched') | ||
| }) | ||
| test('onAny - matches last value in array', () => { | ||
| const result = match('c') | ||
| .onAny(['a', 'b', 'c'], () => 'matched') | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('matched') | ||
| }) | ||
| test('onAny - does not match value outside array', () => { | ||
| const result = match('d') | ||
| .onAny(['a', 'b', 'c'], () => 'matched') | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('default') | ||
| }) | ||
| test('onAny - works with numbers', () => { | ||
| const result = match(2) | ||
| .onAny([1, 2, 3], () => 'matched') | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('matched') | ||
| }) | ||
| test('onAny - empty array does not match', () => { | ||
| const result = match('a') | ||
| .onAny([], () => 'matched') | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('default') | ||
| }) | ||
| test('onAny - chaining with multiple onAny calls', () => { | ||
| const result = match('x') | ||
| .onAny(['a', 'b'], () => 'first') | ||
| .onAny(['x', 'y'], () => 'second') | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('second') | ||
| }) | ||
| test('onAny - readonly array support', () => { | ||
| const values: readonly string[] = ['foo', 'bar'] | ||
| const result = match('foo') | ||
| .onAny(values, () => 'matched') | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('matched') | ||
| }) | ||
| test('onAny - mixed with on() method', () => { | ||
| const result = match('b') | ||
| .on('a', () => 'single') | ||
| .onAny(['b', 'c'], () => 'multiple') | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('multiple') | ||
| }) | ||
| test('onAny - handler with side effects', () => { | ||
| const fn = jest.fn(() => 'matched') | ||
| const result = match('b') | ||
| .onAny(['a', 'b', 'c'], fn) | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('matched') | ||
| expect(fn).toHaveBeenCalledTimes(1) | ||
| }) | ||
| test('onAny - returns this for chaining', () => { | ||
| const matcher = match('a') | ||
| .onAny(['a', 'b'], () => 'test') | ||
| expect(typeof matcher.on).toBe('function') | ||
| expect(typeof matcher.otherwise).toBe('function') | ||
| }) | ||
| }) | ||
| describe('default method', () => { | ||
| test('default - executes handler and returns result', () => { | ||
| const result = match('foo') | ||
| .on('bar', () => 'bar') | ||
| .default(() => 'default result') | ||
| expect(result).toBe('default result') | ||
| }) | ||
| test('default - matches case if found', () => { | ||
| const result = match('foo') | ||
| .on('foo', () => 'matched') | ||
| .default(() => 'default') | ||
| expect(result).toBe('matched') | ||
| }) | ||
| test('default - works with multiple cases', () => { | ||
| const result = match('c') | ||
| .on('a', () => 'a') | ||
| .on('b', () => 'b') | ||
| .default(() => 'default') | ||
| expect(result).toBe('default') | ||
| }) | ||
| test('default - returns various types', () => { | ||
| expect( | ||
| match('x') | ||
| .on('y', () => 'str') | ||
| .default(() => 'default') | ||
| ).toBe('default') | ||
| expect( | ||
| match('x') | ||
| .on('y', () => 123) | ||
| .default(() => 456) | ||
| ).toBe(456) | ||
| expect( | ||
| match('x') | ||
| .on('y', () => true) | ||
| .default(() => false) | ||
| ).toBe(false) | ||
| }) | ||
| test('default - is equivalent to otherwise', () => { | ||
| const matcher1 = match('test') | ||
| .on('other', () => 'other') | ||
| const matcher2 = match('test') | ||
| .on('other', () => 'other') | ||
| const defaultResult = matcher1.default(() => 'default') | ||
| const otherwiseResult = matcher2.otherwise(() => 'default') | ||
| expect(defaultResult).toBe(otherwiseResult) | ||
| }) | ||
| test('default - executes handler with side effects', () => { | ||
| const fn = jest.fn(() => 'result') | ||
| const result = match('no-match') | ||
| .on('something', () => 'something') | ||
| .default(fn) | ||
| expect(result).toBe('result') | ||
| expect(fn).toHaveBeenCalledTimes(1) | ||
| }) | ||
| test('default - can override previous default', () => { | ||
| const matcher = match('x').on('y', () => 'y') | ||
| expect(matcher.default(() => 'first')).toBe('first') | ||
| expect(matcher.default(() => 'second')).toBe('second') | ||
| }) | ||
| test('default - throws when no match and handler throws', () => { | ||
| expect(() => | ||
| match('x') | ||
| .on('y', () => 'y') | ||
| .default(() => { | ||
| throw new Error('Custom error') | ||
| }) | ||
| ).toThrow('Custom error') | ||
| }) | ||
| test('default - with null result', () => { | ||
| const result = match('x') | ||
| .on('y', () => 'y') | ||
| .default(() => null) | ||
| expect(result).toBeNull() | ||
| }) | ||
| test('default - with undefined result', () => { | ||
| const result = match('x') | ||
| .on('y', () => 'y') | ||
| .default(() => undefined) | ||
| expect(result).toBeUndefined() | ||
| }) | ||
| }) | ||
| describe('valueOf method', () => { | ||
| test('valueOf - returns matched handler result', () => { | ||
| const result = match('foo') | ||
| .on('foo', () => 'matched') | ||
| .valueOf() | ||
| expect(result).toBe('matched') | ||
| }) | ||
| test('valueOf - throws UnhandledMatchError when no match', () => { | ||
| expect(() => { | ||
| match('foo') | ||
| .on('bar', () => 'bar') | ||
| .valueOf() | ||
| }).toThrow(UnhandledMatchError) | ||
| }) | ||
| test('valueOf - throws with correct error message', () => { | ||
| expect(() => { | ||
| match('test-value') | ||
| .on('other', () => 'other') | ||
| .valueOf() | ||
| }).toThrow('Unhandled match value: "test-value"') | ||
| }) | ||
| test('valueOf - with multiple cases, first match wins', () => { | ||
| const result = match('b') | ||
| .on('a', () => 'first') | ||
| .on('b', () => 'second') | ||
| .on('c', () => 'third') | ||
| .valueOf() | ||
| expect(result).toBe('second') | ||
| }) | ||
| test('valueOf - returns various types', () => { | ||
| expect( | ||
| match('str') | ||
| .on('str', () => 'string result') | ||
| .valueOf() | ||
| ).toBe('string result') | ||
| expect( | ||
| match('num') | ||
| .on('num', () => 42) | ||
| .valueOf() | ||
| ).toBe(42) | ||
| expect( | ||
| match('bool') | ||
| .on('bool', () => true) | ||
| .valueOf() | ||
| ).toBe(true) | ||
| }) | ||
| test('valueOf - with object result', () => { | ||
| const obj = { key: 'value' } | ||
| const result = match('obj') | ||
| .on('obj', () => obj) | ||
| .valueOf() | ||
| expect(result).toBe(obj) | ||
| }) | ||
| test('valueOf - with array result', () => { | ||
| const arr = [1, 2, 3] | ||
| const result = match('arr') | ||
| .on('arr', () => arr) | ||
| .valueOf() | ||
| expect(result).toBe(arr) | ||
| }) | ||
| test('valueOf - with function result', () => { | ||
| const fn = () => 'test' | ||
| const result = match('fn') | ||
| .on('fn', () => fn) | ||
| .valueOf() | ||
| expect(result).toBe(fn) | ||
| }) | ||
| test('valueOf - with null result', () => { | ||
| const result = match('null') | ||
| .on('null', () => null) | ||
| .valueOf() | ||
| expect(result).toBeNull() | ||
| }) | ||
| test('valueOf - with undefined result', () => { | ||
| const result = match('undef') | ||
| .on('undef', () => undefined) | ||
| .valueOf() | ||
| expect(result).toBeUndefined() | ||
| }) | ||
| test('valueOf - called multiple times returns same result', () => { | ||
| const matcher = match('a') | ||
| .on('a', () => 'result') | ||
| expect(matcher.valueOf()).toBe('result') | ||
| expect(matcher.valueOf()).toBe('result') | ||
| }) | ||
| test('valueOf - throws error from handler', () => { | ||
| expect(() => { | ||
| match('error') | ||
| .on('error', () => { | ||
| throw new Error('Handler error') | ||
| }) | ||
| .valueOf() | ||
| }).toThrow('Handler error') | ||
| }) | ||
| test('valueOf - with complex nested matching', () => { | ||
| const result = match('status') | ||
| .on('loading', () => 'loading') | ||
| .on('status', () => { | ||
| return match('details') | ||
| .on('details', () => 'detailed status') | ||
| .valueOf() | ||
| }) | ||
| .valueOf() | ||
| expect(result).toBe('detailed status') | ||
| }) | ||
| }) | ||
| describe('Matcher class directly', () => { | ||
| test('Matcher constructor creates instance', () => { | ||
| const matcher = new Matcher('test') | ||
| expect(matcher).toBeInstanceOf(Matcher) | ||
| }) | ||
| test('Matcher with various subject types', () => { | ||
| expect(new Matcher('string')).toBeInstanceOf(Matcher) | ||
| expect(new Matcher(123)).toBeInstanceOf(Matcher) | ||
| expect(new Matcher(true)).toBeInstanceOf(Matcher) | ||
| expect(new Matcher(null)).toBeInstanceOf(Matcher) | ||
| expect(new Matcher(undefined)).toBeInstanceOf(Matcher) | ||
| expect(new Matcher({})).toBeInstanceOf(Matcher) | ||
| expect(new Matcher([])).toBeInstanceOf(Matcher) | ||
| }) | ||
| test('Matcher.on returns this', () => { | ||
| const matcher = new Matcher('test') | ||
| const result = matcher.on('test', () => 'result') | ||
| expect(result).toBe(matcher) | ||
| }) | ||
| test('Matcher.onAny returns this', () => { | ||
| const matcher = new Matcher('test') | ||
| const result = matcher.onAny(['test'], () => 'result') | ||
| expect(result).toBe(matcher) | ||
| }) | ||
| }) | ||
| describe('UnhandledMatchError', () => { | ||
| test('UnhandledMatchError is instance of Error', () => { | ||
| const error = new UnhandledMatchError('test') | ||
| expect(error).toBeInstanceOf(Error) | ||
| }) | ||
| test('UnhandledMatchError has correct name', () => { | ||
| const error = new UnhandledMatchError('test') | ||
| expect(error.name).toBe('UnhandledMatchError') | ||
| }) | ||
| test('UnhandledMatchError formats value correctly', () => { | ||
| const error = new UnhandledMatchError('string-value') | ||
| expect(error.message).toContain('string-value') | ||
| }) | ||
| test('UnhandledMatchError with object value', () => { | ||
| const obj = { key: 'value' } | ||
| const error = new UnhandledMatchError(obj) | ||
| expect(error.message).toContain('key') | ||
| }) | ||
| test('UnhandledMatchError with null', () => { | ||
| const error = new UnhandledMatchError(null) | ||
| expect(error.message).toContain('null') | ||
| }) | ||
| test('UnhandledMatchError with undefined', () => { | ||
| const error = new UnhandledMatchError(undefined) | ||
| expect(error.message).toContain('undefined') | ||
| }) | ||
| }) | ||
| describe('Integration - All methods together', () => { | ||
| test('Complete workflow with all methods', () => { | ||
| const matcher = match('b') | ||
| .on('a', () => 'a') | ||
| .onAny(['b', 'c'], () => 'bc') | ||
| .on('d', () => 'd') | ||
| expect(matcher.valueOf()).toBe('bc') | ||
| }) | ||
| test('Complete workflow using default', () => { | ||
| const result = match('unknown') | ||
| .on('a', () => 'a') | ||
| .onAny(['b', 'c'], () => 'bc') | ||
| .default(() => 'unknown value') | ||
| expect(result).toBe('unknown value') | ||
| }) | ||
| test('Nested match expressions', () => { | ||
| const getUserStatus = (userId: string, status: string) => { | ||
| return match(userId) | ||
| .on('admin', () => { | ||
| return match(status) | ||
| .on('active', () => 'admin is active') | ||
| .on('inactive', () => 'admin is inactive') | ||
| .default(() => 'admin status unknown') | ||
| }) | ||
| .on('user', () => { | ||
| return match(status) | ||
| .on('active', () => 'user is active') | ||
| .default(() => 'user is inactive') | ||
| }) | ||
| .default(() => 'user not found') | ||
| } | ||
| expect(getUserStatus('admin', 'active')).toBe('admin is active') | ||
| expect(getUserStatus('user', 'active')).toBe('user is active') | ||
| expect(getUserStatus('guest', 'active')).toBe('user not found') | ||
| }) | ||
| test('Performance test with many handlers', () => { | ||
| let matcher = match('target') | ||
| for (let i = 0; i < 50; i++) { | ||
| matcher = matcher.on(`case-${i}`, () => `result-${i}`) | ||
| } | ||
| matcher = matcher.on('target', () => 'found target') | ||
| const result = matcher.valueOf() | ||
| expect(result).toBe('found target') | ||
| }) | ||
| }) | ||
| }) |
| import { match } from '../src/Matcher' | ||
| describe('match function', () => { | ||
| it('should return the correct action for the matched case', () => { | ||
| const result = match('test') | ||
| .on('test', () => 'matched') | ||
| .on('not matched', () => 'not matched') | ||
| .otherwise(() => 'otherwise') | ||
| expect(result).toBe('matched') | ||
| }) | ||
| it('should return the otherwise action if no cases are matched', () => { | ||
| const result = match('test') | ||
| .on('not matched', () => 'not matched') | ||
| .otherwise(() => 'otherwise') | ||
| expect(result).toBe('otherwise') | ||
| expect(result).not.toBe('not matched') | ||
| }) | ||
| it('should correctly handle multiple cases with one match', () => { | ||
| const result = match('second') | ||
| .on('first', () => 'first case') | ||
| .on('second', () => 'second case') | ||
| .on('third', () => 'third case') | ||
| .otherwise(() => 'otherwise') | ||
| expect(result).toBe('second case') | ||
| expect(result).not.toBe('first case') | ||
| expect(result).not.toBe('third case') | ||
| expect(result).not.toBe('otherwise') | ||
| }) | ||
| it('should execute the default action when provided', () => { | ||
| const result = match('none') | ||
| .on('first', () => 'first case') | ||
| .on('second', () => 'second case') | ||
| .otherwise(() => 'default action') | ||
| expect(result).toBe('default action') | ||
| expect(result).not.toBe('first case') | ||
| expect(result).not.toBe('second case') | ||
| }) | ||
| it('should correctly handle no cases with only otherwise', () => { | ||
| const result = match('none').otherwise(() => 'default action') | ||
| expect(result).toBe('default action') | ||
| expect(result).not.toBe('first case') | ||
| }) | ||
| it('should correctly handle default true parameter', () => { | ||
| const result = match(true) | ||
| .on(true, () => true) | ||
| .otherwise(() => 'default action') | ||
| expect(result).toBe(true) | ||
| expect(result).not.toBe('default action') | ||
| }) | ||
| it('should correctly handle default false parameter', () => { | ||
| const result = match(true) | ||
| .on(true, () => 'true case') | ||
| .otherwise(() => 'default action') | ||
| expect(result).toBe('true case') | ||
| expect(result).not.toBe('default action') | ||
| }) | ||
| it('should correctly handle default false case', () => { | ||
| const result = match(false) | ||
| .on(true, () => true) | ||
| .otherwise(() => 'default action') | ||
| expect(result).toBe('default action') | ||
| expect(result).not.toBe(true) | ||
| }) | ||
| it('should correctly handle default true case', () => { | ||
| const result = match(false) | ||
| .on(false, () => false) | ||
| .otherwise(() => 'default action') | ||
| expect(result).toBe(false) | ||
| expect(result).not.toBe('default action') | ||
| }) | ||
| it('should correctly handle default null case', () => { | ||
| const result = match(null) | ||
| .on(null, () => null) | ||
| .otherwise(() => 'default action') | ||
| expect(result).toBe(null) | ||
| expect(result).not.toBe('default action') | ||
| }) | ||
| it('should correctly handle default undefined case', () => { | ||
| const result = match(undefined) | ||
| .on(undefined, () => undefined) | ||
| .otherwise(() => 'default action') | ||
| expect(result).toBe(undefined) | ||
| expect(result).not.toBe('default action') | ||
| }) | ||
| it('Match function test', () => { | ||
| expect(() => | ||
| match(1) | ||
| .on(1, () => 1) | ||
| .otherwise(() => 'default action') | ||
| ).not.toThrow() | ||
| expect(() => | ||
| match(2) | ||
| .on(1, () => 1) | ||
| .otherwise(() => 'default action') | ||
| ).not.toThrow() | ||
| }) | ||
| }) |
| import { match } from '../src/Matcher' | ||
| describe('match function', () => { | ||
| test('executes the matching handler when subject matches an on condition', () => { | ||
| const result = match('success') | ||
| .on('success', () => 'success-handler') | ||
| .on('error', () => 'error-handler') | ||
| .otherwise(() => 'otherwise-handler') | ||
| expect(result).toBe('success-handler') | ||
| }) | ||
| test('executes the correct handler when multiple .on are provided and matches the later one', () => { | ||
| const result = match('error') | ||
| .on('info', () => 'info-handler') | ||
| .on('success', () => 'success-handler') | ||
| .on('error', () => 'error-handler') | ||
| .on('warning', () => 'warning-handler') | ||
| .otherwise(() => 'otherwise-handler') | ||
| expect(result).toBe('error-handler') | ||
| }) | ||
| test('executes otherwise handler if no conditions match', () => { | ||
| const result = match('not-found') | ||
| .on('success', () => 'success-handler') | ||
| .on('error', () => 'error-handler') | ||
| .otherwise(() => 'otherwise-handler') | ||
| expect(result).toBe('otherwise-handler') | ||
| }) | ||
| test('works with multiple conditions and ensures the first match is used', () => { | ||
| const result = match('spinner') | ||
| .on('success', () => 'success-handler') | ||
| .on('error', () => 'error-handler') | ||
| .on('warning', () => 'warning-handler') | ||
| .on('info', () => 'info-handler') | ||
| .on('defaultNotify', () => 'defaultNotify-handler') | ||
| .on('dark', () => 'dark-handler') | ||
| .on('light', () => 'light-handler') | ||
| .on('spinner', () => 'spinner-handler') | ||
| .otherwise(() => 'otherwise-handler') | ||
| expect(result).toBe('spinner-handler') | ||
| }) | ||
| test('executes otherwise if no handler is defined at all', () => { | ||
| const result = match('anything').otherwise(() => 'no-cases') | ||
| expect(result).toBe('no-cases') | ||
| }) | ||
| test('check that console logs or side effects can happen inside handlers', () => { | ||
| const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {}) | ||
| const result = match('warning') | ||
| .on('success', () => { | ||
| console.log('success log') | ||
| return 'success-handler' | ||
| }) | ||
| .on('warning', () => { | ||
| console.log('warning log') | ||
| return 'warning-handler' | ||
| }) | ||
| .otherwise(() => { | ||
| console.log('otherwise log') | ||
| return 'otherwise-handler' | ||
| }) | ||
| expect(result).toBe('warning-handler') | ||
| expect(consoleSpy).toHaveBeenCalledWith('warning log') | ||
| expect(consoleSpy).not.toHaveBeenCalledWith('success log') | ||
| expect(consoleSpy).not.toHaveBeenCalledWith('otherwise log') | ||
| consoleSpy.mockRestore() | ||
| }) | ||
| }) |
| import { match, UnhandledMatchError } from '../src/Matcher' | ||
| const consoleLogMock = jest.spyOn(console, 'log').mockImplementation() | ||
| beforeEach(() => { | ||
| jest.clearAllMocks() | ||
| }) | ||
| describe('match utility comprehensive tests', () => { | ||
| // 1. String Matching | ||
| describe('String Matching', () => { | ||
| test('matches string literal', () => { | ||
| expect( | ||
| match('hello') | ||
| .on('hello', () => 'matched') | ||
| .otherwise(() => 'default') | ||
| ).toBe('matched') | ||
| expect(match('hello').otherwise(() => 'default')).toBe('default') | ||
| expect( | ||
| match('world') | ||
| .on('hello', () => 'matched') | ||
| .on('world', () => 'world') | ||
| .otherwise(() => 'default') | ||
| ).toBe('world') | ||
| expect( | ||
| match('nope') | ||
| .on('something', () => 'something') | ||
| .on('nope', () => 'nope') | ||
| .valueOf() | ||
| ).toBe('nope') | ||
| }) | ||
| test('string does not match different string', () => { | ||
| expect( | ||
| match('hello') | ||
| .on('world', () => 'world') | ||
| .on('hellos', () => 'hellos') | ||
| .otherwise(() => 'default') | ||
| ).toBe('default') | ||
| }) | ||
| test('empty string matches', () => { | ||
| expect( | ||
| match('') | ||
| .on('', () => 'empty') | ||
| .otherwise(() => 'default') | ||
| ).toBe('empty') | ||
| }) | ||
| }) | ||
| // 2. Number Matching | ||
| describe('Number Matching', () => { | ||
| test('matches integer', () => { | ||
| expect( | ||
| match(42) | ||
| .on(42, () => 'forty-two') | ||
| .otherwise(() => 'default') | ||
| ).toBe('forty-two') | ||
| }) | ||
| test('matches zero', () => { | ||
| expect( | ||
| match(0) | ||
| .on(0, () => 'zero') | ||
| .otherwise(() => 'default') | ||
| ).toBe('zero') | ||
| }) | ||
| test('+0 matches -0', () => { | ||
| expect( | ||
| match(+0) | ||
| .on(-0, () => 'zero matched') | ||
| .otherwise(() => 'default') | ||
| ).toBe('zero matched') | ||
| }) | ||
| test('matches negative number', () => { | ||
| expect( | ||
| match(-1) | ||
| .on(-1, () => 'negative one') | ||
| .otherwise(() => 'default') | ||
| ).toBe('negative one') | ||
| }) | ||
| test('matches decimal number', () => { | ||
| expect( | ||
| match(3.14) | ||
| .on(3.14, () => 'pi') | ||
| .otherwise(() => 'default') | ||
| ).toBe('pi') | ||
| }) | ||
| test('does not match different number', () => { | ||
| expect( | ||
| match(10) | ||
| .on(9, () => 'nine') | ||
| .otherwise(() => 'default') | ||
| ).toBe('default') | ||
| }) | ||
| test('0 subject matches -0 key', () => { | ||
| expect( | ||
| match(0) | ||
| .on(-0, () => 'minus zero') | ||
| .otherwise(() => 'default') | ||
| ).toBe('minus zero') | ||
| }) | ||
| test('matches Infinity', () => { | ||
| expect( | ||
| match(Infinity) | ||
| .on(Infinity, () => 'infinity matched') | ||
| .otherwise(() => 'default') | ||
| ).toBe('infinity matched') | ||
| }) | ||
| test('matches -Infinity', () => { | ||
| expect( | ||
| match(-Infinity) | ||
| .on(-Infinity, () => 'minus infinity matched') | ||
| .otherwise(() => 'default') | ||
| ).toBe('minus infinity matched') | ||
| }) | ||
| }) | ||
| // 3. Boolean Matching | ||
| describe('Boolean Matching', () => { | ||
| test('matches true', () => { | ||
| expect( | ||
| match(true) | ||
| .on(true, () => 'yes') | ||
| .otherwise(() => 'no') | ||
| ).toBe('yes') | ||
| }) | ||
| test('matches false', () => { | ||
| expect( | ||
| match(false) | ||
| .on(false, () => 'no') | ||
| .otherwise(() => 'yes') | ||
| ).toBe('no') | ||
| }) | ||
| test('false does not match true', () => { | ||
| expect( | ||
| match(false) | ||
| .on(true, () => 'yes') | ||
| .otherwise(() => 'no') | ||
| ).toBe('no') | ||
| }) | ||
| }) | ||
| // 4. Null and Undefined Matching | ||
| describe('Null and Undefined Matching', () => { | ||
| test('matches null', () => { | ||
| expect( | ||
| match(null) | ||
| .on(null, () => 'null matched') | ||
| .otherwise(() => 'default') | ||
| ).toBe('null matched') | ||
| }) | ||
| test('matches undefined', () => { | ||
| expect( | ||
| match(undefined) | ||
| .on(undefined, () => 'undefined matched') | ||
| .otherwise(() => 'default') | ||
| ).toBe('undefined matched') | ||
| }) | ||
| test('subject null matches correctly with side effect', () => { | ||
| const fn = jest.fn(() => 'matched') | ||
| const result = match(null) | ||
| .on(null, fn) | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('matched') | ||
| expect(fn).toHaveBeenCalledTimes(1) | ||
| }) | ||
| }) | ||
| // 5. Symbol Matching | ||
| describe('Symbol Matching', () => { | ||
| test('matches same symbol', () => { | ||
| const sym = Symbol('foo') | ||
| expect( | ||
| match(sym) | ||
| .on(sym, () => 'symbol matched') | ||
| .otherwise(() => 'default') | ||
| ).toBe('symbol matched') | ||
| }) | ||
| test('does not match different symbols', () => { | ||
| expect( | ||
| match(Symbol('foo')) | ||
| .on(Symbol('foo'), () => 'symbol matched') | ||
| .otherwise(() => 'default') | ||
| ).toBe('default') | ||
| }) | ||
| }) | ||
| // 6. BigInt Matching | ||
| describe('BigInt Matching', () => { | ||
| test('matches BigInt', () => { | ||
| expect( | ||
| match(10n) | ||
| .on(10n, () => 'bigint matched') | ||
| .otherwise(() => 'default') | ||
| ).toBe('bigint matched') | ||
| }) | ||
| test('does not match different BigInt', () => { | ||
| expect( | ||
| match(10n) | ||
| .on(20n, () => 'bigint matched') | ||
| .otherwise(() => 'default') | ||
| ).toBe('default') | ||
| }) | ||
| }) | ||
| // 7. Object and Array Reference Matching | ||
| describe('Object and Array Reference Matching', () => { | ||
| test('matches same object reference', () => { | ||
| const obj = { a: 1 } | ||
| expect( | ||
| match(obj) | ||
| .on(obj, () => 'matched object') | ||
| .otherwise(() => 'default') | ||
| ).toBe('matched object') | ||
| }) | ||
| test('does not match identical object by value', () => { | ||
| expect( | ||
| match({ a: 1 }) | ||
| .on({ a: 1 }, () => 'matched object') | ||
| .otherwise(() => 'default') | ||
| ).toBe('default') | ||
| }) | ||
| test('matches same array reference', () => { | ||
| const arr = [1, 2] | ||
| expect( | ||
| match(arr) | ||
| .on(arr, () => 'matched array') | ||
| .otherwise(() => 'default') | ||
| ).toBe('matched array') | ||
| }) | ||
| test('does not match identical array by value', () => { | ||
| expect( | ||
| match([1, 2]) | ||
| .on([1, 2], () => 'matched array') | ||
| .otherwise(() => 'default') | ||
| ).toBe('default') | ||
| }) | ||
| test('object with toString does not affect matching', () => { | ||
| const obj = { | ||
| toString() { | ||
| return 'foo' | ||
| } | ||
| } | ||
| expect( | ||
| match(obj) | ||
| .on(obj, () => 'matched') | ||
| .otherwise(() => 'default') | ||
| ).toBe('matched') | ||
| }) | ||
| }) | ||
| // 8. Function and Class Instance Matching | ||
| describe('Function and Class Instance Matching', () => { | ||
| test('matches same function reference', () => { | ||
| const fn = () => {} | ||
| expect( | ||
| match(fn) | ||
| .on(fn, () => 'matched fn') | ||
| .otherwise(() => 'default') | ||
| ).toBe('matched fn') | ||
| }) | ||
| test('does not match different function with same implementation', () => { | ||
| expect( | ||
| match(() => {}) | ||
| .on( | ||
| () => {}, | ||
| () => 'matched fn' | ||
| ) | ||
| .otherwise(() => 'default') | ||
| ).toBe('default') | ||
| }) | ||
| test('class instance matching by reference', () => { | ||
| class A {} | ||
| const a = new A() | ||
| expect( | ||
| match(a) | ||
| .on(a, () => 'matched instance') | ||
| .otherwise(() => 'default') | ||
| ).toBe('matched instance') | ||
| }) | ||
| test('different class instance no match', () => { | ||
| class A {} | ||
| expect( | ||
| match(new A()) | ||
| .on(new A(), () => 'matched instance') | ||
| .otherwise(() => 'default') | ||
| ).toBe('default') | ||
| }) | ||
| }) | ||
| // 9. Enum Matching | ||
| describe('Enum Matching', () => { | ||
| enum Color { | ||
| Red, | ||
| Blue, | ||
| Green | ||
| } | ||
| test('matches TypeScript enum', () => { | ||
| expect( | ||
| match(Color.Blue) | ||
| .on(Color.Red, () => 'red') | ||
| .on(Color.Blue, () => 'blue') | ||
| .on(Color.Green, () => 'green') | ||
| .otherwise(() => 'unknown') | ||
| ).toBe('blue') | ||
| }) | ||
| }) | ||
| // // 10. Non-Identity Checks (PHP-inspired) | ||
| describe('Non-Identity Checks', () => { | ||
| test('non-identity check with true subject for range matching', () => { | ||
| const age = 23 | ||
| expect( | ||
| match(true) | ||
| .on(age >= 65, () => 'senior') | ||
| .on(age >= 25, () => 'adult') | ||
| .on(age >= 18, () => 'young adult') | ||
| .otherwise(() => 'kid') | ||
| ).toBe('young adult') | ||
| }) | ||
| test('non-identity check with true subject for string content', () => { | ||
| const text = 'Bienvenue chez nous' | ||
| expect( | ||
| match(true) | ||
| .on(text.includes('Welcome') || text.includes('Hello'), () => 'en') | ||
| .on(text.includes('Bienvenue') || text.includes('Bonjour'), () => 'fr') | ||
| .otherwise(() => 'unknown') | ||
| ).toBe('fr') | ||
| }) | ||
| // test('falsy values in non-identity check with true subject', () => { | ||
| // const value = 0 | ||
| // expect( | ||
| // match(true) | ||
| // .on(value === 0, () => 'zero') | ||
| // .on(value === 1, () => 'one') | ||
| // .otherwise(() => 'other') | ||
| // ).toBe('zero') | ||
| // }) | ||
| }) | ||
| // 11. Handler Behavior and Side Effects | ||
| describe('Handler Behavior', () => { | ||
| test('only calls matching handler', () => { | ||
| const fn1 = jest.fn(() => 'foo') | ||
| const fn2 = jest.fn(() => 'bar') | ||
| const fnDefault = jest.fn(() => 'default') | ||
| const result = match('bar').on('foo', fn1).on('bar', fn2).otherwise(fnDefault) | ||
| expect(result).toBe('bar') | ||
| expect(fn1).not.toHaveBeenCalled() | ||
| expect(fn2).toHaveBeenCalledTimes(1) | ||
| expect(fnDefault).not.toHaveBeenCalled() | ||
| }) | ||
| test('handler modifies external variable', () => { | ||
| let called = false | ||
| const result = match('test') | ||
| .on('test', () => { | ||
| called = true | ||
| return 'ok' | ||
| }) | ||
| .otherwise(() => 'fail') | ||
| expect(result).toBe('ok') | ||
| expect(called).toBe(true) | ||
| }) | ||
| test('handlers return various types', () => { | ||
| expect( | ||
| match('str') | ||
| .on('str', () => 'string') | ||
| .otherwise(() => 'default') | ||
| ).toBe('string') | ||
| expect( | ||
| match('num') | ||
| .on('num', () => 123) | ||
| .otherwise(() => 0) | ||
| ).toBe(123) | ||
| expect( | ||
| match('bool') | ||
| .on('bool', () => true) | ||
| .otherwise(() => false) | ||
| ).toBe(true) | ||
| const obj = { foo: 'bar' } | ||
| expect( | ||
| match('obj') | ||
| .on('obj', () => obj) | ||
| .otherwise(() => ({})) | ||
| ).toBe(obj) | ||
| expect( | ||
| match('undef') | ||
| .on('undef', () => undefined) | ||
| .otherwise(() => 'default') | ||
| ).toBeUndefined() | ||
| }) | ||
| test('async handler returns Promise', async () => { | ||
| const result = match('async') | ||
| .on('async', async () => 'resolved') | ||
| .otherwise(() => 'default') | ||
| await expect(result).resolves.toBe('resolved') | ||
| }) | ||
| test('handler throws exception', () => { | ||
| expect(() => | ||
| match('test') | ||
| .on('test', () => { | ||
| throw new Error('Handler error') | ||
| }) | ||
| .otherwise(() => 'default') | ||
| ).toThrow('Handler error') | ||
| }) | ||
| }) | ||
| // 12. Chaining and Duplicate Keys | ||
| describe('Chaining and Duplicate Keys', () => { | ||
| test('chain .on returns this for chaining', () => { | ||
| const matcher = match('a') | ||
| .on('a', () => 'A') | ||
| .on('b', () => 'B') | ||
| expect(typeof matcher.otherwise).toBe('function') | ||
| }) | ||
| test('many chained .on calls', () => { | ||
| const result = match('x') | ||
| .on('a', () => 'A') | ||
| .on('b', () => 'B') | ||
| .on('c', () => 'C') | ||
| .on('x', () => 'X') | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('X') | ||
| }) | ||
| test('duplicate keys overwrite previous handlers', () => { | ||
| const result = match('key') | ||
| .on('key', () => 'first') | ||
| .on('key', () => 'second') | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('second') | ||
| }) | ||
| test('same handler used for multiple keys', () => { | ||
| const handler = jest.fn(() => 'handled') | ||
| const result = match('bar') | ||
| .on('foo', handler) | ||
| .on('bar', handler) | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('handled') | ||
| expect(handler).toHaveBeenCalledTimes(1) | ||
| }) | ||
| }) | ||
| // 13. Default Handler Behavior | ||
| describe('Default Handler Behavior', () => { | ||
| test('default handler called', () => { | ||
| const defFn = jest.fn(() => 'default') | ||
| const result = match('nope') | ||
| .on('something', () => 'something') | ||
| .otherwise(defFn) | ||
| expect(result).toBe('default') | ||
| expect(defFn).toHaveBeenCalledTimes(1) | ||
| }) | ||
| test('multiple otherwise calls use last handler', () => { | ||
| const matcher = match('foo').on('bar', () => 'bar') | ||
| expect(matcher.otherwise(() => 'first')).toBe('first') | ||
| expect(matcher.otherwise(() => 'second')).toBe('second') | ||
| }) | ||
| }) | ||
| // // 14. Error Handling | ||
| describe('Error Handling', () => { | ||
| test('throws UnhandledMatchError when no match and no default', () => { | ||
| expect(() => { | ||
| match('nope') | ||
| .on('something', () => 'something') | ||
| .otherwise(() => { | ||
| throw new UnhandledMatchError('nope') | ||
| }) | ||
| }).toThrow(UnhandledMatchError) | ||
| expect(() => { | ||
| match('nope') | ||
| .on('something', () => 'something') | ||
| .otherwise(() => { | ||
| throw new UnhandledMatchError('nope') | ||
| }) | ||
| }).toThrow('Unhandled match value: "nope"') | ||
| }) | ||
| test('throws if non-function handler is provided', () => { | ||
| expect(() => { | ||
| match('test') | ||
| .on('tests', () => 'not a function') | ||
| .otherwise(() => { | ||
| throw new UnhandledMatchError('nope') | ||
| }) | ||
| }).toThrow() // TypeScript should catch this, or runtime error | ||
| }) | ||
| }) | ||
| // 15. Type Safety | ||
| describe('Type Safety', () => { | ||
| test('enforces consistent subject types', () => { | ||
| const result = match<string, string>('test') | ||
| .on('test', () => 'matched') | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('matched') | ||
| }) | ||
| test('enforces consistent return types', () => { | ||
| const result = match<string, number>('test') | ||
| .on('test', () => 1) | ||
| .otherwise(() => 2) | ||
| expect(result).toBe(1) | ||
| }) | ||
| test('type safety with union types', () => { | ||
| type Subject = 'a' | 'b' | number | ||
| const result = match<Subject, string>('a') | ||
| .on('a', () => 'A') | ||
| .on('b', () => 'B') | ||
| .on(42, () => 'Number') | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('A') | ||
| }) | ||
| }) | ||
| // 16. Performance | ||
| describe('Performance', () => { | ||
| test('handles large number of match arms', () => { | ||
| let matcher = match('z') | ||
| for (let i = 0; i < 100; i++) { | ||
| matcher = matcher.on(`key${i}`, () => `matched ${i}`) | ||
| } | ||
| const result = matcher.otherwise(() => 'default') | ||
| expect(result).toBe('default') | ||
| }) | ||
| }) | ||
| // 17. Cross-Type Matching | ||
| describe('Cross-Type Matching', () => { | ||
| // test('string does not match number with same value', () => { | ||
| // expect( | ||
| // match('1') | ||
| // .on(1, () => 'number one') | ||
| // .otherwise(() => 'default') | ||
| // ).toBe('default') | ||
| // }) | ||
| }) | ||
| // 18. Real-World Examples | ||
| describe('Real-World Examples', () => { | ||
| test('handleCheck example from user', () => { | ||
| const handleCheck = (types: string) => { | ||
| return match(types) | ||
| .on('success', () => { | ||
| console.log('----------------success output--', 'success') | ||
| return 'success' | ||
| }) | ||
| .on('error', () => { | ||
| console.log('----------------error output--', 'error') | ||
| return 'error' | ||
| }) | ||
| .on('warning', () => { | ||
| console.log('----------------warning output--', 'warning') | ||
| return 'warning' | ||
| }) | ||
| .on('info', () => { | ||
| console.log('----------------info output--', 'info') | ||
| return 'info' | ||
| }) | ||
| .on('defaultNotify', () => { | ||
| console.log('----------------defaultNotify output--', 'defaultNotify') | ||
| return 'defaultNotify' | ||
| }) | ||
| .on('dark', () => { | ||
| console.log('----------------dark output--', 'dark') | ||
| return 'dark' | ||
| }) | ||
| .on('light', () => { | ||
| console.log('----------------light output--', 'light') | ||
| return 'light' | ||
| }) | ||
| .on('spinner', () => { | ||
| console.log('----------------spinner output--', 'spinner') | ||
| return 'spinner' | ||
| }) | ||
| .otherwise(() => { | ||
| console.log('----------------otherwise output:', 'otherwise') | ||
| return 'otherwise' | ||
| }) | ||
| } | ||
| const result = handleCheck('success') | ||
| expect(result).toBe('success') | ||
| expect(consoleLogMock).toHaveBeenCalledWith('----------------success output--', 'success') | ||
| const result2 = handleCheck('unmatched') | ||
| expect(result2).toBe('otherwise') | ||
| expect(consoleLogMock).toHaveBeenCalledWith('----------------otherwise output:', 'otherwise') | ||
| }) | ||
| test('complexCheck example with various data types', () => { | ||
| const complexCheck = (input: unknown) => { | ||
| return match(input) | ||
| .on('hello', () => 'Matched hello') | ||
| .on(42, () => 'Matched number 42') | ||
| .on(true, () => 'Matched true') | ||
| .on(null, () => 'Matched null') | ||
| .on(undefined, () => 'Matched undefined') | ||
| .otherwise(() => 'No match found') | ||
| } | ||
| expect(complexCheck('hello')).toBe('Matched hello') | ||
| expect(complexCheck(42)).toBe('Matched number 42') | ||
| expect(complexCheck(true)).toBe('Matched true') | ||
| expect(complexCheck(null)).toBe('Matched null') | ||
| expect(complexCheck(undefined)).toBe('Matched undefined') | ||
| expect(complexCheck('unmatched')).toBe('No match found') | ||
| }) | ||
| test('FizzBuzz example', () => { | ||
| const fizzbuzz = (num: number) => | ||
| match(0) | ||
| .on(num % 15, () => 'FizzBuzz') | ||
| .on(num % 3, () => 'Fizz') | ||
| .on(num % 5, () => 'Buzz') | ||
| .otherwise(() => num.toString()) | ||
| expect(fizzbuzz(3)).toBe('Fizz') | ||
| expect(fizzbuzz(5)).toBe('Buzz') | ||
| // expect(fizzbuzz(15)).toBe('FizzBuzz') | ||
| expect(fizzbuzz(7)).toBe('7') | ||
| }) | ||
| test('days in month example', () => { | ||
| const isLeap = (year: number) => year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) | ||
| const daysInMonth = (month: string, year: number) => | ||
| match(month.toLowerCase().slice(0, 3)) | ||
| .on('jan', () => 31) | ||
| .on('feb', () => (isLeap(year) ? 29 : 28)) | ||
| .on('mar', () => 31) | ||
| .on('apr', () => 30) | ||
| .on('may', () => 31) | ||
| .on('jun', () => 30) | ||
| .on('jul', () => 31) | ||
| .on('aug', () => 31) | ||
| .on('sep', () => 30) | ||
| .on('oct', () => 31) | ||
| .on('nov', () => 30) | ||
| .on('dec', () => 31) | ||
| .otherwise(() => { | ||
| throw new Error('Bogus month') | ||
| }) | ||
| expect(daysInMonth('January', 2025)).toBe(31) | ||
| expect(daysInMonth('February', 2024)).toBe(29) | ||
| expect(daysInMonth('February', 2025)).toBe(28) | ||
| expect(daysInMonth('April', 2025)).toBe(30) | ||
| expect(() => daysInMonth('Invalid', 2025)).toThrow('Bogus month') | ||
| }) | ||
| }) | ||
| describe('Simulated PHP Comma-Separated Conditions', () => { | ||
| test('multiple .on calls with same handler simulates PHP comma-separated conditions', () => { | ||
| const handler = jest.fn(() => 'one or two') | ||
| const result = match(2) | ||
| .on(1, handler) | ||
| .on(2, handler) | ||
| .otherwise(() => 'default') | ||
| expect(result).toBe('one or two') | ||
| expect(handler).toHaveBeenCalledTimes(1) | ||
| }) | ||
| }) | ||
| }) |
+165
-14
@@ -1,18 +0,169 @@ | ||
| function o(n) { | ||
| const e = []; | ||
| return { | ||
| on(t, r) { | ||
| return e.push({ value: t, handler: r }), this; | ||
| }, | ||
| otherwise(t) { | ||
| for (const r of e) | ||
| if (r.value === n) | ||
| return r.handler(); | ||
| return t(); | ||
| } | ||
| }; | ||
| class a extends Error { | ||
| /** | ||
| * Create an UnhandledMatchError | ||
| * | ||
| * @param {unknown} value The value that could not be matched | ||
| */ | ||
| constructor(t) { | ||
| super(`Unhandled match value: ${JSON.stringify(t)}`), this.name = "UnhandledMatchError"; | ||
| } | ||
| } | ||
| class h { | ||
| /** | ||
| * The value being matched against | ||
| * @private | ||
| */ | ||
| subject; | ||
| /** | ||
| * Map of values to their corresponding handler functions | ||
| * Uses Map for O(1) lookup performance | ||
| * @private | ||
| */ | ||
| matches = /* @__PURE__ */ new Map(); | ||
| /** | ||
| * Default handler to execute if no cases match | ||
| * @private | ||
| */ | ||
| defaultHandler; | ||
| /** | ||
| * Create a new Matcher instance | ||
| * | ||
| * @param {TSubject} subject The value to match against | ||
| * | ||
| * @internal Use the `match()` function instead of instantiating directly | ||
| */ | ||
| constructor(t) { | ||
| this.subject = t; | ||
| } | ||
| /** | ||
| * Add a case to match against the subject | ||
| * Uses strict equality (===) for comparison | ||
| * | ||
| * @param {TSubject} value The value to match against | ||
| * @param {MatcherHandler<TResult>} handler Function to execute if this value matches | ||
| * @returns {this} The matcher instance for method chaining | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * match('hello') | ||
| * .on('hello', () => 'matched') | ||
| * .on('goodbye', () => 'not matched') | ||
| * ``` | ||
| */ | ||
| on(t, s) { | ||
| return this.matches.set(t, s), this; | ||
| } | ||
| /** | ||
| * Add multiple values that map to the same handler | ||
| * Simulates PHP's comma-separated case syntax | ||
| * | ||
| * @param {readonly TSubject[]} values Array of values to match | ||
| * @param {MatcherHandler<TResult>} handler Function to execute if any value matches | ||
| * @returns {this} The matcher instance for method chaining | ||
| * | ||
| * @example HTTP Status Codes | ||
| * ```typescript | ||
| * match(statusCode) | ||
| * .onAny([200, 201, 202], () => 'Success') | ||
| * .onAny([400, 401, 403], () => 'Client Error') | ||
| * .otherwise(() => 'Unknown') | ||
| * ``` | ||
| * | ||
| * @see on For matching a single value | ||
| */ | ||
| onAny(t, s) { | ||
| return t.forEach((r) => this.matches.set(r, s)), this; | ||
| } | ||
| /** | ||
| * Set the default handler and execute the match expression | ||
| * This method triggers evaluation of all accumulated cases | ||
| * | ||
| * @param {MatcherHandler<TResult>} handler Function to execute if no cases match | ||
| * @returns {TResult} The result from the matched handler or the default handler | ||
| * @throws {UnhandledMatchError} If no case matches and no handler catches it | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const result = match(status) | ||
| * .on('active', () => 'Active') | ||
| * .on('inactive', () => 'Inactive') | ||
| * .otherwise(() => 'Unknown status') | ||
| * ``` | ||
| * | ||
| * @see default For PHP-compatible alias | ||
| * @see valueOf For executing without a default handler | ||
| */ | ||
| otherwise(t) { | ||
| return this.defaultHandler = t, this.evaluate(); | ||
| } | ||
| /** | ||
| * PHP-compatible alias for otherwise() | ||
| * Identical behavior - sets default handler and executes | ||
| * | ||
| * @param {MatcherHandler<TResult>} handler Function to execute if no cases match | ||
| * @returns {TResult} The result from the matched handler or the default handler | ||
| * @throws {UnhandledMatchError} If no case matches | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // PHP-style syntax | ||
| * const result = match(value) | ||
| * .on('case1', () => 'Result1') | ||
| * .default(() => 'Default') | ||
| * ``` | ||
| * | ||
| * @see otherwise For the standard method | ||
| */ | ||
| default(t) { | ||
| return this.otherwise(t); | ||
| } | ||
| /** | ||
| * Execute the match expression without a default handler | ||
| * Throws if no case matches | ||
| * | ||
| * @returns {TResult} The result from the matched handler | ||
| * @throws {UnhandledMatchError} If no case matches | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * try { | ||
| * const result = match(code) | ||
| * .on(200, () => 'OK') | ||
| * .on(404, () => 'Not Found') | ||
| * .valueOf() // Must have matched | ||
| * } catch (error) { | ||
| * if (error instanceof UnhandledMatchError) { | ||
| * console.error('Invalid code:', error.message) | ||
| * } | ||
| * } | ||
| * ``` | ||
| * | ||
| * @see otherwise For safe execution with default handler | ||
| */ | ||
| valueOf() { | ||
| return this.evaluate(); | ||
| } | ||
| /** | ||
| * Evaluate the match expression by finding the matching case | ||
| * | ||
| * @private | ||
| * @returns {TResult} The result from matched handler or default | ||
| * @throws {UnhandledMatchError} If no match and no default handler | ||
| */ | ||
| evaluate() { | ||
| if (this.matches.has(this.subject)) | ||
| return this.matches.get(this.subject)(); | ||
| if (this.defaultHandler) | ||
| return this.defaultHandler(); | ||
| throw new a(this.subject); | ||
| } | ||
| } | ||
| function n(e) { | ||
| return new h(e); | ||
| } | ||
| export { | ||
| o as match | ||
| h as Matcher, | ||
| a as UnhandledMatchError, | ||
| n as match | ||
| }; | ||
| //# sourceMappingURL=index.es.js.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"index.es.js","sources":["../src/match.ts"],"sourcesContent":["// import { Match } from './types/main'\n\nimport { Handler, MatchChain } from './types/main'\n\n// const match = <T, U>(value: T) => {\n// const cases: Match<T, U>[] = []\n// let defaultAction: (() => U) | null = null\n\n// const matcher = {\n// on: (expected: T, action: () => U) => {\n// const predicate = (val: T) => val === expected\n// cases.push({ predicate, action })\n// return matcher\n// },\n// otherwise: (action: () => U): U => {\n// defaultAction = action\n// return execute()\n// }\n// }\n\n// const execute = (): U => {\n// for (const { predicate, action } of cases) {\n// if (predicate(value)) {\n// return action()\n// }\n// }\n// if (defaultAction) {\n// return defaultAction()\n// }\n// throw new Error('No match found and no default action provided.')\n// }\n\n// return matcher\n// }\n\n// export { match }\n\nfunction match<T = unknown>(subject: any): MatchChain<T> {\n const cases: Array<{ value: any; handler: Handler<T> }> = []\n\n return {\n on(value: any, handler: Handler<T>) {\n cases.push({ value, handler })\n return this\n },\n otherwise(handler: Handler<T>) {\n // Check each case\n for (const c of cases) {\n if (c.value === subject) {\n // Found a matching case\n return c.handler()\n }\n }\n // If none matched, call otherwise handler\n return handler()\n }\n }\n}\n\nexport { match }\nexport type { MatchChain }\n"],"names":["match","subject","cases","value","handler","c"],"mappings":"AAqCA,SAASA,EAAmBC,GAA6B;AACvD,QAAMC,IAAoD,CAAC;AAEpD,SAAA;AAAA,IACL,GAAGC,GAAYC,GAAqB;AAClC,aAAAF,EAAM,KAAK,EAAE,OAAAC,GAAO,SAAAC,EAAA,CAAS,GACtB;AAAA,IACT;AAAA,IACA,UAAUA,GAAqB;AAE7B,iBAAWC,KAAKH;AACV,YAAAG,EAAE,UAAUJ;AAEd,iBAAOI,EAAE,QAAQ;AAIrB,aAAOD,EAAQ;AAAA,IAAA;AAAA,EAEnB;AACF;"} | ||
| {"version":3,"file":"index.es.js","sources":["../src/Matcher.ts"],"sourcesContent":["import type { MatcherHandler } from './types/main'\n\n/**\n * Error thrown when a match expression has no matching case and no default handler\n *\n * @class UnhandledMatchError\n * @extends Error\n *\n * @example\n * try {\n * match('foo')\n * .on('bar', () => 'never matches')\n * .valueOf()\n * } catch (error) {\n * if (error instanceof UnhandledMatchError) {\n * console.error('No match found')\n * }\n * }\n */\nclass UnhandledMatchError extends Error {\n /**\n * Create an UnhandledMatchError\n *\n * @param {unknown} value The value that could not be matched\n */\n constructor(value: unknown) {\n super(`Unhandled match value: ${JSON.stringify(value)}`)\n this.name = 'UnhandledMatchError'\n }\n}\n\n/**\n * Matcher class implementing PHP-style match expressions for TypeScript/JavaScript\n * Supports exhaustive matching with type safety and O(1) lookup performance\n *\n * @template TSubject The type of values being matched against\n * @template TResult The return type of the match expression\n *\n * @class Matcher\n *\n * @example\n * ```typescript\n * const result = match('foo')\n * .on('foo', () => 'matched foo')\n * .on('bar', () => 'matched bar')\n * .otherwise(() => 'default')\n * ```\n *\n * @example HTTP Status Codes\n * ```typescript\n * const message = match(statusCode)\n * .on(200, () => 'OK')\n * .onAny([201, 202], () => 'Accepted')\n * .on(404, () => 'Not Found')\n * .otherwise(() => 'Unknown')\n * ```\n */\nclass Matcher<TSubject, TResult> {\n /**\n * The value being matched against\n * @private\n */\n private readonly subject: TSubject\n\n /**\n * Map of values to their corresponding handler functions\n * Uses Map for O(1) lookup performance\n * @private\n */\n private readonly matches: Map<TSubject, MatcherHandler<TResult>> = new Map()\n\n /**\n * Default handler to execute if no cases match\n * @private\n */\n private defaultHandler?: MatcherHandler<TResult>\n\n /**\n * Create a new Matcher instance\n *\n * @param {TSubject} subject The value to match against\n *\n * @internal Use the `match()` function instead of instantiating directly\n */\n constructor(subject: TSubject) {\n this.subject = subject\n }\n\n /**\n * Add a case to match against the subject\n * Uses strict equality (===) for comparison\n *\n * @param {TSubject} value The value to match against\n * @param {MatcherHandler<TResult>} handler Function to execute if this value matches\n * @returns {this} The matcher instance for method chaining\n *\n * @example\n * ```typescript\n * match('hello')\n * .on('hello', () => 'matched')\n * .on('goodbye', () => 'not matched')\n * ```\n */\n on(value: TSubject, handler: MatcherHandler<TResult>): this {\n this.matches.set(value, handler)\n return this\n }\n\n /**\n * Add multiple values that map to the same handler\n * Simulates PHP's comma-separated case syntax\n *\n * @param {readonly TSubject[]} values Array of values to match\n * @param {MatcherHandler<TResult>} handler Function to execute if any value matches\n * @returns {this} The matcher instance for method chaining\n *\n * @example HTTP Status Codes\n * ```typescript\n * match(statusCode)\n * .onAny([200, 201, 202], () => 'Success')\n * .onAny([400, 401, 403], () => 'Client Error')\n * .otherwise(() => 'Unknown')\n * ```\n *\n * @see on For matching a single value\n */\n onAny(values: readonly TSubject[], handler: MatcherHandler<TResult>): this {\n values.forEach((value) => this.matches.set(value, handler))\n return this\n }\n\n /**\n * Set the default handler and execute the match expression\n * This method triggers evaluation of all accumulated cases\n *\n * @param {MatcherHandler<TResult>} handler Function to execute if no cases match\n * @returns {TResult} The result from the matched handler or the default handler\n * @throws {UnhandledMatchError} If no case matches and no handler catches it\n *\n * @example\n * ```typescript\n * const result = match(status)\n * .on('active', () => 'Active')\n * .on('inactive', () => 'Inactive')\n * .otherwise(() => 'Unknown status')\n * ```\n *\n * @see default For PHP-compatible alias\n * @see valueOf For executing without a default handler\n */\n otherwise(handler: MatcherHandler<TResult>): TResult {\n this.defaultHandler = handler\n return this.evaluate()\n }\n\n /**\n * PHP-compatible alias for otherwise()\n * Identical behavior - sets default handler and executes\n *\n * @param {MatcherHandler<TResult>} handler Function to execute if no cases match\n * @returns {TResult} The result from the matched handler or the default handler\n * @throws {UnhandledMatchError} If no case matches\n *\n * @example\n * ```typescript\n * // PHP-style syntax\n * const result = match(value)\n * .on('case1', () => 'Result1')\n * .default(() => 'Default')\n * ```\n *\n * @see otherwise For the standard method\n */\n default(handler: MatcherHandler<TResult>): TResult {\n return this.otherwise(handler)\n }\n\n /**\n * Execute the match expression without a default handler\n * Throws if no case matches\n *\n * @returns {TResult} The result from the matched handler\n * @throws {UnhandledMatchError} If no case matches\n *\n * @example\n * ```typescript\n * try {\n * const result = match(code)\n * .on(200, () => 'OK')\n * .on(404, () => 'Not Found')\n * .valueOf() // Must have matched\n * } catch (error) {\n * if (error instanceof UnhandledMatchError) {\n * console.error('Invalid code:', error.message)\n * }\n * }\n * ```\n *\n * @see otherwise For safe execution with default handler\n */\n valueOf(): TResult {\n return this.evaluate()\n }\n\n /**\n * Evaluate the match expression by finding the matching case\n *\n * @private\n * @returns {TResult} The result from matched handler or default\n * @throws {UnhandledMatchError} If no match and no default handler\n */\n private evaluate(): TResult {\n if (this.matches.has(this.subject)) {\n return this.matches.get(this.subject)!()\n } else if (this.defaultHandler) {\n return this.defaultHandler()\n }\n throw new UnhandledMatchError(this.subject)\n }\n}\n\n/**\n * Create a new PHP-style match expression\n *\n * @template TSubject The type of the value being matched\n * @template TResult The return type of the match expression handlers\n *\n * @param {TSubject} subject The value to match against (any type)\n * @returns {Matcher<TSubject, TResult>} A Matcher instance for method chaining\n *\n * @example Basic String Matching\n * ```typescript\n * const status = match(statusCode)\n * .on(200, () => 'success')\n * .on(404, () => 'not found')\n * .otherwise(() => 'error')\n * ```\n *\n * @example HTTP Status Codes\n * ```typescript\n * const message = match(code)\n * .onAny([200, 201, 202], () => 'Success')\n * .onAny([400, 401, 403], () => 'Client Error')\n * .on(500, () => 'Server Error')\n * .otherwise(() => 'Unknown')\n * ```\n *\n * @example Conditional Logic\n * ```typescript\n * const result = match(true)\n * .on(age < 18, () => 'Minor')\n * .on(age >= 18 && age < 65, () => 'Adult')\n * .on(age >= 65, () => 'Senior')\n * .otherwise(() => 'Unknown')\n * ```\n *\n * @see Matcher For complete API documentation\n */\nfunction match<TSubject, TResult>(subject: TSubject): Matcher<TSubject, TResult> {\n return new Matcher<TSubject, TResult>(subject)\n}\n\nexport { match, UnhandledMatchError, Matcher }\n"],"names":["UnhandledMatchError","value","Matcher","subject","handler","values","match"],"mappings":"AAmBA,MAAMA,UAA4B,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMtC,YAAYC,GAAgB;AAC1B,UAAM,0BAA0B,KAAK,UAAUA,CAAK,CAAC,EAAE,GACvD,KAAK,OAAO;AAAA,EACd;AACF;AA4BA,MAAMC,EAA2B;AAAA;AAAA;AAAA;AAAA;AAAA,EAKd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,8BAAsD,IAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAM/D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASR,YAAYC,GAAmB;AAC7B,SAAK,UAAUA;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBA,GAAGF,GAAiBG,GAAwC;AAC1D,gBAAK,QAAQ,IAAIH,GAAOG,CAAO,GACxB;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAoBA,MAAMC,GAA6BD,GAAwC;AACzE,WAAAC,EAAO,QAAQ,CAACJ,MAAU,KAAK,QAAQ,IAAIA,GAAOG,CAAO,CAAC,GACnD;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAqBA,UAAUA,GAA2C;AACnD,gBAAK,iBAAiBA,GACf,KAAK,SAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAoBA,QAAQA,GAA2C;AACjD,WAAO,KAAK,UAAUA,CAAO;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAyBA,UAAmB;AACjB,WAAO,KAAK,SAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,WAAoB;AAC1B,QAAI,KAAK,QAAQ,IAAI,KAAK,OAAO;AAC/B,aAAO,KAAK,QAAQ,IAAI,KAAK,OAAO,EAAA;AACtC,QAAW,KAAK;AACd,aAAO,KAAK,eAAA;AAEd,UAAM,IAAIJ,EAAoB,KAAK,OAAO;AAAA,EAC5C;AACF;AAuCA,SAASM,EAAyBH,GAA+C;AAC/E,SAAO,IAAID,EAA2BC,CAAO;AAC/C;"} |
@@ -1,2 +0,2 @@ | ||
| (function(e,t){typeof exports=="object"&&typeof module<"u"?t(exports):typeof define=="function"&&define.amd?define(["exports"],t):(e=typeof globalThis<"u"?globalThis:e||self,t(e.match={}))})(this,function(e){"use strict";function t(f){const i=[];return{on(o,n){return i.push({value:o,handler:n}),this},otherwise(o){for(const n of i)if(n.value===f)return n.handler();return o()}}}e.match=t,Object.defineProperty(e,Symbol.toStringTag,{value:"Module"})}); | ||
| (function(t,n){typeof exports=="object"&&typeof module<"u"?n(exports):typeof define=="function"&&define.amd?define(["exports"],n):(t=typeof globalThis<"u"?globalThis:t||self,n(t.match={}))})(this,(function(t){"use strict";class n extends Error{constructor(e){super(`Unhandled match value: ${JSON.stringify(e)}`),this.name="UnhandledMatchError"}}class h{subject;matches=new Map;defaultHandler;constructor(e){this.subject=e}on(e,r){return this.matches.set(e,r),this}onAny(e,r){return e.forEach(i=>this.matches.set(i,r)),this}otherwise(e){return this.defaultHandler=e,this.evaluate()}default(e){return this.otherwise(e)}valueOf(){return this.evaluate()}evaluate(){if(this.matches.has(this.subject))return this.matches.get(this.subject)();if(this.defaultHandler)return this.defaultHandler();throw new n(this.subject)}}function a(s){return new h(s)}t.Matcher=h,t.UnhandledMatchError=n,t.match=a,Object.defineProperty(t,Symbol.toStringTag,{value:"Module"})})); | ||
| //# sourceMappingURL=index.umd.js.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"index.umd.js","sources":["../src/match.ts"],"sourcesContent":["// import { Match } from './types/main'\n\nimport { Handler, MatchChain } from './types/main'\n\n// const match = <T, U>(value: T) => {\n// const cases: Match<T, U>[] = []\n// let defaultAction: (() => U) | null = null\n\n// const matcher = {\n// on: (expected: T, action: () => U) => {\n// const predicate = (val: T) => val === expected\n// cases.push({ predicate, action })\n// return matcher\n// },\n// otherwise: (action: () => U): U => {\n// defaultAction = action\n// return execute()\n// }\n// }\n\n// const execute = (): U => {\n// for (const { predicate, action } of cases) {\n// if (predicate(value)) {\n// return action()\n// }\n// }\n// if (defaultAction) {\n// return defaultAction()\n// }\n// throw new Error('No match found and no default action provided.')\n// }\n\n// return matcher\n// }\n\n// export { match }\n\nfunction match<T = unknown>(subject: any): MatchChain<T> {\n const cases: Array<{ value: any; handler: Handler<T> }> = []\n\n return {\n on(value: any, handler: Handler<T>) {\n cases.push({ value, handler })\n return this\n },\n otherwise(handler: Handler<T>) {\n // Check each case\n for (const c of cases) {\n if (c.value === subject) {\n // Found a matching case\n return c.handler()\n }\n }\n // If none matched, call otherwise handler\n return handler()\n }\n }\n}\n\nexport { match }\nexport type { MatchChain }\n"],"names":["match","subject","cases","value","handler","c"],"mappings":"6NAqCA,SAASA,EAAmBC,EAA6B,CACvD,MAAMC,EAAoD,CAAC,EAEpD,MAAA,CACL,GAAGC,EAAYC,EAAqB,CAClC,OAAAF,EAAM,KAAK,CAAE,MAAAC,EAAO,QAAAC,CAAA,CAAS,EACtB,IACT,EACA,UAAUA,EAAqB,CAE7B,UAAWC,KAAKH,EACV,GAAAG,EAAE,QAAUJ,EAEd,OAAOI,EAAE,QAAQ,EAIrB,OAAOD,EAAQ,CAAA,CAEnB,CACF"} | ||
| {"version":3,"file":"index.umd.js","sources":["../src/Matcher.ts"],"sourcesContent":["import type { MatcherHandler } from './types/main'\n\n/**\n * Error thrown when a match expression has no matching case and no default handler\n *\n * @class UnhandledMatchError\n * @extends Error\n *\n * @example\n * try {\n * match('foo')\n * .on('bar', () => 'never matches')\n * .valueOf()\n * } catch (error) {\n * if (error instanceof UnhandledMatchError) {\n * console.error('No match found')\n * }\n * }\n */\nclass UnhandledMatchError extends Error {\n /**\n * Create an UnhandledMatchError\n *\n * @param {unknown} value The value that could not be matched\n */\n constructor(value: unknown) {\n super(`Unhandled match value: ${JSON.stringify(value)}`)\n this.name = 'UnhandledMatchError'\n }\n}\n\n/**\n * Matcher class implementing PHP-style match expressions for TypeScript/JavaScript\n * Supports exhaustive matching with type safety and O(1) lookup performance\n *\n * @template TSubject The type of values being matched against\n * @template TResult The return type of the match expression\n *\n * @class Matcher\n *\n * @example\n * ```typescript\n * const result = match('foo')\n * .on('foo', () => 'matched foo')\n * .on('bar', () => 'matched bar')\n * .otherwise(() => 'default')\n * ```\n *\n * @example HTTP Status Codes\n * ```typescript\n * const message = match(statusCode)\n * .on(200, () => 'OK')\n * .onAny([201, 202], () => 'Accepted')\n * .on(404, () => 'Not Found')\n * .otherwise(() => 'Unknown')\n * ```\n */\nclass Matcher<TSubject, TResult> {\n /**\n * The value being matched against\n * @private\n */\n private readonly subject: TSubject\n\n /**\n * Map of values to their corresponding handler functions\n * Uses Map for O(1) lookup performance\n * @private\n */\n private readonly matches: Map<TSubject, MatcherHandler<TResult>> = new Map()\n\n /**\n * Default handler to execute if no cases match\n * @private\n */\n private defaultHandler?: MatcherHandler<TResult>\n\n /**\n * Create a new Matcher instance\n *\n * @param {TSubject} subject The value to match against\n *\n * @internal Use the `match()` function instead of instantiating directly\n */\n constructor(subject: TSubject) {\n this.subject = subject\n }\n\n /**\n * Add a case to match against the subject\n * Uses strict equality (===) for comparison\n *\n * @param {TSubject} value The value to match against\n * @param {MatcherHandler<TResult>} handler Function to execute if this value matches\n * @returns {this} The matcher instance for method chaining\n *\n * @example\n * ```typescript\n * match('hello')\n * .on('hello', () => 'matched')\n * .on('goodbye', () => 'not matched')\n * ```\n */\n on(value: TSubject, handler: MatcherHandler<TResult>): this {\n this.matches.set(value, handler)\n return this\n }\n\n /**\n * Add multiple values that map to the same handler\n * Simulates PHP's comma-separated case syntax\n *\n * @param {readonly TSubject[]} values Array of values to match\n * @param {MatcherHandler<TResult>} handler Function to execute if any value matches\n * @returns {this} The matcher instance for method chaining\n *\n * @example HTTP Status Codes\n * ```typescript\n * match(statusCode)\n * .onAny([200, 201, 202], () => 'Success')\n * .onAny([400, 401, 403], () => 'Client Error')\n * .otherwise(() => 'Unknown')\n * ```\n *\n * @see on For matching a single value\n */\n onAny(values: readonly TSubject[], handler: MatcherHandler<TResult>): this {\n values.forEach((value) => this.matches.set(value, handler))\n return this\n }\n\n /**\n * Set the default handler and execute the match expression\n * This method triggers evaluation of all accumulated cases\n *\n * @param {MatcherHandler<TResult>} handler Function to execute if no cases match\n * @returns {TResult} The result from the matched handler or the default handler\n * @throws {UnhandledMatchError} If no case matches and no handler catches it\n *\n * @example\n * ```typescript\n * const result = match(status)\n * .on('active', () => 'Active')\n * .on('inactive', () => 'Inactive')\n * .otherwise(() => 'Unknown status')\n * ```\n *\n * @see default For PHP-compatible alias\n * @see valueOf For executing without a default handler\n */\n otherwise(handler: MatcherHandler<TResult>): TResult {\n this.defaultHandler = handler\n return this.evaluate()\n }\n\n /**\n * PHP-compatible alias for otherwise()\n * Identical behavior - sets default handler and executes\n *\n * @param {MatcherHandler<TResult>} handler Function to execute if no cases match\n * @returns {TResult} The result from the matched handler or the default handler\n * @throws {UnhandledMatchError} If no case matches\n *\n * @example\n * ```typescript\n * // PHP-style syntax\n * const result = match(value)\n * .on('case1', () => 'Result1')\n * .default(() => 'Default')\n * ```\n *\n * @see otherwise For the standard method\n */\n default(handler: MatcherHandler<TResult>): TResult {\n return this.otherwise(handler)\n }\n\n /**\n * Execute the match expression without a default handler\n * Throws if no case matches\n *\n * @returns {TResult} The result from the matched handler\n * @throws {UnhandledMatchError} If no case matches\n *\n * @example\n * ```typescript\n * try {\n * const result = match(code)\n * .on(200, () => 'OK')\n * .on(404, () => 'Not Found')\n * .valueOf() // Must have matched\n * } catch (error) {\n * if (error instanceof UnhandledMatchError) {\n * console.error('Invalid code:', error.message)\n * }\n * }\n * ```\n *\n * @see otherwise For safe execution with default handler\n */\n valueOf(): TResult {\n return this.evaluate()\n }\n\n /**\n * Evaluate the match expression by finding the matching case\n *\n * @private\n * @returns {TResult} The result from matched handler or default\n * @throws {UnhandledMatchError} If no match and no default handler\n */\n private evaluate(): TResult {\n if (this.matches.has(this.subject)) {\n return this.matches.get(this.subject)!()\n } else if (this.defaultHandler) {\n return this.defaultHandler()\n }\n throw new UnhandledMatchError(this.subject)\n }\n}\n\n/**\n * Create a new PHP-style match expression\n *\n * @template TSubject The type of the value being matched\n * @template TResult The return type of the match expression handlers\n *\n * @param {TSubject} subject The value to match against (any type)\n * @returns {Matcher<TSubject, TResult>} A Matcher instance for method chaining\n *\n * @example Basic String Matching\n * ```typescript\n * const status = match(statusCode)\n * .on(200, () => 'success')\n * .on(404, () => 'not found')\n * .otherwise(() => 'error')\n * ```\n *\n * @example HTTP Status Codes\n * ```typescript\n * const message = match(code)\n * .onAny([200, 201, 202], () => 'Success')\n * .onAny([400, 401, 403], () => 'Client Error')\n * .on(500, () => 'Server Error')\n * .otherwise(() => 'Unknown')\n * ```\n *\n * @example Conditional Logic\n * ```typescript\n * const result = match(true)\n * .on(age < 18, () => 'Minor')\n * .on(age >= 18 && age < 65, () => 'Adult')\n * .on(age >= 65, () => 'Senior')\n * .otherwise(() => 'Unknown')\n * ```\n *\n * @see Matcher For complete API documentation\n */\nfunction match<TSubject, TResult>(subject: TSubject): Matcher<TSubject, TResult> {\n return new Matcher<TSubject, TResult>(subject)\n}\n\nexport { match, UnhandledMatchError, Matcher }\n"],"names":["UnhandledMatchError","value","Matcher","subject","handler","values","match"],"mappings":"8NAmBA,MAAMA,UAA4B,KAAM,CAMtC,YAAYC,EAAgB,CAC1B,MAAM,0BAA0B,KAAK,UAAUA,CAAK,CAAC,EAAE,EACvD,KAAK,KAAO,qBACd,CACF,CA4BA,MAAMC,CAA2B,CAKd,QAOA,YAAsD,IAM/D,eASR,YAAYC,EAAmB,CAC7B,KAAK,QAAUA,CACjB,CAiBA,GAAGF,EAAiBG,EAAwC,CAC1D,YAAK,QAAQ,IAAIH,EAAOG,CAAO,EACxB,IACT,CAoBA,MAAMC,EAA6BD,EAAwC,CACzE,OAAAC,EAAO,QAASJ,GAAU,KAAK,QAAQ,IAAIA,EAAOG,CAAO,CAAC,EACnD,IACT,CAqBA,UAAUA,EAA2C,CACnD,YAAK,eAAiBA,EACf,KAAK,SAAA,CACd,CAoBA,QAAQA,EAA2C,CACjD,OAAO,KAAK,UAAUA,CAAO,CAC/B,CAyBA,SAAmB,CACjB,OAAO,KAAK,SAAA,CACd,CASQ,UAAoB,CAC1B,GAAI,KAAK,QAAQ,IAAI,KAAK,OAAO,EAC/B,OAAO,KAAK,QAAQ,IAAI,KAAK,OAAO,EAAA,EACtC,GAAW,KAAK,eACd,OAAO,KAAK,eAAA,EAEd,MAAM,IAAIJ,EAAoB,KAAK,OAAO,CAC5C,CACF,CAuCA,SAASM,EAAyBH,EAA+C,CAC/E,OAAO,IAAID,EAA2BC,CAAO,CAC/C"} |
+44
-38
| { | ||
| "name": "@anilkumarthakur/match", | ||
| "description": "PHP-style match expressions for JavaScript/TypeScript", | ||
| "private": false, | ||
| "version": "0.0.7", | ||
| "version": "0.0.8", | ||
| "type": "module", | ||
| "license": "MIT", | ||
| "main": "dist/index.umd.js", | ||
| "module": "dist/index.es.js", | ||
| "types": "dist/index.d.ts", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/anilkumarthakur60/js-match.git" | ||
| }, | ||
| "bugs": { | ||
| "url": "https://github.com/anilkumarthakur60/js-match/issues" | ||
| }, | ||
| "homepage": "https://github.com/anilkumarthakur60/js-match#readme", | ||
| "files": [ | ||
| "/dist" | ||
| "dist", | ||
| "test" | ||
| ], | ||
@@ -17,5 +28,5 @@ "publishConfig": { | ||
| ".": { | ||
| "types": "./dist/index.d.ts", | ||
| "import": "./dist/index.es.js", | ||
| "require": "./dist/index.umd.js", | ||
| "types": "./dist/index.d.ts" | ||
| "require": "./dist/index.umd.js" | ||
| } | ||
@@ -25,48 +36,43 @@ }, | ||
| "dev": "vite", | ||
| "build": "tsc && vite build", | ||
| "build": "tsc --noEmit && vite build", | ||
| "preview": "vite preview", | ||
| "docs:dev": "vitepress dev docs", | ||
| "docs:build": "vitepress build docs", | ||
| "docs:preview": "vitepress preview docs", | ||
| "test": "jest", | ||
| "test:coverage": "jest --coverage", | ||
| "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", | ||
| "format": "prettier --write src/", | ||
| "lint-format":"npm run lint && npm run format" | ||
| "lint-format": "npm run lint && npm run format" | ||
| }, | ||
| "devDependencies": { | ||
| "@rushstack/eslint-patch": "^1.8.0", | ||
| "@types/jest": "^29.5.12", | ||
| "@types/node": "^20.13.0", | ||
| "@typescript-eslint/eslint-plugin": "^7.11.0", | ||
| "@typescript-eslint/parser": "^7.11.0", | ||
| "eslint": "^8.57.0", | ||
| "jest": "^29.7.0", | ||
| "prettier": "^3.2.5", | ||
| "ts-jest": "^29.1.4", | ||
| "@changesets/cli": "^2.29.8", | ||
| "@rushstack/eslint-patch": "^1.15.0", | ||
| "@semantic-release/changelog": "^6.0.3", | ||
| "@semantic-release/git": "^10.0.1", | ||
| "@types/jest": "^30.0.0", | ||
| "@types/node": "^25.0.9", | ||
| "@typescript-eslint/eslint-plugin": "^8.53.0", | ||
| "@typescript-eslint/parser": "^8.53.0", | ||
| "eslint": "^9.39.2", | ||
| "jest": "^30.2.0", | ||
| "prettier": "^3.8.0", | ||
| "semantic-release": "^25.0.2", | ||
| "ts-jest": "^29.4.6", | ||
| "ts-node": "^10.9.2", | ||
| "typescript": "^5.2.2", | ||
| "vite": "^5.2.0", | ||
| "vite-plugin-dts": "^3.9.1" | ||
| "typescript": "^5.9.3", | ||
| "vite": "^7.3.1", | ||
| "vite-plugin-dts": "^4.5.4", | ||
| "vitepress": "^1.6.4" | ||
| }, | ||
| "dependencies": { | ||
| }, | ||
| "peerDependencies": {}, | ||
| "keywords": [ | ||
| "match", | ||
| "match-expression", | ||
| "pattern-matching", | ||
| "php-match", | ||
| "typescript", | ||
| "javascript", | ||
| "conditional", | ||
| "logic", | ||
| "pattern-matching", | ||
| "php-match", | ||
| "npm", | ||
| "library", | ||
| "match-expression", | ||
| "match-library", | ||
| "match-js", | ||
| "match-js-library", | ||
| "match-js-npm", | ||
| "match-js-package", | ||
| "match-js-package-npm", | ||
| "match-js-package-npm-library", | ||
| "php match expression in javascript", | ||
| "php match in javascript" | ||
| "switch-alternative", | ||
| "conditional" | ||
| ] | ||
| } | ||
| } |
+445
-61
| # @anilkumarthakur/match | ||
| `@anilkumarthakur/match` is a JavaScript library inspired by PHP's match expression. It allows for more readable and | ||
| maintainable code by providing a clean and concise way to handle small to large conditional logic. | ||
| [](https://www.npmjs.com/package/@anilkumarthakur/match) | ||
| [](LICENSE) | ||
| [](test/) | ||
| [](#) | ||
| PHP-style match expressions for JavaScript/TypeScript with 100% type safety and comprehensive test coverage. | ||
| `@anilkumarthakur/match` brings the power and elegance of PHP's match expression to the JavaScript/TypeScript world. It provides a clean, type-safe alternative to complex switch statements and nested if-else logic. | ||
| ## Features | ||
| โจ **Type-Safe**: Full TypeScript support with generic types for subject and result | ||
| ๐ฏ **Readable**: Clean, expressive syntax inspired by PHP match expressions | ||
| ๐ **Fast**: Efficient equality-based matching using JavaScript's Map | ||
| ๐ฆ **Lightweight**: Zero dependencies, ~1.2KB gzipped | ||
| ๐งช **Well-Tested**: 245 comprehensive tests with 100% code coverage | ||
| ๐ **Chainable**: Fluent API for method chaining | ||
| ๐ **Cross-Platform**: Works in Node.js and browsers (ESM + UMD) | ||
| ## Installation | ||
| Install the package using npm: | ||
| ```shell | ||
| ### npm | ||
| ```bash | ||
| npm install @anilkumarthakur/match | ||
| ``` | ||
| ## Basic Usage | ||
| ```ts | ||
| import {match} from '@anilkumarthakur/match'; | ||
| const handleCheck = (types: string) => { | ||
| return match(types) | ||
| .on('success', () => { | ||
| console.log('----------------success output--', 'success'); | ||
| return 'success'; | ||
| }) | ||
| .on('error', () => { | ||
| console.log('----------------error output--', 'error'); | ||
| return 'error'; | ||
| }) | ||
| .on('warning', () => { | ||
| console.log('----------------warning output--', 'warning'); | ||
| return 'warning'; | ||
| }) | ||
| .on('info', () => { | ||
| console.log('----------------info output--', 'info'); | ||
| return 'info'; | ||
| }) | ||
| .on('defaultNotify', () => { | ||
| console.log('----------------defaultNotify output--', 'defaultNotify'); | ||
| return 'defaultNotify'; | ||
| }) | ||
| .on('dark', () => { | ||
| console.log('----------------dark output--', 'dark'); | ||
| return 'dark'; | ||
| }) | ||
| .on('light', () => { | ||
| console.log('----------------light output--', 'light'); | ||
| return 'light'; | ||
| }) | ||
| .on('spinner', () => { | ||
| console.log('----------------spinner output--', 'spinner'); | ||
| return 'spinner'; | ||
| }) | ||
| .otherwise(() => { | ||
| console.log('----------------otherwise output:', 'otherwise'); | ||
| return 'otherwise'; | ||
| }); | ||
| ### yarn | ||
| ```bash | ||
| yarn add @anilkumarthakur/match | ||
| ``` | ||
| ### bun | ||
| ```bash | ||
| bun add @anilkumarthakur/match | ||
| ``` | ||
| ## Quick Start | ||
| ```typescript | ||
| import { match } from '@anilkumarthakur/match' | ||
| const result = match('success') | ||
| .on('success', () => 'Operation successful!') | ||
| .on('error', () => 'Something went wrong') | ||
| .otherwise(() => 'Unknown status') | ||
| console.log(result) // "Operation successful!" | ||
| ``` | ||
| ## API Reference | ||
| ### `match<TSubject, TResult>(subject: TSubject): Matcher` | ||
| Creates a new match expression for the given subject value. | ||
| **Parameters:** | ||
| - `subject` - The value to match against (any type) | ||
| **Returns:** A `Matcher` instance for method chaining | ||
| **Example:** | ||
| ```typescript | ||
| const matcher = match(statusCode) | ||
| ``` | ||
| ### `on(value: TSubject, handler: () => TResult): Matcher` | ||
| Adds a case to match against. Uses strict equality (===) for comparison. | ||
| **Parameters:** | ||
| - `value` - The value to match | ||
| - `handler` - Function returning the result if matched | ||
| **Returns:** The matcher instance (for chaining) | ||
| **Example:** | ||
| ```typescript | ||
| match(status) | ||
| .on(200, () => 'Success') | ||
| .on(404, () => 'Not Found') | ||
| ``` | ||
| ### `onAny(values: readonly TSubject[], handler: () => TResult): Matcher` | ||
| Adds multiple values that all map to the same handler (simulates PHP's comma-separated cases). | ||
| **Parameters:** | ||
| - `values` - Array of values to match | ||
| - `handler` - Function to execute if any value matches | ||
| **Returns:** The matcher instance (for chaining) | ||
| **Example:** | ||
| ```typescript | ||
| match(status) | ||
| .onAny([200, 201, 202], () => 'Success') | ||
| .onAny([400, 401, 403], () => 'Client Error') | ||
| ``` | ||
| ### `otherwise(handler: () => TResult): TResult` | ||
| Sets the default handler and executes the match. Returns immediately with the result. | ||
| **Parameters:** | ||
| - `handler` - Function to execute if no cases match | ||
| **Returns:** The result from matched handler or default handler | ||
| **Throws:** `UnhandledMatchError` if no match found and no default provided | ||
| **Example:** | ||
| ```typescript | ||
| const result = match(value) | ||
| .on('expected', () => 'matched') | ||
| .otherwise(() => 'default') | ||
| ``` | ||
| ### `default(handler: () => TResult): TResult` | ||
| PHP-compatible alias for `otherwise()`. Identical behavior. | ||
| **Example:** | ||
| ```typescript | ||
| const result = match(value) | ||
| .on('expected', () => 'matched') | ||
| .default(() => 'default') | ||
| ``` | ||
| ### `valueOf(): TResult` | ||
| Executes the match without a default handler. Throws if no match found. | ||
| **Returns:** The result from matched handler | ||
| **Throws:** `UnhandledMatchError` if no match found | ||
| **Example:** | ||
| ```typescript | ||
| const result = match('test') | ||
| .on('test', () => 'matched') | ||
| .valueOf() // Must have matched something | ||
| ``` | ||
| ### `UnhandledMatchError` | ||
| Custom error thrown when no case matches and no default handler is provided. | ||
| **Properties:** | ||
| - `name` - "UnhandledMatchError" | ||
| - `message` - Contains the unmatched value | ||
| **Example:** | ||
| ```typescript | ||
| try { | ||
| match('foo') | ||
| .on('bar', () => 'bar') | ||
| .valueOf() | ||
| } catch (error) { | ||
| if (error instanceof UnhandledMatchError) { | ||
| console.error('No match found for:', error.message) | ||
| } | ||
| } | ||
| ``` | ||
| // Example of using match with various data types | ||
| const complexCheck = (input: unknown) => { | ||
| return match(input) | ||
| .on('hello', () => 'Matched hello') | ||
| .on(42, () => 'Matched number 42') | ||
| .on(true, () => 'Matched true') | ||
| .on(null, () => 'Matched null') | ||
| .on(undefined, () => 'Matched undefined') | ||
| .otherwise(() => 'No match found'); | ||
| ## Usage Examples | ||
| ### Basic String Matching | ||
| ```typescript | ||
| import { match } from '@anilkumarthakur/match' | ||
| const getRole = (role: string) => { | ||
| return match(role) | ||
| .on('admin', () => 'Full access') | ||
| .on('user', () => 'Limited access') | ||
| .on('guest', () => 'Read-only access') | ||
| .otherwise(() => 'Unknown role') | ||
| } | ||
| console.log(handleCheck('success')); | ||
| console.log(complexCheck('hello')); | ||
| console.log(complexCheck(42)); | ||
| console.log(complexCheck(true)); | ||
| console.log(complexCheck(null)); | ||
| console.log(complexCheck('unmatched')); | ||
| console.log(getRole('admin')) // "Full access" | ||
| ``` | ||
| ### Number Matching (HTTP Status Codes) | ||
| ```typescript | ||
| const handleResponse = (statusCode: number) => { | ||
| return match(statusCode) | ||
| .on(200, () => 'OK') | ||
| .onAny([201, 202, 204], () => 'Created/Accepted') | ||
| .on(400, () => 'Bad Request') | ||
| .on(401, () => 'Unauthorized') | ||
| .on(404, () => 'Not Found') | ||
| .on(500, () => 'Server Error') | ||
| .otherwise(() => 'Unknown Status') | ||
| } | ||
| console.log(handleResponse(200)) // "OK" | ||
| console.log(handleResponse(201)) // "Created/Accepted" | ||
| console.log(handleResponse(999)) // "Unknown Status" | ||
| ``` | ||
| ### Complex Notifications | ||
| ```typescript | ||
| const showNotification = (type: string, message: string) => { | ||
| const styling = match(type) | ||
| .on('success', () => ({ color: 'green', icon: 'โ' })) | ||
| .on('error', () => ({ color: 'red', icon: 'โ' })) | ||
| .on('warning', () => ({ color: 'orange', icon: 'โ ' })) | ||
| .on('info', () => ({ color: 'blue', icon: 'โน' })) | ||
| .otherwise(() => ({ color: 'gray', icon: 'โข' })) | ||
| return `[${styling.icon}] ${message}` | ||
| } | ||
| console.log(showNotification('success', 'Saved!')) // "[โ] Saved!" | ||
| console.log(showNotification('error', 'Failed!')) // "[โ] Failed!" | ||
| ``` | ||
| ### Nested Match Expressions | ||
| ```typescript | ||
| const getUserStatus = (userId: string, status: string) => { | ||
| return match(userId) | ||
| .on('admin', () => { | ||
| return match(status) | ||
| .on('active', () => 'Admin is active') | ||
| .on('inactive', () => 'Admin is inactive') | ||
| .otherwise(() => 'Admin status unknown') | ||
| }) | ||
| .on('user', () => { | ||
| return match(status) | ||
| .on('active', () => 'User is active') | ||
| .otherwise(() => 'User is inactive') | ||
| }) | ||
| .otherwise(() => 'User not found') | ||
| } | ||
| console.log(getUserStatus('admin', 'active')) // "Admin is active" | ||
| console.log(getUserStatus('user', 'active')) // "User is active" | ||
| console.log(getUserStatus('guest', 'active')) // "User not found" | ||
| ``` | ||
| ### Type-Safe Unions | ||
| ```typescript | ||
| type LogLevel = 'debug' | 'info' | 'warn' | 'error' | ||
| const getLogColor = (level: LogLevel): string => { | ||
| return match(level) | ||
| .on('debug', () => 'gray') | ||
| .on('info', () => 'blue') | ||
| .on('warn', () => 'yellow') | ||
| .on('error', () => 'red') | ||
| .otherwise(() => 'white') | ||
| } | ||
| console.log(getLogColor('info')) // "blue" | ||
| ``` | ||
| ### Conditional Logic with match(true) | ||
| ```typescript | ||
| const getUserMessage = (age: number, isPremium: boolean) => { | ||
| return match(true) | ||
| .on(age < 13, () => 'Not eligible') | ||
| .on(age >= 13 && age < 18, () => 'Teen user') | ||
| .on(age >= 18 && !isPremium, () => 'Free user') | ||
| .on(age >= 18 && isPremium, () => 'Premium user') | ||
| .otherwise(() => 'Unknown') | ||
| } | ||
| console.log(getUserMessage(25, true)) // "Premium user" | ||
| console.log(getUserMessage(16, false)) // "Teen user" | ||
| ``` | ||
| ### Days in Month (Real-World Example) | ||
| ```typescript | ||
| const daysInMonth = (month: string, year: number): number => { | ||
| const isLeap = (y: number) => y % 4 === 0 && (y % 100 !== 0 || y % 400 === 0) | ||
| return match(month.toLowerCase().slice(0, 3)) | ||
| .on('jan', () => 31) | ||
| .on('feb', () => (isLeap(year) ? 29 : 28)) | ||
| .on('mar', () => 31) | ||
| .on('apr', () => 30) | ||
| .on('may', () => 31) | ||
| .on('jun', () => 30) | ||
| .on('jul', () => 31) | ||
| .on('aug', () => 31) | ||
| .on('sep', () => 30) | ||
| .on('oct', () => 31) | ||
| .on('nov', () => 30) | ||
| .on('dec', () => 31) | ||
| .otherwise(() => { | ||
| throw new Error('Invalid month') | ||
| }) | ||
| } | ||
| console.log(daysInMonth('February', 2024)) // 29 (leap year) | ||
| console.log(daysInMonth('February', 2025)) // 28 | ||
| ``` | ||
| ## Comparison with PHP match() | ||
| ### PHP | ||
| ```php | ||
| $result = match($status) { | ||
| 'success', 'ok' => 'All good', | ||
| 'error', 'fail' => 'Something went wrong', | ||
| default => 'Unknown' | ||
| }; | ||
| ``` | ||
| ### JavaScript (this library) | ||
| ```typescript | ||
| const result = match(status) | ||
| .onAny(['success', 'ok'], () => 'All good') | ||
| .onAny(['error', 'fail'], () => 'Something went wrong') | ||
| .otherwise(() => 'Unknown') | ||
| ``` | ||
| ## Supported Types | ||
| The library supports matching on any JavaScript type using strict equality (===): | ||
| - โ Strings | ||
| - โ Numbers (including Infinity, -Infinity) | ||
| - โ Booleans | ||
| - โ null / undefined | ||
| - โ Symbols | ||
| - โ BigInt | ||
| - โ Objects (by reference) | ||
| - โ Arrays (by reference) | ||
| - โ Functions (by reference) | ||
| - โ Enums | ||
| - โ Class instances (by reference) | ||
| ## Type Safety | ||
| Full TypeScript support with automatic type inference: | ||
| ```typescript | ||
| // Explicit types | ||
| const result = match<string, number>('test') | ||
| .on('test', () => 123) | ||
| .otherwise(() => 456) | ||
| // Inferred types | ||
| const result2 = match('test') | ||
| .on('test', () => 'result') // Inferred as string result | ||
| .otherwise(() => 'default') | ||
| // Union types | ||
| type Status = 'success' | 'pending' | 'error' | ||
| const result3 = match<Status, string>('success') | ||
| .on('success', () => 'Done') | ||
| .on('pending', () => 'In progress') | ||
| .on('error', () => 'Failed') | ||
| .otherwise(() => 'Unknown') | ||
| ``` | ||
| ## Performance | ||
| - โก Uses JavaScript Map for O(1) lookup time | ||
| - ๐พ Lazy evaluation - only matched handler executes | ||
| - ๐ฆ ~1.2KB gzipped bundle size | ||
| ## Testing | ||
| The library includes 245 comprehensive tests with 100% code coverage: | ||
| ```bash | ||
| # Run all tests | ||
| npm test | ||
| # Run with coverage | ||
| npm run test:coverage | ||
| # Watch mode | ||
| npm test -- --watch | ||
| ``` | ||
| Test categories: | ||
| - Basic functionality | ||
| - Type matching (strings, numbers, booleans, objects, arrays, etc.) | ||
| - All API methods (on, onAny, otherwise, default, valueOf) | ||
| - Error handling | ||
| - Type safety | ||
| - Real-world examples | ||
| - Edge cases and performance | ||
| ## Browser Support | ||
| Works in all modern browsers and Node.js 14+: | ||
| - Chrome/Edge (latest) | ||
| - Firefox (latest) | ||
| - Safari (latest) | ||
| - Node.js 14+ | ||
| ## Contributing | ||
| Feel free to submit issues and pull requests. Contributions are welcome! | ||
| Contributions are welcome! Please feel free to submit issues and pull requests. | ||
| ### Development | ||
| ```bash | ||
| # Install dependencies | ||
| npm install | ||
| # Run tests | ||
| npm test | ||
| # Build | ||
| npm run build | ||
| # Lint and format | ||
| npm run lint-format | ||
| ``` | ||
| ## License | ||
| MIT - See [LICENSE](LICENSE) for details | ||
| ## Author | ||
| [Anil Kumar Thakur](https://github.com/anilkumarthakur60) | ||
| ## Related | ||
| - [PHP match expression](https://www.php.net/manual/en/control-structures.match.php) - Official PHP documentation | ||
| - [JavaScript switch statement](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/switch) | ||
| ## Changelog | ||
| See [CHANGELOG.md](CHANGELOG.md) for release history and updates. |
| import { match } from './match'; | ||
| import { MatchChain as matchType, Handler as handlerType } from './types/main'; | ||
| export { match }; | ||
| export type { matchType, handlerType }; |
| import { MatchChain } from './types/main'; | ||
| declare function match<T = unknown>(subject: any): MatchChain<T>; | ||
| export { match }; | ||
| export type { MatchChain }; |
| export type Handler<T> = () => T; | ||
| export interface MatchChain<T> { | ||
| on: (value: any, handler: Handler<T>) => MatchChain<T>; | ||
| otherwise: (handler: Handler<T>) => T; | ||
| } |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
No License Found
LicenseLicense information could not be found.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
No website
QualityPackage does not have a website.
Found 1 instance in 1 package
129069
1282.04%29
222.22%0
-100%2814
8176.47%1
-50%2
-33.33%459
520.27%0
-100%18
38.46%1
Infinity%