From 7d82676582a3248e60f70c58636f8b1c1e68d304 Mon Sep 17 00:00:00 2001 From: Simon Date: Sat, 7 Apr 2018 21:36:26 -0700 Subject: [PATCH 1/3] Added update function --- src/index.spec.ts | 45 +++++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 39 ++++++++++++++++++++++++++++++++++----- 2 files changed, 79 insertions(+), 5 deletions(-) diff --git a/src/index.spec.ts b/src/index.spec.ts index 6d8b2d9..54fa24f 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -279,3 +279,48 @@ describe('config object', () => { expect(A.a(1)).toEqual({ tag: 'a', val: 1 }); }); }); + +describe('update suite', () => { + const Paylod = unionize( + { + num: ofType(), + str: ofType(), + }, + { value: 'payload' }, + ); + + it('skips unmet cases', () => { + const num = Paylod.num(1); + expect(Paylod.update(num, { str: s => s + '?' })).toBe(num); + expect(Paylod.update({ str: s => s + '??' })(num)).toBe(num); + }); + + it('updates with met cases', () => { + const str = Paylod.str('hi'); + const exclaim = (s: string) => s + '!'; + const exclaimed = Paylod.str('hi!'); + expect(Paylod.update({ str: exclaim })(str)).toEqual(exclaimed); + expect(Paylod.update(str, { str: exclaim })).toEqual(exclaimed); + }); + + it('updates immutably by partial state', () => { + const Data = unionize({ + A: ofType<{ a: 'not used' }>(), + BC: ofType<{ b: number; c: string }>(), + }); + + const bc = Object.freeze(Data.BC({ b: 1, c: 'hi' })); + const expected = Data.BC({ b: 1, c: 'hi!' }); + expect(Data.update({ BC: ({ c }) => ({ c: c + '!' }) })(bc)).toEqual(expected); + expect(Data.update(bc, { BC: ({ c }) => ({ c: c + '!' }) })).toEqual(expected); + }); + + it('still updated even with value prop', () => { + const Data = unionize({ BC: ofType<{ b: number; c: string }>() }, { value: 'payload' }); + + const bc = Object.freeze(Data.BC({ b: 1, c: 'hi' })); + const expected = Data.BC({ b: 1, c: 'hi!' }); + expect(Data.update({ BC: ({ c }) => ({ c: c + '!' }) })(bc)).toEqual(expected); + expect(Data.update(bc, { BC: ({ c }) => ({ c: c + '!' }) })).toEqual(expected); + }); +}); diff --git a/src/index.ts b/src/index.ts index 0544721..c106c8d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ export type Unionized = { is: Predicates; as: Casts; match: Match; + update: Update; } & Creators; export type Creators = { @@ -30,6 +31,15 @@ export type Match = { (variant: Union, cases: MatchCases): A; }; +export type UpdateCases = Partial< + { [T in keyof Record]: (value: Record[T]) => Partial } +>; + +export type Update = { + (cases: UpdateCases): (variant: Union) => Union; + (variant: Union, cases: UpdateCases): Union; +}; + export type MultiValueVariants = { [T in keyof Record]: { [_ in TagProp]: T } & Record[T] }; @@ -55,8 +65,9 @@ export type NoDefaultRec = { * * @param record A record mapping tags to value types. The actual values of the record don't * matter; they're just used in the types of the resulting tagged union. See `ofType`. - * @param tagProp An optional custom name for the tag property of the union. - * @param valProp An optional custom name for the value property of the union. If not specified, + * @param config An optional config object. By default tag='tag' and payload is merged into object itself + * @param config.tag An optional custom name for the tag property of the union. + * @param config.value An optional custom name for the value property of the union. If not specified, * the value must be a dictionary type. */ @@ -75,6 +86,8 @@ export function unionize( export function unionize(record: Record, config?: { value?: string; tag?: string }) { const { value: valProp = undefined, tag: tagProp = 'tag' } = config || {}; + const payload = (variant: any) => (valProp ? variant[valProp] : variant); + const creators = {} as Creators; for (const tag in record) { creators[tag] = (value: any) => @@ -89,9 +102,7 @@ export function unionize(record: Record, config?: { value?: string; tag? function evalMatch(variant: any, cases: any): any { const k = variant[tagProp]; const handler = cases[k]; - return handler !== undefined - ? handler(valProp ? variant[valProp] : variant) - : cases.default(variant); + return handler !== undefined ? handler(payload(variant)) : cases.default(variant); } const match = (first: any, second?: any) => @@ -107,17 +118,35 @@ export function unionize(record: Record, config?: { value?: string; tag? }); } + const evalUpd = (variant: any, cases: any): any => { + const k: keyof Record = variant[tagProp]; + return k in cases + ? creators[k](immutableUpd(payload(variant), cases[k](payload(variant)))) + : variant; + }; + + const update = (first: any, second?: any) => + second ? evalUpd(first, second) : (variant: any) => evalUpd(variant, first); + return Object.assign( { is, as, match, + update, _Record: record, }, creators, ); } +// Should we merge objects or just replace them? +// was unable to find a better solution to that +const objType = Object.prototype.toString.call({}); +const isObject = (maybeObj: any) => Object.prototype.toString.call(maybeObj) === objType; +const immutableUpd = (old: any, updated: any) => + isObject(old) ? Object.assign({}, old, updated) : updated; + /** * Creates a pseudo-witness of a given type. That is, it pretends to return a value of * type `T` for any `T`, but it's really just returning `undefined`. This white lie From a0c03546c8d79e0762a32b6dc48442d3818ab76f Mon Sep 17 00:00:00 2001 From: Simon Date: Sun, 8 Apr 2018 11:03:29 -0700 Subject: [PATCH 2/3] improved typings for tag prop in config obj --- src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index c106c8d..ea93441 100644 --- a/src/index.ts +++ b/src/index.ts @@ -73,13 +73,13 @@ export type NoDefaultRec = { export function unionize< Record extends SingleValueRec, - TagProp extends string, - ValProp extends string + ValProp extends string, + TagProp extends string = 'tag' >( record: Record, config: { value: ValProp; tag?: TagProp }, ): Unionized>; -export function unionize( +export function unionize( record: Record, config?: { tag: TagProp }, ): Unionized>; From 115177ea1b88b1d1aa4d1be7de85c89175389a25 Mon Sep 17 00:00:00 2001 From: Simon Date: Sun, 8 Apr 2018 23:49:25 -0700 Subject: [PATCH 3/3] replaced update with transform for immutable updates/state transitions --- src/index.spec.ts | 58 ++++++++++++++++++++++++++--------------------- src/index.ts | 37 ++++++++++++------------------ 2 files changed, 46 insertions(+), 49 deletions(-) diff --git a/src/index.spec.ts b/src/index.spec.ts index 54fa24f..50dca6d 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -280,8 +280,8 @@ describe('config object', () => { }); }); -describe('update suite', () => { - const Paylod = unionize( +describe('transform with value prop', () => { + const Payload = unionize( { num: ofType(), str: ofType(), @@ -290,37 +290,43 @@ describe('update suite', () => { ); it('skips unmet cases', () => { - const num = Paylod.num(1); - expect(Paylod.update(num, { str: s => s + '?' })).toBe(num); - expect(Paylod.update({ str: s => s + '??' })(num)).toBe(num); + const num = Payload.num(1); + expect(Payload.transform(num, { str: s => Payload.num(s.length) })).toBe(num); + expect(Payload.transform({ str: s => Payload.num(s.length) })(num)).toBe(num); }); - it('updates with met cases', () => { - const str = Paylod.str('hi'); - const exclaim = (s: string) => s + '!'; - const exclaimed = Paylod.str('hi!'); - expect(Paylod.update({ str: exclaim })(str)).toEqual(exclaimed); - expect(Paylod.update(str, { str: exclaim })).toEqual(exclaimed); + it('transforms with met cases', () => { + const str = Payload.str('s'); + const expected = Payload.num(1); + expect(Payload.transform(str, { str: s => Payload.num(s.length) })).toEqual(expected); + expect(Payload.transform({ str: s => Payload.num(s.length) })(str)).toEqual(expected); }); - it('updates immutably by partial state', () => { - const Data = unionize({ - A: ofType<{ a: 'not used' }>(), - BC: ofType<{ b: number; c: string }>(), - }); + it('technically we allow an empty object for cases', () => { + const str = Payload.str('s'); + expect(Payload.transform(str, {})).toBe(str); + expect(Payload.transform({})(str)).toBe(str); + }); +}); - const bc = Object.freeze(Data.BC({ b: 1, c: 'hi' })); - const expected = Data.BC({ b: 1, c: 'hi!' }); - expect(Data.update({ BC: ({ c }) => ({ c: c + '!' }) })(bc)).toEqual(expected); - expect(Data.update(bc, { BC: ({ c }) => ({ c: c + '!' }) })).toEqual(expected); +describe('transform without value prop', () => { + const Data = unionize({ + num: ofType<{ n: number }>(), + str: ofType<{ s: string }>(), }); - it('still updated even with value prop', () => { - const Data = unionize({ BC: ofType<{ b: number; c: string }>() }, { value: 'payload' }); + it('Just all at once', () => { + const num = Data.num({ n: 1 }); + const str = Data.str({ s: 's' }); + + const strLen = ({ s }: { s: string }) => Data.num({ n: s.length }); + + // unmet + expect(Data.transform(num, { str: strLen })).toBe(num); + expect(Data.transform({ str: strLen })(num)).toBe(num); - const bc = Object.freeze(Data.BC({ b: 1, c: 'hi' })); - const expected = Data.BC({ b: 1, c: 'hi!' }); - expect(Data.update({ BC: ({ c }) => ({ c: c + '!' }) })(bc)).toEqual(expected); - expect(Data.update(bc, { BC: ({ c }) => ({ c: c + '!' }) })).toEqual(expected); + //met cases + expect(Data.transform(str, { str: strLen })).toEqual(num); + expect(Data.transform({ str: strLen })(str)).toEqual(num); }); }); diff --git a/src/index.ts b/src/index.ts index ea93441..473d057 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ export type Unionized = { is: Predicates; as: Casts; match: Match; - update: Update; + transform: Transform; } & Creators; export type Creators = { @@ -31,13 +31,13 @@ export type Match = { (variant: Union, cases: MatchCases): A; }; -export type UpdateCases = Partial< - { [T in keyof Record]: (value: Record[T]) => Partial } +export type TransformCases = Partial< + { [T in keyof Record]: (value: Record[T]) => Union } >; -export type Update = { - (cases: UpdateCases): (variant: Union) => Union; - (variant: Union, cases: UpdateCases): Union; +export type Transform = { + (cases: TransformCases): (variant: Union) => Union; + (variant: Union, cases: TransformCases): Union; }; export type MultiValueVariants = { @@ -65,7 +65,7 @@ export type NoDefaultRec = { * * @param record A record mapping tags to value types. The actual values of the record don't * matter; they're just used in the types of the resulting tagged union. See `ofType`. - * @param config An optional config object. By default tag='tag' and payload is merged into object itself + * @param config An optional config object. By default tag='tag' and value is merged into object itself * @param config.tag An optional custom name for the tag property of the union. * @param config.value An optional custom name for the value property of the union. If not specified, * the value must be a dictionary type. @@ -86,7 +86,7 @@ export function unionize(record: Record, config?: { value?: string; tag?: string }) { const { value: valProp = undefined, tag: tagProp = 'tag' } = config || {}; - const payload = (variant: any) => (valProp ? variant[valProp] : variant); + const getVal = (variant: any) => (valProp ? variant[valProp] : variant); const creators = {} as Creators; for (const tag in record) { @@ -102,7 +102,7 @@ export function unionize(record: Record, config?: { value?: string; tag? function evalMatch(variant: any, cases: any): any { const k = variant[tagProp]; const handler = cases[k]; - return handler !== undefined ? handler(payload(variant)) : cases.default(variant); + return handler !== undefined ? handler(getVal(variant)) : cases.default(variant); } const match = (first: any, second?: any) => @@ -118,35 +118,26 @@ export function unionize(record: Record, config?: { value?: string; tag? }); } - const evalUpd = (variant: any, cases: any): any => { + const evalTransform = (variant: any, cases: any): any => { const k: keyof Record = variant[tagProp]; - return k in cases - ? creators[k](immutableUpd(payload(variant), cases[k](payload(variant)))) - : variant; + return k in cases ? cases[k](getVal(variant)) : variant; }; - const update = (first: any, second?: any) => - second ? evalUpd(first, second) : (variant: any) => evalUpd(variant, first); + const transform = (first: any, second?: any) => + second ? evalTransform(first, second) : (variant: any) => evalTransform(variant, first); return Object.assign( { is, as, match, - update, + transform, _Record: record, }, creators, ); } -// Should we merge objects or just replace them? -// was unable to find a better solution to that -const objType = Object.prototype.toString.call({}); -const isObject = (maybeObj: any) => Object.prototype.toString.call(maybeObj) === objType; -const immutableUpd = (old: any, updated: any) => - isObject(old) ? Object.assign({}, old, updated) : updated; - /** * Creates a pseudo-witness of a given type. That is, it pretends to return a value of * type `T` for any `T`, but it's really just returning `undefined`. This white lie