Skip to content

Commit

Permalink
Merge branch 'release/1.3.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
vankeisb committed Nov 23, 2020
2 parents 7e3ee23 + 05ef1a3 commit f129abc
Show file tree
Hide file tree
Showing 9 changed files with 349 additions and 7 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## v1.2.0 (03/11/2020)

#### closed

- [**closed**] Run .ts tests only [#36](https://github.com/vankeisb/react-tea-cup/pull/36)
- [**closed**] parallel tasks [#34](https://github.com/vankeisb/react-tea-cup/pull/34)

---

## v1.1.2 (19/10/2020)

#### closed
Expand Down
2 changes: 1 addition & 1 deletion core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tea-cup-core",
"version": "1.2.0",
"version": "1.3.0",
"description": "react-tea-cup core classes and utilities (Maybe etc)",
"author": "Rémi Van Keisbelck <[email protected]>",
"license": "MIT",
Expand Down
206 changes: 204 additions & 2 deletions core/src/TeaCup/Decode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
*
*/

import { Decode, Decoder } from './Decode';
import { err, ok } from './Result';
import { Decode, Decoder, DecoderObject } from './Decode';
import { err, ok, Result } from './Result';
import { just, nothing } from './Maybe';
const num = Decode.num;
const field = Decode.field;
Expand Down Expand Up @@ -154,8 +154,160 @@ test('map8', () => {
field('h', num),
).decodeValue(o),
).toEqual(ok(o));

});

describe('mapObject', () => {
type MyType = {
foo: string,
bar: number
};
const expected: MyType = {
foo: 'a foo',
bar: 13
}

test('simple', () => {
const value = { foo: 'a foo', bar: 13 }
expect(Decode.mapObject<MyType>({
foo: Decode.field('foo', Decode.str),
bar: Decode.field('bar', Decode.num)
}).decodeValue(value)).toEqual(ok(expected));
})


test('simpler', () => {
const value = { foo: 'a foo', bar: 13 }
expect(Decode.mapObject<MyType>(Decode.mapRequiredFields({
foo: Decode.str,
bar: Decode.num
})).decodeValue(value)).toEqual(ok(expected));
})


test('missing field', () => {
const value = { foo: 'a foo' }
expect(Decode.mapObject<MyType>({
foo: Decode.field('foo', Decode.str),
bar: Decode.field('bar', Decode.num)
}).decodeValue(value)).toEqual(err('path not found [bar] on {"foo":"a foo"}'));
})

test('superfluous field', () => {
const value = { foo: 'a foo', bar: 13, toto: true }
expect(Decode.mapObject<MyType>({
foo: Decode.field('foo', Decode.str),
bar: Decode.field('bar', Decode.num)
}).decodeValue(value)).toEqual(ok(expected));
})

test('optional field', () => {
type MyType2 = {
foo: string,
bar?: number
};
const expected: MyType2 = {
foo: 'a foo',
}

const value = { foo: 'a foo', toto: true }
expect(Decode.mapObject<MyType2>({
foo: Decode.field('foo', Decode.str),
bar: Decode.optionalField('bar', Decode.num)
}).decodeValue(value)).toEqual(ok(expected));

// the type system will compile fail this test:
// expect(Decode.mapObject<MyType2>({
// foo: Decode.field('foo', Decode.str),
// }).decodeValue(value)).toEqual(ok(expected));
})

test('simpler optional field', () => {
type MyType2 = {
foo: string,
bar?: number
};
const expected: MyType2 = {
foo: 'a foo',
}

const decoder: DecoderObject<MyType2> = {
...Decode.mapRequiredFields({
foo: Decode.str,
}),
...Decode.mapOptionalFields({
bar: Decode.num,
})
}

const value = { foo: 'a foo', toto: true }
expect(Decode.mapObject<MyType2>(decoder).decodeValue(value)).toEqual(ok(expected));
})
})

