Skip to content

Commit

Permalink
Fixed typos and code style in README.md (#10)
Browse files Browse the repository at this point in the history
* Fixed typos and code style in README.md
 - err handling in map
- validation mess as func
 - remove map impl, where Result map can be used
  - minor stylistic chages
  - fixed some minor unit tests issues

  courtesy of https://github.com/whiteand

* added unit test

* update version

Co-authored-by: Art Deineka <@darkest_ruby>
  • Loading branch information
venil7 authored Apr 10, 2020
1 parent 7de2081 commit f73c1a3
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 72 deletions.
36 changes: 22 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ Below is a list of basic decoders supplied with `json-decoder`:
- `undefinedDecoder` - decodes an `undefined` value:

```TypeScript
const result: Result<null> = nullDecoder.decode(undefined); //Ok(undefined);
const result: Result<null> = boolDecoder.decode(null); //Err("undefined expected");
const result: Result<undefined> = undefinedDecoder.decode(undefined); //Ok(undefined);
const result: Result<undefined> = boolDecoder.decode(null); //Err("undefined expected");
```

- `arrayDecoder<T>(decoder: Decoder<T>)` - decodes an array, requires one parameter of array item decoder:
Expand All @@ -69,14 +69,14 @@ Below is a list of basic decoders supplied with `json-decoder`:

```TypeScript
type Pet = {name: string, age: number};
const petDecoder = objectDecoder<Person>({
const petDecoder = objectDecoder<Pet>({
name: stringDecoder,
age: numberDecoder,
});
const result: Result<Pet> = petDecoder.decode({name: "Varia", age: 0.5}); //Ok({name: "Varia", age: 0.5});
const result: Result<Pet> = petDecoder.decode({name: "Varia", type: "cat"}); //Err("name: string expected");

const petDecoder = objectDecoder<Person>({
const petDecoder = objectDecoder<Pet>({
name: stringDecoder,
type: stringDecoder, //<-- error: field type is not defined in Pet
});
Expand Down Expand Up @@ -138,14 +138,14 @@ Each decoder has the following methods:
```TypeScript
const getPet = async (): Promise<Pet> => {
const result = await fetch("http://some.pet.api/cat/1");
const pet:Pet = await petDecoder.decodeAsync(await result.json());
const pet: Pet = await petDecoder.decodeAsync(await result.json());
return pet;
};
```

- `map(func: (t:T) => T2) : Decoder<T2>` - each decoder is a [functor](https://wiki.haskell.org/Functor). `Map` allows you to apply a function to an underlying deocoder value, provided that decoding succeeded. Map accepts a function of type `(t:T) -> T2`, where `T` is a type of decoder (and underlying value), and `T2` is a type of resulting decoder.
- `map(func: (t: T) => T2): Decoder<T2>` - each decoder is a [functor](https://wiki.haskell.org/Functor). `Map` allows you to apply a function to an underlying decoder value, provided that decoding succeeded. Map accepts a function of type `(t: T) -> T2`, where `T` is a type of decoder (and underlying value), and `T2` is a type of resulting decoder.

- `then(bindFunc: (t:T) => Decoder<T2>): Decoder<T2>` - allows for [monadic](https://wiki.haskell.org/Monad) chaining of decoders. Takes a function, that returns a `Decoder<T2>`, and returns a `Decoder<T2>`
- `then(bindFunc: (t: T) => Decoder<T2>): Decoder<T2>` - allows for [monadic](https://wiki.haskell.org/Monad) chaining of decoders. Takes a function, that returns a `Decoder<T2>`, and returns a `Decoder<T2>`

### Custom decoder

Expand All @@ -154,13 +154,13 @@ Each decoder has the following methods:
Decoding can either succeed or fail, to denote that `json-decoder` has [ADT](https://en.wikipedia.org/wiki/Algebraic_data_type) type `Result<T>`, which can take two forms:

- `Ok<T>` - carries a succesfull decoding result of type `T`, use `.value` to access value
- `Err<T>` - carries an unsuccesfull decodign result of type `T`, use `.message` to access error message
- `Err<T>` - carries an unsuccesfull decoding result of type `T`, use `.message` to access error message

`Result` also has functorial `map` function that allows to apply a function to a value, provided that it exists

```TypeScript
const r:Result<string> = Ok("cat").map(s => s.toUpperCase); //Ok("CAT")
const e:Result<string> = Err("some error").map(s => s.toUpperCase); //Err("some error")
const r: Result<string> = Ok("cat").map(s => s.toUpperCase()); //Ok("CAT")
const e: Result<string> = Err("some error").map(s => s.toUpperCase()); //Err("some error")
```

It is possible to pattern-match (using poor man's pattern matching provided by TypeScript) to determite the type of `Result`
Expand All @@ -184,24 +184,32 @@ TBC

## Validation

`JSON` only exposes an handful of types: `string`, `number`, `null`, `boolean`, `array` and `object`. There's no way t enforce special kind of validation on ny of above types using just `JSON`. `json-decoder` allows to validate values against a predicate.
`JSON` only exposes an handful of types: `string`, `number`, `null`, `boolean`, `array` and `object`. There's no way to enforce special kind of validation on any of above types using just `JSON`. `json-decoder` allows to validate values against a predicate.

#### Example: `integerDecoder` - only decodes an integer and fails on a float value

```TypeScript
const integerDecoder : Decoder<number> = numberDecoder.validate(n => Math.floor(n) === n, "not an integer");
const integerDecoder: Decoder<number> = numberDecoder.validate(n => Math.floor(n) === n, "not an integer");
const integer = integerDecoder.decode(123); //Ok(123)
const float = integerDecoder.decode(123.45); //Err("not an integer")

```

#### Example: `emailDecoder` - only decodes a string that matches email regex, fails otherwise

```TypeScript
const emailDecoder : Decoder<number> = stringDecoder.validate(/^\S+@\S+$/.test, "not an email");
const emailDecoder: Decoder<number> = stringDecoder.validate(/^\S+@\S+$/.test, "not an email");
const email = emailDecoder.decode("[email protected]"); //Ok("[email protected]")
const notEmail = emailDecoder.decode("joe"); //Err("not an email")
```

Also `decoder.validate` can take function as a second parameter. It should have such type: `(value: T) => string`.

#### Example: `emailDecoder` - only decodes a string that matches email regex, fails otherwise

```TypeScript
const emailDecoder: Decoder<number> = stringDecoder.validate(/^\S+@\S+$/.test, (invalidEmail) => `${invalidEmail} not an email`);
const email = emailDecoder.decode("[email protected]"); //Ok("[email protected]")
const notEmail = emailDecoder.decode("joe"); //Err("joe is not an email")
```

## Contributions are welcome
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "json-decoder",
"version": "1.3.3",
"version": "1.3.4",
"description": "Lightweight, lightning-fast, type safe JSON decoder for TypeScript",
"main": "dist/decoder.js",
"typings": "dist/decoder.d.ts",
Expand Down
41 changes: 31 additions & 10 deletions src/__tests__/decoder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import {
Err,
OK,
Ok,
valueDecoder
valueDecoder,
undefinedDecoder,
exactDecoder,
} from "../decoder";

test("string decoder", async () => {
Expand Down Expand Up @@ -40,14 +42,15 @@ test("null decoder", async () => {
});

test("undefined decoder", async () => {
const val: null = null;
const result = await nullDecoder.decodeAsync(val);
const val: undefined = undefined;
const result = await undefinedDecoder.decodeAsync(val);
expect(result).toBe(val);
});

test("map decoder", async () => {
const val = "12";
const result = await stringDecoder.map(parseInt).decodeAsync(val);
const stringToNumberDecoder = stringDecoder.map(parseInt);
const result = await stringToNumberDecoder.decodeAsync(val);
expect(result).toBe(12);
});

Expand All @@ -64,14 +67,14 @@ test("all of decoders", async () => {
const val = "12.0";
const result = await allOfDecoders(
stringDecoder.map(parseFloat),
numberDecoder.map(x => x * 2)
numberDecoder.map((x) => x * 2)
).decodeAsync(val);
expect(result).toBe(24.0);
});

test("failing map returns Err", async () => {
const val = "cat";
const decoder = stringDecoder.map(x => {
const decoder = stringDecoder.map((x) => {
throw new Error("mapping fault");
return "hello"; // this is ti satisfy type inference
});
Expand All @@ -89,6 +92,18 @@ test("failing validation returns Err", async () => {
expect((result as Err<string>).message).toBe("not a cat");
});

test("failing validation with functional explanation returns Err", async () => {
const val = "dog";
const validate = (s: string) => s === "cat";
const decoder = stringDecoder.validate(
validate,
(value) => value + " not a cat"
);
const result = decoder.decode(val);
expect(result.type).toBe(ERR);
expect((result as Err<string>).message).toBe("dog not a cat");
});

test("succesfull validation returns Ok", async () => {
const val = 123;
const isInteger = (n: number) => Math.floor(n) === n;
Expand All @@ -102,7 +117,7 @@ test("object decoder (success)", async () => {
const val: unknown = { name: "peter", age: 26 };
const testDecoder = objectDecoder<Person>({
name: stringDecoder,
age: numberDecoder
age: numberDecoder,
});
const result = await testDecoder.decodeAsync(val);
expect(result).toStrictEqual(val as Person);
Expand All @@ -111,7 +126,7 @@ test("object decoder (success)", async () => {
test("object decoder (null)", async () => {
const testDecoder = objectDecoder({
name: stringDecoder,
age: numberDecoder
age: numberDecoder,
});
const result = testDecoder.decode(null);
expect((result as Err<null>).message).toEqual("expected object, got null");
Expand All @@ -124,18 +139,24 @@ test("any decoder", async () => {
expect(result).toStrictEqual(val);
});

test("exact decoder", async () => {
const val: "EXACT-VALUE" = "EXACT-VALUE";
const result = await exactDecoder(val).decodeAsync(val);
expect(result).toEqual(val);
});

test("mapping Ok result", async () => {
const val: string = "cat";
const result_: Result<string> = stringDecoder.decode(val);
const result = result_.map(x => x.toUpperCase());
const result = result_.map((x) => x.toUpperCase());
expect(result.type).toBe(OK);
expect((result as Ok<string>).value).toBe("CAT");
});

test("mapping Err result", async () => {
const val = 123;
const result_ = stringDecoder.decode(val);
const result = result_.map(x => x.toUpperCase());
const result = result_.map((x) => x.toUpperCase());
expect(result.type).toBe(ERR);
expect((result as Err<string>).message).toBe("expected string, got number");
});
Expand Down
78 changes: 32 additions & 46 deletions src/decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,29 @@ export type Result<T> = Ok<T> | Err<T>;
export const ok = <T>(value: T): Result<T> => ({
type: OK,
value,
map: func => ok(func(value))
map: (func) => {
try {
return ok(func(value));
} catch (error) {
return err(error.message);
}
},
});
export const err = <T>(message: string): Result<T> => ({
type: ERR,
message,
map: func => err(message)
map: () => err(message),
});

export type Decoder<T> = {
decode: (a: unknown) => Result<T>;
decodeAsync: (a: unknown) => Promise<T>;
map: <T2>(func: (t: T) => T2) => Decoder<T2>;
then: <T2>(nextDecoder: Decoder<T2>) => Decoder<T2>;
validate: (func: (t: T) => boolean, errMessage?: string) => Decoder<T>;
validate: (
func: (t: T) => boolean,
errMessage?: string | ((t: T) => string)
) => Decoder<T>;
};

export type DecoderType<D> = D extends Decoder<infer T> ? T : never;
Expand All @@ -37,79 +46,65 @@ export type DecoderArrayType<DD> = DecoderType<ArrayType<DD>>;

export const decoder = <T>(decode: (a: unknown) => Result<T>): Decoder<T> => ({
decode,
decodeAsync: (a: unknown) =>
new Promise<T>((accept, reject) => {
decodeAsync: (a) =>
new Promise<T>((resolve, reject) => {
const res = decode(a);
switch (res.type) {
case OK:
return accept(res.value);
return resolve(res.value);
case ERR:
return reject(new Error(res.message));
}
}),
map: <T2>(func: (t: T) => T2): Decoder<T2> =>
decoder<T2>((b: unknown) => {
const res = decode(b);
switch (res.type) {
case OK: {
try {
return ok(func(res.value));
} catch (error) {
return err(error.message);
}
}
case ERR:
return (res as unknown) as Err<T2>;
}
}),
decoder<T2>((b: unknown) => decode(b).map(func)),
then: <T2>(nextDecoder: Decoder<T2>): Decoder<T2> =>
allOfDecoders(decoder(decode), nextDecoder),
validate: (
func: (t: T) => boolean,
errMessage: string = "validation failed"
): Decoder<T> =>
validate: (func, errMessage = "validation failed"): Decoder<T> =>
decoder(decode).map<T>((t: T) => {
if (func(t)) {
return t;
} else {
throw new Error(errMessage);
throw new Error(
typeof errMessage === "function" ? errMessage(t) : errMessage
);
}
})
}),
});

type ArrayDecoder<T> = Decoder<T[]>;
type DecoderMap<T> = { [K in keyof T]: Decoder<T[K]> };

export const stringDecoder: Decoder<string> = decoder((a: unknown) =>
export const stringDecoder: Decoder<string> = decoder((a) =>
typeof a === "string"
? ok<string>(a as string)
: err(`expected string, got ${typeof a}`)
);

export const numberDecoder: Decoder<number> = decoder((a: unknown) =>
export const numberDecoder: Decoder<number> = decoder((a) =>
typeof a === "number"
? ok<number>(a as number)
: err(`expected number, got ${typeof a}`)
);

export const boolDecoder: Decoder<boolean> = decoder((a: unknown) =>
export const boolDecoder: Decoder<boolean> = decoder((a) =>
typeof a === "boolean"
? ok<boolean>(a as boolean)
: err(`expected boolean, got ${typeof a}`)
);

export const nullDecoder: Decoder<null> = decoder((a: unknown) =>
export const nullDecoder: Decoder<null> = decoder((a) =>
a === null ? ok<null>(null) : err(`expected null, got ${typeof a}`)
);

export const undefinedDecoder: Decoder<undefined> = decoder((a: unknown) =>
export const undefinedDecoder: Decoder<undefined> = decoder((a) =>
a === undefined
? ok<undefined>(undefined)
: err(`expected undefined, got ${typeof a}`)
);

export const arrayDecoder = <T>(itemDecoder: Decoder<T>): ArrayDecoder<T> =>
decoder((a: unknown) => {
decoder((a) => {
if (Array.isArray(a)) {
const res: T[] = [];
for (const [index, item] of a.entries()) {
Expand All @@ -133,12 +128,7 @@ export const oneOfDecoders = <T>(
decoder((a: unknown) => {
for (const decoderTry of decoders) {
const result = decoderTry.decode(a);
switch (result.type) {
case OK:
return ok(result.value);
case ERR:
continue;
}
if (result.type === OK) return ok(result.value);
}
return err(`one of: none of decoders match`);
}) as ArrayType<typeof decoders>;
Expand All @@ -159,14 +149,10 @@ export const allOfDecoders = <
): Decoder<R> =>
decoder((a: unknown) => {
return decoders.reduce(
(result: Result<R>, decoderNext: Decoder<unknown>) => {
switch (result.type) {
case OK:
return decoderNext.decode(result.value) as Result<R>;
default:
return err<R>(result.message);
}
},
(result: Result<R>, decoderNext: Decoder<unknown>) =>
result.type === OK
? (decoderNext.decode(result.value) as Result<R>)
: err<R>(result.message),
ok<R>(a as R)
);
});
Expand Down

0 comments on commit f73c1a3

Please sign in to comment.