Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: match handler parameter #267

Merged
merged 1 commit into from
Feb 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/core/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import type { DecoderMethods, EncoderMethods } from '../data/types/Controller.js'
import type { AssertString, ObjectValues, UnionToIntersection } from '../lib/utils.js'
import type { RecordController } from '../record/types/controller.js'
import type { GetTagProperty, RecordController, SomeTaggedRecord } from '../record/types/controller.js'
import type { SomeStoredRecord, StoredRecord } from '../record/types/StoredRecord.js'
import type { z } from 'zod'

export type OmitTag<T> = Omit<T, '_tag'>
// prettier-ignore
export type OmitWithTag2<TaggedRecord extends SomeTaggedRecord> =
Omit<TaggedRecord, GetTagProperty<TaggedRecord>>

export type OmitWithTag<T> = Omit<T, '_tag'>

export type ExtensionsBase = Record<string, unknown>

Expand Down
44 changes: 23 additions & 21 deletions src/match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Terminology:
- Handler
*/

import type { OmitTag } from './core/types.js'
import type { OmitWithTag2 } from './core/types.js'
import { inspect } from './lib/utils.js'
import type { GetTag, GetTagProperty, SomeRecord, SomeTaggedRecord } from './record/types/controller.js'
import { getTag } from './record/types/controller.js'
Expand All @@ -35,16 +35,15 @@ export interface DataMatcherDefinition {
}

