io-ts-extra
Version:
Adds pattern matching, optional properties, and several other helpers and types, to io-ts.
154 lines • 5.96 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.collect = exports.matcher = exports.match = void 0;
const Either = __importStar(require("fp-ts/lib/Either"));
const t = __importStar(require("io-ts"));
const util_1 = require("./util");
const shorthand_1 = require("./shorthand");
const maybeMatchObject = (obj, cases) => {
for (const [type, map] of cases) {
const decoded = type.decode(obj);
if (decoded._tag === 'Right') {
return Either.right(map(decoded.right));
}
}
return Either.left({ noMatchFoundFor: obj, types: cases.map(c => c[0]) });
};
const matchObject = (obj, cases) => {
const either = maybeMatchObject(obj, cases);
if (either._tag === 'Right') {
return either.right;
}
util_1.RichError.throw({ noMatchFoundFor: obj, types: cases.map(c => c[0]) });
};
const patternMatcher = (cases, obj) => ({
case: (type, ...fns) => {
const codec = shorthand_1.codecFromShorthand(type);
const refined = fns.length > 1 ? t.refinement(codec, fns[0]) : codec;
return patternMatcher(cases.concat([[refined, fns[fns.length - 1]]]), obj);
},
default: (map) => patternMatcher(cases.concat([[t.any, map]]), obj),
get: () => matchObject(obj, cases),
});
/**
* Match an object against a number of cases. Loosely based on Scala's pattern matching.
*
* @example
* // get a value which could be a string or a number:
* const value = Math.random() < 0.5 ? 'foo' : Math.random() * 10
* const stringified = match(value)
* .case(String, s => `the message is ${s}`)
* .case(7, () => 'exactly seven')
* .case(Number, n => `the number is ${n}`)
* .get()
*
* @description
* Under the hood, io-ts is used for validation. The first argument can be a "shorthand" for a type,
* but you can also pass in io-ts codecs directly for more complex types:
*
* @example
* // get a value which could be a string or a number:
* const value = Math.random() < 0.5 ? 'foo' : 123
* const stringified = match(value)
* .case(t.number, n => `the number is ${n}`)
* .case(t.string, s => `the message is ${s}`)
* .get()
*
* @description
* you can use a predicate function or `t.refinement` for the equivalent of scala's `case x: Int if x > 2`:
*
* @example
* // value which could be a string, or a real number in [0, 10):
* const value = Math.random() < 0.5 ? 'foo' : Math.random() * 10
* const stringified = match(value)
* .case(Number, n => n > 2, n => `big number: ${n}`)
* .case(Number, n => `small number: ${n}`)
* .default(x => `not a number: ${x}`)
* .get()
*
* @example
* // value which could be a string, or a real number in [0, 10):
* const value = Math.random() < 0.5 ? 'foo' : Math.random() * 10
* const stringified = match(value)
* .case(t.refinement(t.number, n => n > 2), n => `big number: ${n}`)
* .case(t.number, n => `small number: ${n}`)
* .default(x => `not a number: ${x}`)
* .get()
*
* @description
*
* note: when using predicates or `t.refinement`, the type being refined is not considered exhaustively matched,
* so you'll usually need to add a non-refined option, or you can also use `.default` as a fallback
* case (the equivalent of `.case(t.any, ...)`)
*
* @param obj the object to be pattern-matched
*/
const match = (obj) => patternMatcher([], obj);
exports.match = match;
/**
* Like @see match but no object is passed in when constructing the case statements.
* Instead `.get` is a function into which a value should be passed.
*
* @example
* const Email = t.type({sender: t.string, subject: t.string, body: t.string})
* const SMS = t.type({from: t.string, content: t.string})
* const Message = t.union([Email, SMS])
* type Message = typeof Message._A
*
* const content = matcher<MessageType>()
* .case(SMS, s => s.content)
* .case(Email, e => e.subject + '\n\n' + e.body)
* .get({from: '123', content: 'hello'})
*
* expect(content).toEqual('hello')
*
* @description
* The function returned by `.get` is stateless and has no `this` context,
* you can store it in a variable and pass it around:
*
* @example
* const getContent = matcher<Message>()
* .case(SMS, s => s.content)
* .case(Email, e => e.subject + '\n\n' + e.body)
* .get
*
* const allMessages: Message[] = getAllMessages();
* const contents = allMessages.map(getContent);
*/
const matcher = () => matcherRecursive([]);
exports.matcher = matcher;
const matcherRecursive = (cases) => ({
case: (type, ...fns) => {
const codec = shorthand_1.codecFromShorthand(type);
const refined = fns.length > 1 ? t.refinement(codec, fns[0]) : codec;
return matcherRecursive(cases.concat([[refined, fns[fns.length - 1]]]));
},
default: (map) => matcherRecursive(cases.concat([[t.any, map]])),
get: (obj) => matchObject(obj, cases),
tryGet: (obj) => maybeMatchObject(obj, cases),
});
const collect = (items, partialFunc) => items
.map(partialFunc)
.filter((o) => o._tag === 'Right')
.map(o => o.right);
exports.collect = collect;
//# sourceMappingURL=match.js.map