describe('mapArray', () => {
type MyType = [
string,
number
]
const expected: MyType = [
'a foo',
13
]

test('simple', () => {
type ValueType = [string, number]
const value: ValueType = ['a foo', 13]
expect(Decode.mapTuple<ValueType>([
Decode.str,
Decode.num
]).decodeValue(value)).toEqual(ok(expected));
})

test('type mismatch', () => {
type ValueType = [string, number]
const value: ValueType = ['a foo', 13]

// the type system will compile fail this test:
// expect(Decode.mapArray<ValueType>([
// Decode.str,
// Decode.str
// ]).decodeValue(value)).toEqual(err('ran into decoder error at [1] : value is not a string : 13'));

// the type system will let though to runtime:
expect(Decode.mapTuple([
Decode.str,
Decode.str
]).decodeValue(value)).toEqual(err('ran into decoder error at [1] : value is not a string : 13'));
})

test('missing item', () => {
type ValueType = [string, number]
// the type system will compile fail this test:
// const value: ValueType = ['a foo']

// the type system will let though to runtime:
const value = ['a foo']
expect(Decode.mapTuple([
Decode.str,
Decode.num
]).decodeValue(value)).toEqual(err('path not found [1] on [\"a foo\"]'));
})

test('too many items', () => {
type ValueType = [string, number]
// the type system will compile fail this test:
// const value: ValueType = ['a foo', 13, true]

// the type system will let though to runtime:
const value = ['a foo', 13, true]
expect(Decode.mapTuple([
Decode.str,
Decode.num
]).decodeValue(value)).toEqual(ok(expected));
})
})

test('andThen', () => {
type Stuff = { readonly tag: 'stuff1'; readonly foo: string } | { readonly tag: 'stuff2'; readonly bar: string };

Expand Down Expand Up @@ -291,4 +443,54 @@ describe('optional field', () => {
Decode.optionalField('gnu', Decode.num)).decodeValue(value)
).toEqual(ok(expected));
})

test('simpler optional field', () => {
type MyType2 = {
foo: string,
bar?: number
};
const expected: MyType2 = {
foo: 'a foo',
}

const value = { foo: 'a foo', toto: true }
expect(Decode.mapObject<MyType2>({
...Decode.mapRequiredFields({
foo: Decode.str,
}),
bar: Decode.optionalField('bar', Decode.num)
}).decodeValue(value)).toEqual(ok(expected));
})
});

describe('null types', () => {
test("non null value", () => {
const value = { foo: 'bar' };
const result: Result<string, string | null> = Decode.orNull(Decode.field('foo', Decode.str)).decodeValue(value)
expect(result).toEqual(ok('bar'));
})

test("null value", () => {
const value = { foo: null };
const result: Result<string, string | null> = (Decode.field('foo', Decode.orNull(Decode.str))).decodeValue(value)
expect(result).toEqual(ok(null));
})

test("typical use case", () => {
type MyType = {
gnu: number | null;
foo: string | null
}
const value = { foo: null, gnu: null };
const expected: MyType = {
foo: null,
gnu: null
};
expect(Decode.map2(
(foo, gnu) => { return { foo, gnu } },
Decode.field('foo', Decode.orNull(Decode.str)),
Decode.field('gnu', Decode.orNull(Decode.num)))
.decodeValue(value)
).toEqual(ok(expected));
})
})
84 changes: 83 additions & 1 deletion core/src/TeaCup/Decode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,14 @@ export class Decode {
});
}

/**
* Decoder for nullable types
* @param d the decoder to be used if the value is not null
*/
static orNull<T>(d: Decoder<T>): Decoder<T | null> {
return this.map(v => v.map<T | null>(v => v).withDefault(null), this.nullable(d));
}

/**
* Decoder for lists
* @param d the decoder for elements in the list
Expand Down Expand Up @@ -436,6 +444,64 @@ export class Decode {
);
}

/**
* Decoder for big objects, where map8() is not enough.
* @param dobject an object with decoders
*/
static mapObject<T>(dobject: DecoderObject<T>): Decoder<T> {
const keys = Object.keys(dobject) as Array<keyof typeof dobject>
const partialDecoder: Decoder<Partial<T>> = keys.reduce((dacc, key) => Decode.andThen(object => {
const propertyDecoder = getProperty(dobject, key);
return Decode.map(property => {
object[key] = property;
return object;
}, propertyDecoder);
}, dacc), Decode.succeed({} as Partial<T>))
return Decode.map(v => v as T, partialDecoder);
}