// prettier-ignore
export function match<Tag extends SomeTag>(tag: Tag): ChainTagPreMatcher<Tag, never>
// prettier-ignore
export function match<AlgebraicDataType extends SomeTaggedRecord>(algebraicDataType: AlgebraicDataType): ChainPreMatcher<AlgebraicDataType, never>
// prettier-ignore
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
export function match <ADTOrTag extends SomeTag | SomeTaggedRecord>(input: ADTOrTag):
ADTOrTag extends string ? ChainTagPreMatcher<ADTOrTag, never> :
ADTOrTag extends SomeTaggedRecord ? ChainPreMatcher<ADTOrTag, never> :
never {
interface Match {
<Tag extends SomeTag>(tag: Tag) : ChainTagPreMatcher<Tag, never>
<AlgebraicDataType extends SomeTaggedRecord>(algebraicDataType: AlgebraicDataType) : ChainPreMatcher<AlgebraicDataType, never>
}

type MatchArgs = [input: SomeTag | SomeTaggedRecord]

export const match: Match = (...args: MatchArgs) => {
const input = args[0]
const elseBranch: { defined: boolean; value: unknown | ((data: object) => unknown) } = {
defined: false,
value: undefined,
Expand All @@ -54,7 +53,10 @@ export function match <ADTOrTag extends SomeTag | SomeTaggedRecord>(input: ADTOr

const execute = () => {
for (const matcher of matcherStack) {
if (typeof input === `string` && matcher.tag === input || typeof input === `object` && matcher.tag === getTag(input)) {
if (
(typeof input === `string` && matcher.tag === input) ||
(typeof input === `object` && matcher.tag === getTag(input))
) {
if (matcher._tag === `DataMatcherDefinition`) {
if (isMatch(input as SomeRecord, matcher.dataPattern)) {
return matcher.handler(input as SomeRecord)
Expand All @@ -65,9 +67,7 @@ export function match <ADTOrTag extends SomeTag | SomeTaggedRecord>(input: ADTOr
}
}
if (elseBranch.defined) {
return typeof elseBranch.value === `function`
? (elseBranch.value(input) as unknown)
: elseBranch.value
return typeof elseBranch.value === `function` ? (elseBranch.value(input) as unknown) : elseBranch.value
}
throw new Error(
`No matcher matched on the given data. This should be impossible. Are you sure the runtime is not different than the static types? Please report a bug at https://jasonkuhrt/alge. The given data was:\n${inspect(
Expand Down Expand Up @@ -133,15 +133,17 @@ export function match <ADTOrTag extends SomeTag | SomeTaggedRecord>(input: ADTOr
return proxy as any
}

type PickRecordHavingTag<Tag extends string, ADT extends SomeTaggedRecord> = ADT extends { _tag: Tag }
? ADT
: never
// prettier-ignore
type PickWithTag<Tag extends string, ADT extends SomeTaggedRecord> =
ADT extends SomeTaggedRecord<Tag>
? ADT
: never

//prettier-ignore
type ChainPreMatcher<ADT extends SomeTaggedRecord, Result> = {
[Tag in GetTag<ADT>]:
(<ThisResult extends unknown, Pattern extends Partial<OmitTag<PickRecordHavingTag<Tag, ADT>>>>(dataPattern: Pattern, handler: (data: Pattern & PickRecordHavingTag<Tag, ADT>, test:Pattern) => ThisResult) => ChainPostMatcher<ADT, never, ThisResult | Result>) &
(<ThisResult extends unknown>(handler: (data: PickRecordHavingTag<Tag, ADT>) => ThisResult) => ChainPostMatcher<ADT, Tag, ThisResult | Result>)
(<ThisResult extends unknown, Pattern extends Partial<OmitWithTag2<PickWithTag<Tag, ADT>>>>(dataPattern: Pattern, handler: (data: Pattern & PickWithTag<Tag, ADT>, test:Pattern) => ThisResult) => ChainPostMatcher<ADT, never, ThisResult | Result>) &
(<ThisResult extends unknown>(handler: (data: PickWithTag<Tag, ADT>) => ThisResult) => ChainPostMatcher<ADT, Tag, ThisResult | Result>)
}

/**
Expand All @@ -153,8 +155,8 @@ type ChainPreMatcher<ADT extends SomeTaggedRecord, Result> = {
type ChainPostMatcher<ADT extends SomeTaggedRecord, TagsPreviouslyMatched extends string, Result> = {
[Tag in Exclude<GetTag<ADT>, TagsPreviouslyMatched>]:
(
(<ThisResult extends unknown, Pattern extends Partial<OmitTag<PickRecordHavingTag<Tag, ADT>>>>(dataPattern: Pattern, handler: (data: Pattern & PickRecordHavingTag<Tag, ADT>) => ThisResult) => ChainPostMatcher<ADT, TagsPreviouslyMatched, '__init__' extends Result ? ThisResult : ThisResult | Result>) &
(<ThisResult extends unknown>(handler: (data: PickRecordHavingTag<Tag, ADT>) => ThisResult) => ChainPostMatcher<ADT, Tag|TagsPreviouslyMatched, ThisResult | Result>)
(<ThisResult extends unknown, Pattern extends Partial<OmitWithTag2<PickWithTag<Tag, ADT>>>>(dataPattern: Pattern, handler: (data: Pattern & PickWithTag<Tag, ADT>) => ThisResult) => ChainPostMatcher<ADT, TagsPreviouslyMatched, '__init__' extends Result ? ThisResult : ThisResult | Result>) &
(<ThisResult extends unknown>(handler: (data: PickWithTag<Tag, ADT>) => ThisResult) => ChainPostMatcher<ADT, Tag|TagsPreviouslyMatched, ThisResult | Result>)
)
// ^[1] ^[1]
} & (
Expand Down
22 changes: 11 additions & 11 deletions src/record/types/controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { SomeSchema, SomeSchemaDef } from '../../core/internal.js'
import type { Encoder, OmitTag, SomeName, StoredRecords } from '../../core/types.js'
import type { Encoder, OmitWithTag, SomeName, StoredRecords } from '../../core/types.js'
import type { OmitRequired, Rest } from '../../lib/utils.js'
import type { z } from '../../lib/z/index.js'
import type {
Expand Down Expand Up @@ -43,16 +43,16 @@ export type GetTagProperty<TaggedRecord extends SomeTaggedRecord> =
TaggedRecord extends { kind : string } ? '_tag' :
never

export type SomeTaggedRecord =
| SomeRecord<'__typename'>
| SomeRecord<'_tag'>
| SomeRecord<'_type'>
| SomeRecord<'_kind'>
| SomeRecord<'type'>
| SomeRecord<'kind'>
export type SomeTaggedRecord<Tag extends string = string> =
| SomeRecord<'__typename', Tag>
| SomeRecord<'_tag', Tag>
| SomeRecord<'_type', Tag>
| SomeRecord<'_kind', Tag>
| SomeRecord<'type', Tag>
| SomeRecord<'kind', Tag>

export type SomeRecord<TagPropertyName extends string = '_tag'> = {
[PropertyName in TagPropertyName]: string
export type SomeRecord<TagPropertyName extends string = '_tag', Tag extends string = string> = {
[PropertyName in TagPropertyName]: Tag
}

export type SomeRecordInternal = {
Expand Down Expand Up @@ -146,7 +146,7 @@ export type RecordController<Rs extends StoredRecords, R extends SomeStoredRecor
*
* @throws If zod schema violated: bad types, failed validation, throw from a transformer.
*/
update(record: StoredRecord.GetType<R>, changes: Any.Compute<Partial<OmitTag<StoredRecord.GetType<R>>>>): StoredRecord.GetType<R>
update(record: StoredRecord.GetType<R>, changes: Any.Compute<Partial<OmitWithTag<StoredRecord.GetType<R>>>>): StoredRecord.GetType<R>
/**
* Decoders for this record. Decoders are used to transform other representations of your record back into a record instance.
*/
Expand Down
65 changes: 27 additions & 38 deletions tests/match/tag-match.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,61 +2,50 @@
import { Alge } from '../../src/index.js'
import { expectType } from 'tsd'
import { describe, expect, it } from 'vitest'
import { I } from 'ts-toolbelt'

type B = 'B'
type A = 'A'
type Tag = A | B
const tagA = 'A' as Tag
const tagB = 'B' as Tag

const cast = <T>(): T => 0 as any

describe('accepted tag properties', () => {
it(`_tag`, () => {
const adt = Math.random() > 0.5 ? { _tag: 'A' as const } : { _tag: 'B' as const }
type adt = { _tag: 'A'; a: 0 } | { _tag: 'B'; b: '' }
// prettier-ignore
const adts = [{ _tag: 'A', a: 0 }, { _tag: 'B', b: '' }] as const //satisfies [adt,...adt[]]
const adt = adts[Math.floor(Math.random() * adts.length)]!
const builder = Alge.match(adt)
expect(typeof builder.A).toBe(`function`)
expect(typeof builder.B).toBe(`function`)
expectType<(handler: () => unknown) => any>(builder.A)
expectType<(handler: () => unknown) => any>(builder.B)
expectType<(handler: (data: { b: '' }) => unknown) => any>(builder.B)
const builder2 = builder.A(() => 'a')
// @ts-expect-error tag is omitted
builder2.B({ _tag: 'B' }, () => {})
})
it(`__typename`, () => {
const adt = Math.random() > 0.5 ? { __typename: 'A' as const } : { __typename: 'B' as const }
const builder = Alge.match(adt)
expect(typeof builder.A).toBe(`function`)
expect(typeof builder.B).toBe(`function`)
expectType<(handler: () => unknown) => any>(builder.A)
expectType<(handler: () => unknown) => any>(builder.B)
})
it(`_type`, () => {
const adt = Math.random() > 0.5 ? { _type: 'A' as const } : { _type: 'B' as const }
const builder = Alge.match(adt)
expect(typeof builder.A).toBe(`function`)
expect(typeof builder.B).toBe(`function`)
expectType<(handler: () => unknown) => any>(builder.A)
expectType<(handler: () => unknown) => any>(builder.B)
})
it(`_kind`, () => {
const adt = Math.random() > 0.5 ? { _kind: 'A' as const } : { _kind: 'B' as const }
type adt = { __typename: 'A'; a: 0 } | { __typename: 'B'; b: '' }
// prettier-ignore
const adts = [{ __typename: 'A', a: 0 }, { __typename: 'B', b: '' }] as const //satisfies [adt,...adt[]]
const adt = adts[Math.floor(Math.random() * adts.length)]!
const builder = Alge.match(adt)
expect(typeof builder.A).toBe(`function`)
expect(typeof builder.B).toBe(`function`)
expectType<(handler: () => unknown) => any>(builder.A)
expectType<(handler: () => unknown) => any>(builder.B)
})
it(`type`, () => {
const adt = Math.random() > 0.5 ? { type: 'A' as const } : { type: 'B' as const }
const builder = Alge.match(adt)
expect(typeof builder.A).toBe(`function`)
expect(typeof builder.B).toBe(`function`)
expectType<(handler: () => unknown) => any>(builder.A)
expectType<(handler: () => unknown) => any>(builder.B)
})
it(`kind`, () => {
const adt = Math.random() > 0.5 ? { kind: 'A' as const } : { kind: 'B' as const }
const builder = Alge.match(adt)
expect(typeof builder.A).toBe(`function`)
expect(typeof builder.B).toBe(`function`)
expectType<(handler: () => unknown) => any>(builder.A)
expectType<(handler: () => unknown) => any>(builder.B)
expectType<(handler: (data: { a: 0 }) => unknown) => any>(builder.A)
expectType<(handler: (data: { b: '' }) => unknown) => any>(builder.B)
const builder2 = builder.A(() => 'a')
expect(typeof builder2.B).toBe(`function`)
// @ts-expect-error tag is omitted
builder2.B({ __typename: 'B' }, () => {})
builder2.B((data) => {
expectType<typeof data>(cast<{ __typename: 'B'; b: '' }>())
})
// TODO make this possible but having Match be immutable
// // @ts-expect-error tag is omitted
// builder2.B({ __typename: 'B' }, () => {})
})
})

Expand Down