doddle
Version:
Tiny yet feature-packed (async) iteration toolkit.
190 lines (180 loc) • 7.37 kB
JavaScript
import { getClassName, getValueDesc, isArrayLike, isAsyncIterable, isBool, isDoddle, isFunction, isInt, isIterable, isNextable, isPair, isPosInt, isReadableStream, isThenable, orderedStages } from "../utils.js";
export class DoddleError extends Error {
constructor(message) {
super((!Array.isArray(message) ? [message] : message).flat(5).join(" "));
}
}
/*
Sample error messages:
> Doddle: Argument 'predicate' of oprator 'Seq.filter' must be a function,
> but got 'true'.
> Doddle: Optional argument 'projection' of operator 'Seq.window' must be a function,
> but got 1.
> Doddle: Function argument 'predicate' to operator 'Seq.filter' must return
> true or false, but got 1.
> Doddle: {conversion|operator} 'X' must be called with {A-B|A} arguments, but got Z.
> Doddle: Input of conversion 'aseq' must be an async iterable, iterable, or a function,
> but got "hello world"
> Doddle: Each element of array argument 'others' to operator 'Seq.zip' must be an
> (async) iterable or function, but got "hello world" at index 1.
> OVERALL
> ${subject} must ${verb} ${expectation}, but got ${value} ${suffix}
subject.must
# Subject
- ${descriptor} '${name}' ${context}
# Descriptors
- Argument
- Function argument
# context
- of operator '${operator}'
-
*/
const getButGot = (value) => {
return ["but got", getValueDesc(value)];
};
export const getSubject = (thing, context, verb) => {
return [thing, "of", context, "must", verb];
};
const expectation = (expectation, check) => {
return (start) => (x) => {
if (!check(x)) {
const msg = [start, expectation, getButGot(x)];
throw new DoddleError(msg);
}
return x;
};
};
const expectInt = expectation(`a integer`, isInt);
const expectString = expectation(`a string`, x => typeof x === "string");
const expectIntOrInfinity = expectation(`an integer or Infinity`, x => isInt(x) || x === Infinity);
const expectPosInt = expectation(`a positive integer`, isPosInt);
const expectBool = expectation(`true or false`, isBool);
const expectError = expectation(`an error`, x => x instanceof Error);
const expectFunc = expectation(`a function`, isFunction);
const expectPair = expectation(`an array of length 2`, isPair);
const expectStage = expectation("'before', 'after', 'both', or undefined", stage => orderedStages.indexOf(stage) > -1);
const anOrStructure = (a, b) => ["an", a, "or", b];
const expectSyncInputValue = expectation(anOrStructure(["iterable", "iterator", "doddle"], "a function"), x => isIterable(x) || isFunction(x) || isDoddle(x) || isNextable(x) || isArrayLike(x));
const expectAsyncInputValue = expectation(anOrStructure(["(async) iterable", "iterator", "doddle"].join(", "), "a function"), x => isIterable(x) ||
isAsyncIterable(x) ||
isFunction(x) ||
isDoddle(x) ||
isNextable(x) ||
isReadableStream(x) ||
isArrayLike(x));
const iterableOrIterator = anOrStructure("iterable", "iterator");
const expectSyncIterableOrIterator = expectation(anOrStructure("iterable", "iterator"), x => isIterable(x) || isNextable(x) || isDoddle(x) || isArrayLike(x));
const expectAsyncIterableOrIterator = expectation(anOrStructure(["(async)", "iterable"], "iterator"), x => isIterable(x) ||
isAsyncIterable(x) ||
isNextable(x) ||
isDoddle(x) ||
isReadableStream(x) ||
isArrayLike(x));
const checkFunctionReturn = (thing, context, expectReturn, allowAsync) => {
return (f) => {
expectFunc(getSubject(thing, context, "be"))(f);
return (...args) => {
const result = f(...args);
const resultChecker = expectReturn(getSubject(["function", thing], context, "return"));
if (isThenable(result) && allowAsync) {
return result.then(x => {
if (isDoddle(x)) {
return x.map(resultChecker);
}
return resultChecker(x);
});
}
if (isDoddle(result)) {
return result.map(resultChecker);
}
return resultChecker(result);
};
};
};
export const forOperator = (operator) => {
const context = ["operator", `'${operator}'`];
function getArgThing(name) {
return ["argument", `'${name}'`];
}
const allowAsync = ["a", "A"].includes(operator[0]);
function getArgSubject(name) {
return getSubject(getArgThing(name), context, "be");
}
function checkValue(name, exp) {
return [name, exp(getArgSubject(name))];
}
function checkFuncReturn(name, exp) {
return [name, checkFunctionReturn(getArgThing(name), context, exp, allowAsync)];
}
const simpleEntries = [
checkValue("size", expectPosInt),
checkValue("start", expectInt),
checkValue("end", expectIntOrInfinity),
checkValue("index", expectInt),
checkValue("count", expectIntOrInfinity),
checkValue("projection", expectFunc),
checkValue("action", expectFunc),
checkValue("handler", expectFunc),
checkValue("separator", expectString),
checkValue("descending", expectBool),
checkValue("reducer", expectFunc),
checkValue("stage", expectStage),
checkValue("skipCount", expectPosInt),
checkValue("keyProjection", expectFunc),
checkFuncReturn("kvpProjection", expectPair),
checkFuncReturn("predicate", expectBool),
checkFuncReturn("thrower", expectError),
checkValue("ms", expectInt)
];
return Object.fromEntries(simpleEntries);
};
const wInput = "input";
const wSeq = "'seq'";
const wAseq = "'aseq'";
export const checkSeqInputValue = (input) => {
const context = ["conversion", wSeq];
expectSyncInputValue(getSubject(wInput, context, "be"))(input);
if (isFunction(input)) {
return checkFunctionReturn(wInput, context, expectSyncIterableOrIterator, false)(input);
}
return input;
};
export const checkASeqInputValue = (input) => {
const context = ["conversion", wAseq];
expectAsyncInputValue(getSubject(wInput, context, "be"))(input);
if (isFunction(input)) {
return checkFunctionReturn(wInput, context, expectAsyncIterableOrIterator, true)(input);
}
return input;
};
export const gotAsyncIteratorInSyncContext = () => {
throw new DoddleError([
getSubject(wInput, [wAseq], "be"),
iterableOrIterator,
["but got", "an async", "iterator"]
]);
};
const __checkers = "__checkers";
const LOADED = Symbol(__checkers);
export function loadCheckers(target) {
if (LOADED in target) {
return target;
}
for (const key of Object.getOwnPropertyNames(target).filter(x => !x.startsWith("_"))) {
const v = target[key];
if (isFunction(v) && !v[__checkers]) {
Object.defineProperty(v, __checkers, {
value: forOperator(`${getClassName(target)}.${key}`)
});
}
}
return Object.defineProperty(target, LOADED, {});
}
export function chk(input) {
return input[__checkers];
}
export const invalidRecursionError = (parentType) => {
return `Child iterable called its own ${parentType} during iteration, which is illegal.`;
};
export const reduceOnEmptyError = "Cannot reduce empty sequence with no initial value";
//# sourceMappingURL=error.js.map