1import { dual, identity } from "../Function.ts"
2import type {
3 Case,
4 Matcher,
5 Not,
6 SafeRefinement,
7 TypeMatcher,
8 Types,
9 ValueFlavor,
10 ValueMatcher,
11 When
12} from "../Match.ts"
13import * as Option from "../Option.ts"
14import { pipeArguments } from "../Pipeable.ts"
15import type * as Predicate from "../Predicate.ts"
16import * as Result from "../Result.ts"
17import type { Unify } from "../Unify.ts"
18
19/** @internal */
20export const TypeId = "~effect/Match/Matcher"
21
22/** @internal */
23export type Contextual<P, Fallback> = [P] extends [never] ? Fallback : P
24
25type TagHandlers<D extends string, R, Ret> = {
26 readonly [Tag in Types.Tags<D, R> & string]: (_: Extract<R, Record<D, Tag>>) => Ret
27}
28
29type PartialTagHandlers<D extends string, R, Ret> = {
30 readonly [Tag in Types.Tags<D, R> & string]?: ((_: Extract<R, Record<D, Tag>>) => Ret) | undefined
31}
32
33type ValueTagHandlers<I> = {
34 readonly [Tag in Types.Tags<"_tag", I> & string]: (_: Extract<I, { readonly _tag: Tag }>) => any
35}
36
37const TypeMatcherProto: Omit<TypeMatcher<any, any, any, any, any, any>, "cases" | "select"> = {
38 [TypeId]: {
39 _input: identity,
40 _filters: identity,
41 _remaining: identity,
42 _result: identity,
43 _return: identity,
44 _args: identity
45 },
46 _tag: "TypeMatcher",
47 add<I, R, RA, A>(
48 this: TypeMatcher<any, any, any, any, any, any>,
49 _case: Case
50 ): TypeMatcher<I, R, RA, A, any, any> {
51 return makeTypeMatcher(this.select, [...this.cases, _case])
52 },
53 pipe() {
54 return pipeArguments(this, arguments)
55 }
56}
57
58function makeTypeMatcher<I, R, RA, A, Ret, Args extends Array<any>>(
59 select: (...args: any) => I,
60 cases: ReadonlyArray<Case>
61): TypeMatcher<I, R, RA, A, Ret, Args> {
62 const matcher = Object.create(TypeMatcherProto)
63 matcher.select = select
64 matcher.cases = cases
65 return matcher
66}
67
68const ValueMatcherProto: Omit<
69 ValueMatcher<any, any, any, any, any>,
70 "provided" | "value"
71> = {
72 [TypeId]: {
73 _input: identity,
74 _filters: identity,
75 _result: identity,
76 _return: identity,
77 _flavor: identity
78 },
79 _tag: "ValueMatcher",
80 add<I, R, RA, A, Provided>(
81 this: ValueMatcher<any, any, any, any, any>,
82 _case: Case
83 ): ValueMatcher<I, R, RA, A, Provided> {
84 if (Result.isSuccess(this.value)) {
85 return this
86 }
87
88 if (_case._tag === "When" && _case.guard(this.provided) === true) {
89 return makeValueMatcher(
90 this.provided,
91 Result.succeed(_case.evaluate(this.provided))
92 )
93 } else if (_case._tag === "Not" && _case.guard(this.provided) === false) {
94 return makeValueMatcher(
95 this.provided,
96 Result.succeed(_case.evaluate(this.provided))
97 )
98 }
99
100 return this
101 },
102 pipe() {
103 return pipeArguments(this, arguments)
104 }
105}
106
107function makeValueMatcher<I, R, RA, A, Provided>(
108 provided: Provided,
109 value: Result.Result<Provided, RA>
110): ValueMatcher<I, R, RA, A, Provided> {
111 const matcher = Object.create(ValueMatcherProto)
112 matcher.provided = provided
113 matcher.value = value
114 return matcher
115}
116
117const makeWhen = (
118 guard: (u: unknown) => boolean,
119 evaluate: (input: unknown, ...args: Array<any>) => any
120): When => ({
121 _tag: "When",
122 guard,
123 evaluate
124})
125
126const makeNot = (
127 guard: (u: unknown) => boolean,
128 evaluate: (input: unknown, ...args: Array<any>) => any
129): Not => ({
130 _tag: "Not",
131 guard,
132 evaluate
133})
134
135const makePredicate = (pattern: unknown): Predicate.Predicate<unknown> => {
136 if (typeof pattern === "function") {
137 return pattern as Predicate.Predicate<unknown>
138 } else if (Array.isArray(pattern)) {
139 const predicates = pattern.map(makePredicate)
140 const len = predicates.length
141
142 return (u: unknown) => {
143 if (!Array.isArray(u)) {
144 return false
145 }
146
147 for (let i = 0; i < len; i++) {
148 if (predicates[i](u[i]) === false) {
149 return false
150 }
151 }
152
153 return true
154 }
155 } else if (pattern !== null && typeof pattern === "object") {
156 const keysAndPredicates = Reflect.ownKeys(pattern).map(
157 (key) => [key, makePredicate((pattern as any)[key])] as const
158 )
159 const len = keysAndPredicates.length
160
161 return (u: unknown) => {
162 if (typeof u !== "object" || u === null) {
163 return false
164 }
165
166 for (let i = 0; i < len; i++) {
167 const [key, predicate] = keysAndPredicates[i]
168 if (!(key in u) || predicate((u as any)[key]) === false) {
169 return false
170 }
171 }
172
173 return true
174 }
175 }
176
177 return (u: unknown) => u === pattern
178}
179
180const makeOrPredicate = (
181 patterns: ReadonlyArray<unknown>
182): Predicate.Predicate<unknown> => {
183 const predicates = patterns.map(makePredicate)
184 const len = predicates.length
185
186 return (u: unknown) => {
187 for (let i = 0; i < len; i++) {
188 if (predicates[i](u) === true) {
189 return true
190 }
191 }
192
193 return false
194 }
195}
196
197const makeAndPredicate = (
198 patterns: ReadonlyArray<unknown>
199): Predicate.Predicate<unknown> => {
200 const predicates = patterns.map(makePredicate)
201 const len = predicates.length
202
203 return (u: unknown) => {
204 for (let i = 0; i < len; i++) {
205 if (predicates[i](u) === false) {
206 return false
207 }
208 }
209
210 return true
211 }
212}
213
214/** @internal */
215export const type = <I>(): Matcher<
216 I,
217 Types.Without<never>,
218 I,
219 never,
220 never
221> => makeTypeMatcher(identity, [])
222
223/** @internal */
224export const fn = <Args extends Array<any>, I>(
225 select: (...args: Args) => I
226): Matcher<I, Types.Without<never>, I, never, never, any, Args> => makeTypeMatcher(select, [])
227
228/** @internal */
229export const value = <const I>(
230 i: I
231): Matcher<I, Types.Without<never>, I, never, ValueFlavor> => makeValueMatcher(i, Result.fail(i))
232
233/** @internal */
234export const valueTags: {
235 <
236 const I,
237 P extends
238 & ValueTagHandlers<I>
239 & { readonly [Tag in Exclude<keyof P, Types.Tags<"_tag", I>>]: never }
240 >(
241 fields: Contextual<P, ValueTagHandlers<I>>
242 ): (input: I) => Unify<ReturnType<P[keyof P]>>
243 <
244 const I,
245 P extends
246 & ValueTagHandlers<I>
247 & { readonly [Tag in Exclude<keyof P, Types.Tags<"_tag", I>>]: never }
248 >(
249 input: I,
250 fields: Contextual<P, ValueTagHandlers<I>>
251 ): Unify<ReturnType<P[keyof P]>>
252} = dual(
253 2,
254 <
255 const I,
256 P extends
257 & ValueTagHandlers<I>
258 & { readonly [Tag in Exclude<keyof P, Types.Tags<"_tag", I>>]: never }
259 >(
260 input: I,
261 fields: Contextual<P, ValueTagHandlers<I>>
262 ): Unify<ReturnType<P[keyof P]>> => {
263 const match: any = tagsExhaustive(fields as any)(makeTypeMatcher(identity, []))
264 return match(input)
265 }
266)
267
268/** @internal */
269export const typeTags = <I>() =>
270<
271 P extends {
272 readonly [Tag in Types.Tags<"_tag", I> & string]: (
273 _: Extract<I, { readonly _tag: Tag }>
274 ) => any
275 }
276>(
277 fields: P
278) => {
279 const match: any = tagsExhaustive(fields as any)(makeTypeMatcher(identity, []))
280 return (input: I): Unify<ReturnType<P[keyof P]>> => match(input)
281}
282
283/** @internal */
284export const withReturnType =
285 <Ret>() =>
286 <I, F, R, A, Pr, _, Args extends Array<any>>(self: Matcher<I, F, R, A, Pr, _, Args>): [Ret] extends [
287 [A] extends [never] ? any : A
288 ] ? Matcher<I, F, R, A, Pr, Ret, Args>
289 : "withReturnType constraint does not extend Result type" => self as any
290
291/** @internal */
292export const when = <
293 R,
294 const P extends Types.PatternPrimitive<R> | Types.PatternBase<R>,
295 Ret,
296 Args extends Array<any>,
297 Fn extends (_: Types.WhenMatch<R, P>, ...args: Args) => Ret
298>(
299 pattern: P,
300 f: Fn
301) =>
302<I, F, A, Pr>(
303 self: Matcher<I, F, R, A, Pr, Ret, Args>
304): Matcher<
305 I,
306 Types.AddWithout<F, Types.PForExclude<P>>,
307 Types.ApplyFilters<I, Types.AddWithout<F, Types.PForExclude<P>>>,
308 A | ReturnType<Fn>,
309 Pr,
310 Ret,
311 Args
312> => (self as any).add(makeWhen(makePredicate(pattern), f as any))
313
314/** @internal */
315export const whenOr = <
316 R,
317 const P extends ReadonlyArray<
318 Types.PatternPrimitive<R> | Types.PatternBase<R>
319 >,
320 Ret,
321 Args extends Array<any>,
322 Fn extends (_: Types.WhenMatch<R, P[number]>, ...args: Args) => Ret
323>(
324 ...args: [...patterns: P, f: Fn]
325) =>
326<I, F, A, Pr>(
327 self: Matcher<I, F, R, A, Pr, Ret, Args>
328): Matcher<
329 I,
330 Types.AddWithout<F, Types.PForExclude<P[number]>>,
331 Types.ApplyFilters<I, Types.AddWithout<F, Types.PForExclude<P[number]>>>,
332 A | ReturnType<Fn>,
333 Pr,
334 Ret,
335 Args
336> => {
337 const onMatch = args[args.length - 1] as any
338 const patterns = args.slice(0, -1) as unknown as P
339 return (self as any).add(makeWhen(makeOrPredicate(patterns), onMatch))
340}
341
342/** @internal */
343export const whenAnd = <
344 R,
345 const P extends ReadonlyArray<
346 Types.PatternPrimitive<R> | Types.PatternBase<R>
347 >,
348 Ret,
349 Args extends Array<any>,
350 Fn extends (_: Types.WhenMatch<R, Types.ArrayToIntersection<P>>, ...args: Args) => Ret
351>(
352 ...args: [...patterns: P, f: Fn]
353) =>
354<I, F, A, Pr>(
355 self: Matcher<I, F, R, A, Pr, Ret, Args>
356): Matcher<
357 I,
358 Types.AddWithout<F, Types.PForExclude<Types.ArrayToIntersection<P>>>,
359 Types.ApplyFilters<
360 I,
361 Types.AddWithout<F, Types.PForExclude<Types.ArrayToIntersection<P>>>
362 >,
363 A | ReturnType<Fn>,
364 Pr,
365 Ret,
366 Args
367> => {
368 const onMatch = args[args.length - 1] as any
369 const patterns = args.slice(0, -1) as unknown as P
370 return (self as any).add(makeWhen(makeAndPredicate(patterns), onMatch))
371}
372
373/** @internal */
374export const discriminator = <D extends string>(field: D) =>
375<
376 R,
377 P extends Types.Tags<D, R> & string,
378 Ret,
379 Fn extends (_: Extract<R, Record<D, P>>) => Ret
380>(
381 ...pattern: [
382 first: P,
383 ...values: Array<P>,
384 f: Fn
385 ]
386) => {
387 const f = pattern[pattern.length - 1]
388 const values: Array<P> = pattern.slice(0, -1) as any
389 const pred = values.length === 1
390 ? (_: any) => _ != null && _[field] === values[0]
391 : (_: any) => _ != null && values.includes(_[field])
392
393 return <I, F, A, Pr>(
394 self: Matcher<I, F, R, A, Pr, Ret>
395 ): Matcher<
396 I,
397 Types.AddWithout<F, Extract<R, Record<D, P>>>,
398 Types.ApplyFilters<I, Types.AddWithout<F, Extract<R, Record<D, P>>>>,
399 A | ReturnType<Fn>,
400 Pr,
401 Ret
402 > => (self as any).add(makeWhen(pred, f as any)) as any
403}
404
405/** @internal */
406export const discriminatorStartsWith = <D extends string>(field: D) =>
407<
408 R,
409 P extends string,
410 Ret,
411 Fn extends (_: Extract<R, Record<D, `${P}${string}`>>) => Ret
412>(
413 pattern: P,
414 f: Fn
415) => {
416 const pred = (_: any) => _ != null && typeof _[field] === "string" && _[field].startsWith(pattern)
417
418 return <I, F, A, Pr>(
419 self: Matcher<I, F, R, A, Pr, Ret>
420 ): Matcher<
421 I,
422 Types.AddWithout<F, Extract<R, Record<D, `${P}${string}`>>>,
423 Types.ApplyFilters<
424 I,
425 Types.AddWithout<F, Extract<R, Record<D, `${P}${string}`>>>
426 >,
427 A | ReturnType<Fn>,
428 Pr,
429 Ret
430 > => (self as any).add(makeWhen(pred, f as any)) as any
431}
432
433/** @internal */
434export const discriminators = <D extends string>(field: D) =>
435<
436 R,
437 Ret,
438 P extends
439 & PartialTagHandlers<D, R, Ret>
440 & { readonly [Tag in Exclude<keyof P, Types.Tags<D, R>>]: never }
441>(
442 fields: Contextual<P, PartialTagHandlers<D, R, Ret>>
443) => {
444 const predicate = makeWhen(
445 (arg: any) => arg != null && Object.hasOwn(fields, arg[field]),
446 (data: any) => (fields as any)[data[field]](data)
447 )
448
449 return <I, F, A, Pr>(
450 self: Matcher<I, F, R, A, Pr, Ret>
451 ): Matcher<
452 I,
453 Types.AddWithout<F, Extract<R, Record<D, keyof P>>>,
454 Types.ApplyFilters<I, Types.AddWithout<F, Extract<R, Record<D, keyof P>>>>,
455 A | ReturnType<P[keyof P] & {}>,
456 Pr,
457 Ret
458 > => (self as any).add(predicate)
459}
460
461/** @internal */
462export const discriminatorsExhaustive: <D extends string>(
463 field: D
464) => <
465 R,
466 Ret,
467 P extends
468 & TagHandlers<D, R, Ret>
469 & { readonly [Tag in Exclude<keyof P, Types.Tags<D, R>>]: never }
470>(
471 fields: Contextual<P, TagHandlers<D, R, Ret>>
472) => <I, F, A, Pr>(
473 self: Matcher<I, F, R, A, Pr, Ret>
474) => [Pr] extends [never] ? (u: I) => Unify<A | ReturnType<P[keyof P]>>
475 : Unify<A | ReturnType<P[keyof P]>> = (field: string) => (fields: object) => {
476 const addCases = discriminators(field)(fields)
477 return (matcher: any) => exhaustive(addCases(matcher))
478 }
479
480/** @internal */
481export const tag: <
482 R,
483 P extends Types.Tags<"_tag", R> & string,
484 Ret,
485 Args extends Array<any>,
486 Fn extends (_: Extract<R, Record<"_tag", P>>, ...args: Args) => Ret
487>(
488 ...pattern: [
489 first: P,
490 ...values: Array<P>,
491 f: Fn
492 ]
493) => <I, F, A, Pr>(
494 self: Matcher<I, F, R, A, Pr, Ret, Args>
495) => Matcher<
496 I,
497 Types.AddWithout<F, Extract<R, Record<"_tag", P>>>,
498 Types.ApplyFilters<I, Types.AddWithout<F, Extract<R, Record<"_tag", P>>>>,
499 ReturnType<Fn> | A,
500 Pr,
501 Ret,
502 Args
503> = discriminator("_tag") as any
504
505/** @internal */
506export const tagStartsWith = discriminatorStartsWith("_tag")
507
508/** @internal */
509export const tags = discriminators("_tag")
510
511/** @internal */
512export const tagsExhaustive = discriminatorsExhaustive("_tag")
513
514/** @internal */
515export const not = <
516 R,
517 const P extends Types.PatternPrimitive<R> | Types.PatternBase<R>,
518 Ret,
519 Args extends Array<any>,
520 Fn extends (_: Types.NotMatch<R, P>, ...args: Args) => Ret
521>(
522 pattern: P,
523 f: Fn
524) =>
525<I, F, A, Pr>(
526 self: Matcher<I, F, R, A, Pr, Ret, Args>
527): Matcher<
528 I,
529 Types.AddOnly<F, Types.WhenMatch<R, P>>,
530 Types.ApplyFilters<I, Types.AddOnly<F, Types.WhenMatch<R, P>>>,
531 A | ReturnType<Fn>,
532 Pr,
533 Ret,
534 Args
535> => (self as any).add(makeNot(makePredicate(pattern), f as any))
536
537/** @internal */
538export const nonEmptyString: SafeRefinement<string, never> =
539 ((u: unknown) => typeof u === "string" && u.length > 0) as any
540
541/** @internal */
542export const is: <
543 Literals extends ReadonlyArray<string | number | boolean | null | bigint>
544>(
545 ...literals: Literals
546) => SafeRefinement<Literals[number]> = (...literals): any => {
547 const len = literals.length
548 return (u: unknown) => {
549 for (let i = 0; i < len; i++) {
550 if (u === literals[i]) {
551 return true
552 }
553 }
554 return false
555 }
556}
557
558/** @internal */
559export const any: SafeRefinement<unknown, any> = (() => true) as any
560
561/** @internal */
562export const defined = <A>(u: A): u is A & {} => (u !== undefined && u !== null) as any
563
564/** @internal */
565export const instanceOf = <A extends abstract new(...args: any) => any>(
566 constructor: A
567): SafeRefinement<InstanceType<A>, never> => ((u: unknown) => u instanceof constructor) as any
568
569/** @internal */
570export const orElse =
571 <RA, Ret, Args extends Array<any>, F extends (_: RA, ...args: Args) => Ret>(f: F) =>
572 <I, R, A, Pr>(
573 self: Matcher<I, R, RA, A, Pr, Ret, Args>
574 ): [Pr] extends [never] ? [Args] extends [[]] ? (input: I) => Unify<ReturnType<F> | A>
575 : (...args: Args) => Unify<ReturnType<F> | A>
576 : Unify<ReturnType<F> | A> =>
577 {
578 const toResult = result(self)
579
580 if (Result.isResult(toResult)) {
581 return toResult._tag === "Success" ? toResult.success as any : (f as any)(toResult.failure)
582 }
583
584 // @ts-expect-error
585 return (...args: Array<any>) => {
586 const a = (toResult as any)(...args)
587 return Result.isSuccess(a) ? a.success : (f as any)(a.failure, ...args)
588 }
589 }
590
591/** @internal */
592export const orElseAbsurd: <I, R, RA, A, Pr, Ret, Args extends Array<any>>(
593 self: Matcher<I, R, RA, A, Pr, Ret, Args>
594 // oxlint-disable-next-line max-len
595) => [Pr] extends [never] ? [Args] extends [[]] ? (input: I) => Unify<A> : (...args: Args) => Unify<A> : Unify<A> = ((
596 self: Matcher<any, any, any, any, any, any, any>
597) =>
598 orElse(
599 (() => {
600 throw new Error("effect/Match/orElseAbsurd: absurd")
601 }) as any
602 )(self)) as any
603
604/** @internal */
605export const result: <I, F, R, A, Pr, Ret, Args extends Array<any>>(
606 self: Matcher<I, F, R, A, Pr, Ret, Args>
607) => [Pr] extends [never] ? [Args] extends [[]] ? (input: I) => Result.Result<Unify<A>, R>
608 : (...args: Args) => Result.Result<Unify<A>, R>
609 : Result.Result<Unify<A>, R> = (<I, R, RA, A>(self: Matcher<I, R, RA, A, I>) => {
610 if (self._tag === "ValueMatcher") {
611 return self.value
612 }
613
614 const len = self.cases.length
615 if (len === 1) {
616 const _case = self.cases[0]
617 return (...args: Array<any>): Result.Result<A, RA> => {
618 const input = self.select(...args)
619 if (_case._tag === "When" && _case.guard(input) === true) {
620 return Result.succeed(_case.evaluate(input, ...args))
621 } else if (_case._tag === "Not" && _case.guard(input) === false) {
622 return Result.succeed(_case.evaluate(input, ...args))
623 }
624 return Result.fail(input as any)
625 }
626 }
627 return (...args: Array<any>): Result.Result<A, RA> => {
628 const input = self.select(...args)
629 for (let i = 0; i < len; i++) {
630 const _case = self.cases[i]
631 if (_case._tag === "When" && _case.guard(input) === true) {
632 return Result.succeed(_case.evaluate(input, ...args))
633 } else if (_case._tag === "Not" && _case.guard(input) === false) {
634 return Result.succeed(_case.evaluate(input, ...args))
635 }
636 }
637
638 return Result.fail(input as any)
639 }
640 }) as any
641
642/** @internal */
643export const option: <I, F, R, A, Pr, Ret, Args extends Array<any>>(
644 self: Matcher<I, F, R, A, Pr, Ret, Args>
645) => [Pr] extends [never] ? [Args] extends [[]] ? (input: I) => Option.Option<Unify<A>>
646 : (...args: Args) => Option.Option<Unify<A>>
647 : Option.Option<Unify<A>> = (<I, A>(self: Matcher<I, any, any, A, I>) => {
648 const toResult = result(self)
649 if (Result.isResult(toResult)) {
650 return Result.match(toResult, {
651 onFailure: () => Option.none(),
652 onSuccess: Option.some
653 })
654 }
655 return (...args: Array<any>): Option.Option<A> =>
656 Result.match((toResult as any)(...args), {
657 onFailure: () => Option.none(),
658 onSuccess: Option.some as any
659 })
660 }) as any
661
662const getExhaustiveAbsurdErrorMessage = "effect/match/Match/exhaustive: absurd"
663
664/** @internal */
665export const exhaustive: <I, F, A, Pr, Ret, Args extends Array<any>>(
666 self: Matcher<I, F, never, A, Pr, Ret, Args>
667) => [Pr] extends [never] ? [Args] extends [[]] ? (u: I) => Unify<A> : (...args: Args) => Unify<A>
668 : Unify<A> = (<I, F, A>(
669 self: Matcher<I, F, never, A, I>
670 ) => {
671 const toResult = result(self as any)
672
673 if (Result.isResult(toResult)) {
674 if (Result.isSuccess(toResult)) {
675 return toResult.success
676 }
677
678 throw new Error(getExhaustiveAbsurdErrorMessage)
679 }
680
681 return (...args: Array<any>): A => {
682 // @ts-expect-error
683 const result = toResult(...args)
684
685 if (Result.isSuccess(result)) {
686 return result.success as any
687 }
688
689 throw new Error(getExhaustiveAbsurdErrorMessage)
690 }
691 }) as any