/**
* Convenience, map decoder object to another decoder object
* @param decoders an object with decoders
* @param fun the mapper function
*/
static mapFields<T, T2>(decoders: DecoderObject<T>, fun: DecoderObjectMapper<T, T2>): DecoderObject<T2> {
const keys = Object.keys(decoders) as Array<keyof typeof decoders>
const partial: Partial<DecoderObject<T2>> = keys.reduce((acc, key) => {
const propertyDecoder = getProperty(decoders, key);
const [key2, propertyDecoder2] = fun(key, propertyDecoder);
acc[key2] = propertyDecoder2;
return acc;
}, {} as Partial<DecoderObject<T2>>)
return partial as DecoderObject<T2>;
}

/**
* Convenience, map docoders to required field decoders
* @param decoders an object with decoders
*/
static mapRequiredFields<T>(decoders: DecoderObject<T>): DecoderObject<T> {
return this.mapFields(decoders, (k: keyof T, d: Decoder<T[keyof T]>) => [k, Decode.field(k as string, d)]);
}

/**
* Convenience, map decoders to optional field decoders
* @param decoders an object with decoders
*/
static mapOptionalFields<T>(decoders: DecoderObject<T>): DecoderObject<OptionalFields<T>> {
const mapper: DecoderObjectMapper<T, OptionalFields<T>> =
(k: keyof T, d: Decoder<T[keyof T]>) => [k, Decode.optionalField(k as string, d)]
return this.mapFields(decoders, mapper);
}

/**
* Decoder for fixed-length tuples.
* @param decoders an array with decoders
*/
static mapTuple<T extends any[]>(decoders: DecoderArray<T>): Decoder<T> {
return Decode.map(v => Object.values(v) as T, this.mapObject<T>(this.mapRequiredFields<T>(decoders)));
}

// Fancy Decoding

/**
Expand All @@ -455,7 +521,7 @@ export class Decode {

/**
* Decoder for null
* @param the result to yield in case the decoded value is null
* @param t the result to yield in case the decoded value is null
*/
static null<T>(t: T): Decoder<T> {
return new Decoder<T>((o: any) => {
Expand Down Expand Up @@ -506,3 +572,19 @@ export class Decode {
});
}
}


function getProperty<T, K extends keyof T>(o: T, key: K): T[K] {
return o[key];
}

export type DecoderObject<T> = Required<{ [P in keyof T]: Decoder<T[P]> }>
export type DecoderArray<A extends any[]> = Required<{ [P in keyof A]: A[P] extends A[number] ? Decoder<A[P]> : never }>

export type OptionalFields<T> = {
[P in keyof T]: (T[P] | undefined);
};

export type DecoderObjectMapper<T, T2> =
(k: keyof T, d: Decoder<T[keyof T]>) => [keyof T2, Decoder<T2[keyof T2]>]

4 changes: 4 additions & 0 deletions core/src/TeaCup/Either.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,16 @@ test('left', () => {
const e: Either<string, number> = left('yeah');
expect(e.isLeft()).toBe(true);
expect(e.isRight()).toBe(false);
expect(e.left.map(s => s + '!').withDefault('')).toBe('yeah!');
expect(e.right.withDefault(123)).toBe(123);
});

test('right', () => {
const e: Either<string, number> = right(123);
expect(e.isLeft()).toBe(false);
expect(e.isRight()).toBe(true);
expect(e.left.withDefault('!!!')).toBe('!!!');
expect(e.right.map(x => x + 1).withDefault(456)).toBe(124);
});

test('mapLeft', () => {
Expand Down
Loading

0 comments on commit f129abc

Please sign in to comment.