Skip to content

Commit

Permalink
Avoid using mixeds
Browse files Browse the repository at this point in the history
They're causing too many type incompatibility issues when decoders are
used.  This happened after decoders fully Flow Strict compatible.  This
new approach still uses "any"'s, but deliberately allows them.
  • Loading branch information
nvie committed May 28, 2018
1 parent 1ecb742 commit e187b54
Show file tree
Hide file tree
Showing 15 changed files with 59 additions and 52 deletions.
6 changes: 3 additions & 3 deletions src/array.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
import { annotate } from 'debrief';
import { Err, Ok } from 'lemons';

import type { DecodeResult, Decoder } from './types';
import type { DecodeResult, Decoder, anything } from './types';
import { compose } from './utils';

/**
* Like a "Plain Old JavaScript Object", but for arrays: "Plain Old JavaScript
* Array" ^_^
*/
export const poja: Decoder<Array<mixed>> = (blob: mixed) => {
export const poja: Decoder<Array<mixed>> = (blob: anything) => {
if (!Array.isArray(blob)) {
return Err(annotate(blob, 'Must be an array'));
}
Expand All @@ -23,7 +23,7 @@ export const poja: Decoder<Array<mixed>> = (blob: mixed) => {
* encountered; or
* - a new Ok with an array of all unwrapped Ok'ed values
*/
function all<T>(iterable: Array<DecodeResult<T>>, blobs: Array<mixed>): DecodeResult<Array<T>> {
function all<T>(iterable: Array<DecodeResult<T>>, blobs: anything): DecodeResult<Array<T>> {
const results: Array<T> = [];
let index = 0;
for (const result of iterable) {
Expand Down
6 changes: 3 additions & 3 deletions src/boolean.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,20 @@ import { annotate } from 'debrief';
import { Err, Ok } from 'lemons';

import { number } from './number';
import type { Decoder } from './types';
import type { Decoder, anything } from './types';
import { map } from './utils';

/**
* Decoder that only returns Ok for boolean inputs. Err otherwise.
*/
export const boolean: Decoder<boolean> = (blob: mixed) => {
export const boolean: Decoder<boolean> = (blob: anything) => {
return typeof blob === 'boolean' ? Ok(blob) : Err(annotate(blob, 'Must be boolean'));
};

/**
* Decoder that returns true for all truthy values, and false otherwise. Never fails.
*/
export const truthy: Decoder<boolean> = (blob: mixed) => {
export const truthy: Decoder<boolean> = (blob: anything) => {
return Ok(!!blob);
};

Expand Down
17 changes: 7 additions & 10 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,35 @@
import { annotate } from 'debrief';
import { Err, Ok } from 'lemons';

import type { Decoder } from './types';
import type { Decoder, anything } from './types';

/**
* Decoder that only returns Ok for `null` inputs. Err otherwise.
*/
export const null_: Decoder<null> = (blob: mixed) => (blob === null ? Ok(blob) : Err(annotate(blob, 'Must be null')));
export const null_: Decoder<null> = (blob: anything) =>
blob === null ? Ok(blob) : Err(annotate(blob, 'Must be null'));

/**
* Decoder that only returns Ok for `undefined` inputs. Err otherwise.
*/
export const undefined_: Decoder<void> = (blob: mixed) =>
export const undefined_: Decoder<void> = (blob: anything) =>
blob === undefined ? Ok(blob) : Err(annotate(blob, 'Must be undefined'));

/**
* Decoder that only returns Ok for the given value constant. Err otherwise.
*/
export function constant<T>(value: T): Decoder<T> {
return (blob: mixed) =>
blob === value
? // $FlowFixMe - Potentially unsafe casting!
Ok(blob)
: Err(annotate(blob, `Must be constant ${String(value)}`));
return (blob: anything) => (blob === value ? Ok(blob) : Err(annotate(blob, `Must be constant ${String(value)}`)));
}

/**
* Decoder that always returns Ok for the given hardcoded value, no matter what the input.
*/
export function hardcoded<T>(value: T): Decoder<T> {
return (_: mixed) => Ok(value);
return (_: anything) => Ok(value);
}

/**
* Decoder that always returns Ok for the given hardcoded value, no matter what the input.
*/
export const mixed: Decoder<mixed> = (blob: mixed) => Ok((blob: mixed));
export const mixed: Decoder<mixed> = (blob: anything) => Ok((blob: mixed));
4 changes: 2 additions & 2 deletions src/dispatch.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @flow strict

import type { Decoder } from './types';
import type { Decoder, anything } from './types';

/**
* Given a function that takes a base decoder whose value will be passed to
Expand Down Expand Up @@ -41,7 +41,7 @@ import type { Decoder } from './types';
* );
*/
export function dispatch<T, V>(base: Decoder<T>, next: T => Decoder<V>): Decoder<V> {
return (blob: mixed) =>
return (blob: anything) =>
// We'll dispatch on this value
base(blob).andThen(value =>
// Now dispatch on the value by passing in T, and then invoking
Expand Down
4 changes: 2 additions & 2 deletions src/either.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { annotate, indent } from 'debrief';
import { Err, Ok } from 'lemons';

import type { Decoder } from './types';
import type { Decoder, anything } from './types';

/**
* Indents and adds a dash in front of this (potentially multiline) string.
Expand All @@ -14,7 +14,7 @@ function itemize(s: string = ''): string {
}

export function either<T1, T2>(d1: Decoder<T1>, d2: Decoder<T2>): Decoder<T1 | T2> {
return (blob: mixed) =>
return (blob: anything) =>
d1(blob).dispatch(
value1 => Ok(value1),
err1 =>
Expand Down
4 changes: 2 additions & 2 deletions src/fail.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
import { annotate } from 'debrief';
import { Err } from 'lemons';

import type { Decoder } from './types';
import type { Decoder, anything } from './types';

/**
* Decoder that always fails with the given error message, no matter what the input.
*/
export function fail<T>(msg: string): Decoder<T> {
return (blob: mixed) => Err(annotate(blob, msg));
return (blob: anything) => Err(annotate(blob, msg));
}
4 changes: 2 additions & 2 deletions src/guard.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

import { serialize } from 'debrief';

import type { Decoder, Guard } from './types';
import type { Decoder, Guard, anything } from './types';

export function guard<T>(decoder: Decoder<T>): Guard<T> {
return (blob: mixed) =>
return (blob: anything) =>
decoder(blob)
.mapError(annotation => {
const err = new Error('\n' + serialize(annotation));
Expand Down
2 changes: 1 addition & 1 deletion src/mapping.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { compose } from './utils';
export function mapping<T>(decoder: Decoder<T>): Decoder<Map<string, T>> {
return compose(
pojo,
// $FlowIgnore - deliberately using Object here
// $FlowIgnore: deliberate use of Object here
(blob: Object) => {
let tuples: Array<[string, T]> = [];
let errors: Array<[string, string | Annotation]> = [];
Expand Down
4 changes: 2 additions & 2 deletions src/number.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import { annotate } from 'debrief';
import { Err, Ok } from 'lemons';

import type { Decoder } from './types';
import type { Decoder, anything } from './types';
import { compose, predicate } from './utils';

export const anyNumber: Decoder<number> = (blob: mixed) => {
export const anyNumber: Decoder<number> = (blob: anything) => {
return typeof blob === 'number' && !Number.isNaN(blob) ? Ok(blob) : Err(annotate(blob, 'Must be number'));
};

Expand Down
22 changes: 12 additions & 10 deletions src/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import { annotate, annotateFields, isAnnotation } from 'debrief';
import type { Annotation } from 'debrief';
import { Err, Ok } from 'lemons';

import type { Decoder } from './types';
import type { Decoder, anything } from './types';
import { compose, isDate } from './utils';

// $FlowIgnore - helper to indicate we're deliberately using "any"
// $FlowIgnore: we're deliberately casting
type cast = any;

function isObject(o: mixed): boolean %checks {
function isObject(o: anything): boolean %checks {
return o !== null && typeof o === 'object' && !Array.isArray(o) && !isDate(o);
}

Expand All @@ -24,8 +24,8 @@ function subtract(xs: Set<string>, ys: Set<string>): Set<string> {
return result;
}

// $FlowIgnore - deliberately using Object here
export const pojo: Decoder<Object> = (blob: mixed) => {
// $FlowIgnore: deliberate use of Object here
export const pojo: Decoder<Object> = (blob: anything) => {
return isObject(blob) ? Ok(blob) : Err(annotate(blob, 'Must be an object'));
};

Expand Down Expand Up @@ -55,11 +55,11 @@ type UnwrapDecoder = <T>(Decoder<T>) => T;
* Put simply: it'll "peel off" all of the nested Decoders, puts them together
* in an object, and wraps it in a Guard<...>.
*/
export function object<O: { +[field: string]: Decoder<mixed> }>(mapping: O): Decoder<$ObjMap<O, UnwrapDecoder>> {
export function object<O: { +[field: string]: Decoder<anything> }>(mapping: O): Decoder<$ObjMap<O, UnwrapDecoder>> {
const known = new Set(Object.keys(mapping));
return compose(
pojo,
// $FlowIgnore - deliberately using Object here
// $FlowIgnore: deliberate use of Object here
(blob: Object) => {
const actual = new Set(Object.keys(blob));

Expand Down Expand Up @@ -131,12 +131,14 @@ export function object<O: { +[field: string]: Decoder<mixed> }>(mapping: O): Dec
);
}

export function exact<O: { +[field: string]: Decoder<mixed> }>(mapping: O): Decoder<$Exact<$ObjMap<O, UnwrapDecoder>>> {
export function exact<O: { +[field: string]: Decoder<anything> }>(
mapping: O
): Decoder<$Exact<$ObjMap<O, UnwrapDecoder>>> {
// Check the inputted object for any superfluous keys
const allowed = new Set(Object.keys(mapping));
const checked = compose(
pojo,
// $FlowIgnore - deliberately using Object here
// $FlowIgnore: deliberate use of Object here
(blob: Object) => {
const actual = new Set(Object.keys(blob));
const superfluous = subtract(actual, allowed);
Expand All @@ -162,7 +164,7 @@ export function field<T>(field: string, decoder: Decoder<T>): Decoder<T> {
// like this, not efficient -- pull it out of this function)
return compose(
pojo,
// $FlowIgnore - deliberately using Object here
// $FlowIgnore: deliberate use of Object here
(blob: Object) => {
const value = blob[field];
const result = decoder(value);
Expand Down
4 changes: 2 additions & 2 deletions src/optional.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import { Err, Ok } from 'lemons';

import { undefined_ } from './constants';
import { either } from './either';
import type { Decoder } from './types';
import type { Decoder, anything } from './types';

/**
* Decoder that only returns Ok for `null` or `undefined` inputs. In both
* cases, it will return `undefined`, so `null` inputs will get converted to
* `undefined` outputs. Err otherwise.
*/
export const undefined_or_null: Decoder<void> = (blob: mixed) =>
export const undefined_or_null: Decoder<void> = (blob: anything) =>
blob === undefined || blob === null ? Ok(undefined) : Err(annotate(blob, 'Must be undefined or null'));

/**
Expand Down
4 changes: 2 additions & 2 deletions src/string.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { annotate } from 'debrief';
import { Err, Ok } from 'lemons';

import type { Decoder } from './types';
import type { Decoder, anything } from './types';
import { compose, predicate } from './utils';

/** Match groups in this regex:
Expand All @@ -21,7 +21,7 @@ const DEFAULT_SCHEMES = ['https'];
/**
* Decoder that only returns Ok for string inputs. Err otherwise.
*/
export const string: Decoder<string> = (blob: mixed) => {
export const string: Decoder<string> = (blob: anything) => {
return typeof blob === 'string' ? Ok(blob) : Err(annotate(blob, 'Must be string'));
};

Expand Down
4 changes: 2 additions & 2 deletions src/tuple.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
import { annotate } from 'debrief';
import { Err, Ok } from 'lemons';

import type { Decoder } from './types';
import type { Decoder, anything } from './types';

/**
* Builds a Decoder that returns Ok for 2-tuples of [T1, T2], given Decoders
* for T1 and T2. Err otherwise.
*/
export function tuple2<T1, T2>(decoder1: Decoder<T1>, decoder2: Decoder<T2>): Decoder<[T1, T2]> {
return (blobs: mixed) => {
return (blobs: anything) => {
if (!Array.isArray(blobs)) {
return Err(annotate(blobs, 'Must be an array'));
}
Expand Down
15 changes: 12 additions & 3 deletions src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,17 @@
import type { Annotation } from 'debrief';
import { Result } from 'lemons';

export type Guard<T> = mixed => T;
// NOTE:
// Normally, we should not be discarding Flow warnings about the use of the
// "any" type. But in the case of decoders, it's the very purpose of the
// library to accept *anything* as input. To avoid suppressing the "any"
// warnings everywhere throughout this library, we'll suppress it here once,
// and use this re-aliased version of "any" elsewhere.
//
// $FlowIgnore: decoders take *anything* as input. It's their purpose.
export type anything = any;

export type Guard<T> = anything => T;
export type Predicate<T> = T => boolean;
export type DecodeResult<T> = Result<Annotation, T>;
// $FlowIgnore - deliberately allow `any` here. It's the purpose of decoders!
export type Decoder<T, F = any> = F => DecodeResult<T>;
export type Decoder<T, F = anything> = F => DecodeResult<T>;
11 changes: 5 additions & 6 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { annotate } from 'debrief';
import { Err, Ok } from 'lemons';

import type { Decoder } from './types';
import type { Decoder, anything } from './types';

/**
* This uses duck typing to check whether this is a Date instance. Since
Expand All @@ -15,8 +15,7 @@ import type { Decoder } from './types';
*
* But in this case, I chose the faster check.
*/
export const isDate = (value: mixed): boolean =>
!!value && value.getMonth !== undefined && typeof value.getMonth === 'function';
export const isDate = (value: anything): boolean => !!value && typeof value.getMonth === 'function';

/**
* Given a decoder T and a mapping function from T's to V's, returns a decoder
Expand All @@ -32,15 +31,15 @@ export function map<T, V>(decoder: Decoder<T>, mapper: T => V): Decoder<V> {
/**
* Compose two decoders by passing the result of the first into the second.
* The second decoder may assume as its input type the output type of the first
* decoder (so it's not necessary to accept the typical "mixed"). This is
* useful for "narrowing down" the checks. For example, if you want to write
* decoder (so it's not necessary to accept the typical "any"). This is useful
* for "narrowing down" the checks. For example, if you want to write
* a decoder for positive numbers, you can compose it from an existing decoder
* for any number, and a decoder that, assuming a number, checks if it's
* positive. Very often combined with the predicate() helper as the second
* argument.
*/
export function compose<T, V>(decoder: Decoder<T>, next: Decoder<V, T>): Decoder<V> {
return (blob: mixed) => decoder(blob).andThen(next);
return (blob: anything) => decoder(blob).andThen(next);
}

/**
Expand Down

0 comments on commit e187b54

Please sign in to comment.