UNPKG

inquiry-monad

Version:

Inquiry is an expressive API that allows one ask multiple questions about a subject value, and observe all results. This process returns a collection of all passes, fails, and the original subject value.

635 lines 25.2 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); const simple_maybe_1 = require("simple-maybe"); const symbols_1 = require("./symbols"); exports.$$inquirySymbol = symbols_1.$$inquirySymbol; exports.$$questionSymbol = symbols_1.$$questionSymbol; exports.$$passSymbol = symbols_1.$$passSymbol; exports.$$failSymbol = symbols_1.$$failSymbol; const noop = () => { }; const $$notFoundSymbol = Symbol('Not found'); // like Promise.all without the fast reject functionality const PromiseEvery = (promises) => new Promise((resolve) => { const results = []; let count = 0; promises.forEach((promise, idx) => { promise .catch(err => err) // pass errs down as (presumably) Fail .then((valueOrError) => { results[idx] = valueOrError; count += 1; count === promises.length && resolve(results); }); }); }); const IOU = (x) => ({ map: (f) => IOU(f(x)), chain: (f) => f(x), ap: (y) => y.map(x), inspect: () => `IOU(${x})`, join: () => x, emit: () => x, concat: (o) => o.chain((r) => IOU(x.concat(r))), head: () => Array.isArray(x) && x.length ? x[0] : [], tail: () => Array.isArray(x) && x.length ? x[x.length - 1] : [], isEmpty: () => Boolean(!Array.isArray(x) || x.length === 0), [symbols_1.$$inquirySymbol]: false, [symbols_1.$$passSymbol]: false, [symbols_1.$$failSymbol]: false, [symbols_1.$$iouSymbol]: true }); exports.IOU = IOU; const Pass = (x) => ({ map: (f) => Pass(f(x)), chain: (f) => f(x), fold: (f, _) => f(x), fork: (_, f) => f(x), head: () => (Array.isArray(x) && x.length ? x[0] : []), tail: () => Array.isArray(x) && x.length ? x[x.length - 1] : [], join: () => x, emit: () => x, inspect: () => `Pass(${x})`, concat: (o) => o.fold((r) => Pass(x.concat(r)), noop), ap: (y) => y[symbols_1.$$passSymbol] ? y.concat(Pass(x)) : Pass(x), answer: (i, n = '(anonymous)', Ix = Inquiry) => { i.informant([n, Pass(x)]); return Ix({ subject: i.subject, fail: i.fail, iou: i.iou, pass: i.pass.concat(Pass(x)), informant: i.informant, questionset: i.questionset, receipt: i.receipt.concat(Receipt([[n, Pass(x)]])) }); }, isEmpty: () => Boolean(!Array.isArray(x) || x.length === 0), [symbols_1.$$passSymbol]: true, [symbols_1.$$failSymbol]: false, [symbols_1.$$iouSymbol]: false, [symbols_1.$$inquirySymbol]: false }); exports.Pass = Pass; const Fail = (x) => ({ map: (f) => Fail(f(x)), chain: (f) => f(x), fold: (_, f) => f(x), fork: (f, _) => f(x), head: () => (Array.isArray(x) && x.length ? x[0] : []), tail: () => Array.isArray(x) && x.length ? x[x.length - 1] : [], join: () => x, emit: () => x, inspect: () => `Fail(${x})`, concat: (o) => o.fork((r) => Fail(x.concat(r)), noop), ap: (y) => y[symbols_1.$$passSymbol] ? Fail(x) : y.concat(Fail(x)), answer: (i, n = '(anonymous)', Ix = Inquiry) => { i.informant([n, Fail(x)]); return Ix({ subject: i.subject, fail: i.fail.concat(Fail(x)), pass: i.pass, iou: i.iou, informant: i.informant, questionset: i.questionset, receipt: i.receipt.concat(Receipt([[n, Fail(x)]])) }); }, isEmpty: () => Boolean(!Array.isArray(x) || x.length === 0), [symbols_1.$$passSymbol]: false, [symbols_1.$$failSymbol]: true, [symbols_1.$$iouSymbol]: false, [symbols_1.$$inquirySymbol]: false }); exports.Fail = Fail; const Receipt = (x) => ({ map: (f) => Receipt(f(x)), chain: (f) => f(x), fold: (_, f) => f(x), fork: (f, _) => f(x), head: () => x[0], tail: () => x[x.length - 1], join: () => x, emit: () => x, inspect: () => `Receipt(${x})`, isEmpty: () => Boolean(!Array.isArray(x) || x.length === 0), concat: (o) => o.chain((r) => Receipt(x.concat(r))), ap: (y) => y.map(x), [symbols_1.$$inquirySymbol]: false, [symbols_1.$$receiptSymbol]: true }); exports.Receipt = Receipt; const questionTypeError = (x) => console.error('Question must be passed parameters that adhere to the documented type. Value that was passed:', x); const Question = (x) => ({ map: (f) => Question(f(x)), chain: (f) => f(x), ap: (y) => y.map(x), inspect: () => `Question(${x})`, join: () => x, emit: () => x, call: (i) => x[1](i.join().subject.join()), extract: () => x[1], name: () => x[0], [symbols_1.$$questionSymbol]: true }); const QuestionOf = (x) => Array.isArray(x) ? Question(x) : questionTypeError(x); const exportQuestion = { of: QuestionOf }; exports.Question = exportQuestion; const Questionset = (x) => ({ map: (f) => Questionset(f(x)), chain: (f) => f(x), ap: (y) => y.map(x), inspect: () => `Questionset(${x})`, join: () => x, emit: () => x, concat: (o) => o.chain((r) => Questionset(x.concat(r))), find: (a) => simple_maybe_1.Maybe.of(x.find(i => RegExp(i[0]).test(a))) .map((b) => b[1]) .fork(() => { console.warn('Question was not found: ', a); return $$notFoundSymbol; }, (c) => c), [symbols_1.$$questionsetSymbol]: true }); const questionsetTypeError = (x) => console.error('Questionset must be passed parameters that adhere to the documented type. Value that was passed:', x); const QuestionsetOf = (x) => Array.isArray(x) ? Questionset(x) : questionsetTypeError(x); const exportQuestionset = { of: QuestionsetOf }; exports.Questionset = exportQuestionset; const InquirySubject = (x) => x[symbols_1.$$inquirySymbol] ? x : Inquiry({ subject: simple_maybe_1.Maybe.of(x), fail: Fail([]), pass: Pass([]), iou: IOU([]), informant: (_) => _, questionset: Questionset([['', noop]]), receipt: Receipt([]) }); const warnTypeError = (x) => { console.warn('Inquiry.of requires properties: subject, fail, pass, iou, informant, questionset, receipt. Converting to Inquiry.subject().'); return InquirySubject(x); }; // @todo validate constructor via Symbol const InquiryOf = (x) => 'subject' in x && 'fail' in x && 'pass' in x && 'iou' in x && 'informant' in x && 'questionset' in x && 'receipt' in x ? Inquiry(x) : warnTypeError(x); const Inquiry = (x) => ({ // Inquire: core method // You may pass a Function, a QuestionMonad (with a function), or a string which will look up // in the current Inquiry's questionset. // @todo in 1.x, deprecate Function as an option // @todo should we allow a Questionset to be passed here, and process all of them? // @todo after simplifying, can BoolTable help here? inquire: (f) => { const extractName = (f) => f[symbols_1.$$questionSymbol] ? f.name() : f; const fnName = typeof f === 'function' ? f.name || 'anon' : extractName(f); const fExtractFn = f[symbols_1.$$questionSymbol] ? f.extract() : f; const fIsFn = typeof fExtractFn === 'function'; const inquire = fIsFn ? fExtractFn : x.questionset.find(fExtractFn); const warnNotPassFail = (resp) => { console.warn('inquire was passed a function that does not return Pass or Fail:', fnName); console.warn('response was:', resp); return Inquiry(x); }; const inquireResponse = typeof inquire === 'function' ? inquire(x.subject.join()) : {}; return inquireResponse[symbols_1.$$failSymbol] || inquireResponse[symbols_1.$$passSymbol] || inquireResponse[symbols_1.$$inquirySymbol] ? inquireResponse.answer(x, fnName, Inquiry) : warnNotPassFail([inquireResponse, fnName]); }, inquireMap: (f, i) => i.reduce((inq, ii) => { const extractName = (f) => f[symbols_1.$$questionSymbol] ? f.name() : f; const fnName = typeof f === 'function' ? f.name || 'anon' : extractName(f); const fExtractFn = f[symbols_1.$$questionSymbol] ? f.extract() : f; const fIsFn = typeof fExtractFn === 'function'; const inquire = fIsFn ? fExtractFn : x.questionset.find(fExtractFn); const warnNotPassFail = (resp) => { console.warn('inquire was passed a function that does not return Pass or Fail:', fnName); console.warn('response was:', resp); return inq; }; const inquireResponse = typeof inquire === 'function' ? inquire(ii)(inq.join().subject.join()) : {}; // each return aggregates new contained value through exit return inquireResponse[symbols_1.$$failSymbol] || inquireResponse[symbols_1.$$passSymbol] || inquireResponse[symbols_1.$$inquirySymbol] ? inquireResponse.answer(inq.join(), fnName, Inquiry) : warnNotPassFail([inquireResponse, fnName]); }, // initial Inquiry will be what is in `x` now Inquiry({ subject: x.subject, iou: x.iou, fail: x.fail, pass: x.pass, informant: x.informant, questionset: x.questionset, receipt: x.receipt })), // @todo handle if no Questionset / .using() inquireAll: () => x.questionset.chain((questions) => questions.reduce((inq, q) => inq.inquire(QuestionOf(q)), Inquiry(x))), using: (a) => Inquiry({ subject: x.subject, iou: x.iou, fail: x.fail, pass: x.pass, informant: x.informant, questionset: a, receipt: x.receipt }), // Informant: for spying/logging/observable informant: (f) => Inquiry({ subject: x.subject, iou: x.iou, fail: x.fail, pass: x.pass, informant: f, questionset: x.questionset, receipt: x.receipt }), inspect: () => `Inquiry(${x.fail.inspect()} ${x.pass.inspect()} ${x.iou.inspect()}`, // Flow control: swap pass/fail swap: () => Inquiry({ subject: x.subject, iou: x.iou, fail: Fail(x.pass.join()), pass: Pass(x.fail.join()), informant: x.informant, questionset: x.questionset, receipt: x.receipt }), // Mapping across both branches unison: (f) => // apply a single map to both fail & pass (e.g. sort) Inquiry({ subject: x.subject, iou: x.iou, fail: Fail(f(x.fail.join())), pass: Pass(f(x.pass.join())), informant: x.informant, questionset: x.questionset, receipt: x.receipt }), // standard Monad methods map: (f) => InquirySubject(f(x)), ap: (y) => y.map(x), chain: (f) => f(x), join: () => x, emit: () => x, // execute the provided function if there are failures, else continue breakpoint: (f) => x.fail.join().length ? Inquiry(f(x)) : Inquiry(x), // execute the provided function if there are passes, else continue milestone: (f) => x.pass.join().length ? Inquiry(f(x)) : Inquiry(x), // internal method: execute informant, return new InquiryP() based on updated results answer: (i, n, _) => { i.informant([n, Inquiry(x)]); return Inquiry({ subject: i.subject, iou: i.iou, fail: i.fail.concat(x.fail), pass: i.pass.concat(x.pass), informant: i.informant, questionset: i.questionset, receipt: i.receipt }); }, // Unwrap methods // unwraps, mapping for both branches, full value returned conclude: (f, g) => ({ subject: x.subject, iou: x.iou, fail: f(x.fail), pass: g(x.pass), informant: x.informant, questionset: x.questionset, receipt: x.receipt }), // If there are no fails, handoff aggregated passes to supplied function; if any fails, return noop cleared: (f) => x.fail.isEmpty() ? f(x.pass) : noop(), // If there are fails, handoff aggregated fails to supplied function; if no fails, return noop faulted: (f) => x.fail.isEmpty() ? noop() : f(x.fail), // If there are passes, handoff aggregated passes to supplied function; if no passes, return noop suffice: (f) => x.pass.isEmpty() ? noop() : f(x.pass), // If there are no passes, handoff aggregated fails to supplied function; if any passes, return noop scratch: (f) => x.pass.isEmpty() ? f(x.fail) : noop(), // unwrap left if any fails, right if not fork: (f, g) => x.fail.join().length ? f(x.fail) : g(x.pass), // unwrap left if any passes, right if not fold: (f, g) => x.pass.join().length ? f(x.pass) : g(x.fail), // return a merged pass/fail zip: (f) => f(x.fail.join().concat(x.pass.join())), [symbols_1.$$inquirySymbol]: true }); const exportInquiry = { subject: InquirySubject, of: InquiryOf }; exports.Inquiry = exportInquiry; const InquiryPSubject = (x) => x[symbols_1.$$inquirySymbol] ? x : InquiryP({ subject: simple_maybe_1.Maybe.of(x), fail: Fail([]), pass: Pass([]), iou: IOU([]), informant: (_) => _, questionset: Questionset([['', noop]]), receipt: Receipt([]) }); const warnTypeErrorP = (x) => { console.warn('InquiryP.of requires properties: subject, fail, pass, iou, informant, questionset, receipt. Converting to InquiryP.subject().'); return InquiryPSubject(x); }; const InquiryPOf = (x) => 'subject' in x && 'fail' in x && 'pass' in x && 'iou' in x && 'informant' in x && 'questionset' in x && 'receipt' in x ? InquiryP(x) : warnTypeErrorP(x); const buildInq = (x) => (vals) => vals ? vals.reduce((acc, cur) => cur ? cur[1].answer(acc, cur[0], InquiryP).join() : acc, x) : x; // this is a bit complex, so here it goes: // Take all our IOUs (Questions), extract and resolve their Promises // then take those results apply to a tuple with the question name/description const resolveQs = (x) => x.iou.join().length ? x.iou.join().map((q) => q .extract()() .catch((err) => err) .then((result) => Promise.resolve([q.name(), result]))) : [Promise.resolve()]; const InquiryP = (x) => ({ inquire: (f) => { const extractName = (f) => f[symbols_1.$$questionSymbol] ? f.name() : f; const fnName = typeof f === 'function' ? f.name || 'anon' : extractName(f); const fExtractFn = f[symbols_1.$$questionSymbol] ? f.extract() : f; const fIsFn = typeof fExtractFn === 'function'; const inquire = fIsFn ? fExtractFn : x.questionset.find(fExtractFn); const warnNotPassFail = (resp) => { console.warn('inquire was passed a function that does not return Pass or Fail:', fnName); console.warn('response was:', resp); return InquiryP(x); }; const inquireResponse = typeof inquire === 'function' ? inquire(x.subject.join()) : {}; const syncronousResult = (response) => response[symbols_1.$$failSymbol] || response[symbols_1.$$passSymbol] || response[symbols_1.$$inquirySymbol] ? response.answer(x, fnName, InquiryP) : warnNotPassFail([inquireResponse, fnName]); const inquireIOU = inquireResponse.then ? QuestionOf([fnName, () => inquireResponse]) : false; return inquireIOU ? InquiryP({ subject: x.subject, fail: x.fail, pass: x.pass, iou: x.iou.concat(IOU([inquireIOU])), informant: x.informant, questionset: x.questionset, receipt: x.receipt }) : syncronousResult(inquireResponse); }, inquireMap: (f, i) => i.reduce((inq, ii) => { const extractName = (f) => f[symbols_1.$$questionSymbol] ? f.name() : f; const fnName = typeof f === 'function' ? f.name || 'anon' : extractName(f); const fExtractFn = f[symbols_1.$$questionSymbol] ? f.extract() : f; const fIsFn = typeof fExtractFn === 'function'; const inquire = fIsFn ? fExtractFn : x.questionset.find(fExtractFn); const warnNotPassFail = (resp) => { console.warn('inquire was passed a function that does not return Pass or Fail:', fnName); console.warn('response was:', resp); return inq; }; const inquireResponse = typeof inquire === 'function' ? inquire(ii)(inq.join().subject.join()) : {}; const syncronousResult = (response) => response[symbols_1.$$failSymbol] || response[symbols_1.$$passSymbol] || response[symbols_1.$$inquirySymbol] ? response.answer(inq.join(), fnName, InquiryP) : warnNotPassFail(response); return inquireResponse.then ? InquiryP({ subject: inq.join().subject, fail: inq.join().fail, pass: inq.join().pass, iou: inq.join().iou.concat(IOU([inquireResponse])), informant: inq.join().informant, questionset: inq.join().questionset, receipt: inq.join().receipt }) : syncronousResult(inquireResponse); }, // initial Inquiry will be what is in `x` now InquiryP({ subject: x.subject, iou: x.iou, fail: x.fail, pass: x.pass, informant: x.informant, questionset: x.questionset, receipt: x.receipt })), inquireAll: () => x.questionset.chain((questions) => questions.reduce((inq, q) => inq.inquire(QuestionOf(q)), InquiryP(x))), using: (a) => InquiryP({ subject: x.subject, iou: x.iou, fail: x.fail, pass: x.pass, informant: x.informant, questionset: a, receipt: x.receipt }), // Informant: for spying/logging/observable informant: (f) => InquiryP({ subject: x.subject, iou: x.iou, fail: x.fail, pass: x.pass, informant: f, questionset: x.questionset, receipt: x.receipt }), inspect: () => `InquiryP(${x.fail.inspect()} ${x.pass.inspect()} ${x.iou.inspect()}`, // Flow control: swap left/right pass/fail (iou is untouched) swap: () => InquiryP({ subject: x.subject, iou: x.iou, fail: Fail(x.pass.join()), pass: Pass(x.fail.join()), informant: x.informant, questionset: x.questionset, receipt: x.receipt }), // Mapping across both branches unison: (f) => // apply a single map to both fail & pass (e.g. sort), iou untouched InquiryP({ subject: x.subject, iou: x.iou, fail: Fail(f(x.fail.join())), pass: Pass(f(x.pass.join())), informant: x.informant, questionset: x.questionset, receipt: x.receipt }), // Standard monad methods - note that while these work, remember that `x` is a typed Object map: (f) => InquiryPSubject(f(x)), ap: (y) => y.map(x), chain: (f) => f(x), join: () => x, emit: () => x, // execute the provided function if there are failures, else continue breakpoint: (f) => x.fail.join().length ? InquiryP(f(x)) : InquiryP(x), // execute the provided function if there are passes, else continue milestone: (f) => x.pass.join().length ? InquiryP(f(x)) : InquiryP(x), // internal method: execute informant, return new InquiryP() based on updated results answer: (i, n, _) => { i.informant([n, InquiryP(x)]); return InquiryP({ subject: i.subject, iou: i.iou, fail: i.fail.concat(x.fail), pass: i.pass.concat(x.pass), informant: i.informant, questionset: i.questionset, receipt: i.receipt }); }, // Unwrapping methods: all return Promises, all complete outstanding IOUs // Unwraps the Inquiry after ensuring all IOUs are completed conclude: (f, g) => __awaiter(this, void 0, void 0, function* () { return PromiseEvery(resolveQs(x)) .then(buildInq(x)) .then(i => (i[symbols_1.$$inquirySymbol] ? i.join() : i)) .then(y => ({ subject: y.subject, iou: y.iou, fail: f(y.fail), pass: g(y.pass), informant: y.informant, questionset: y.questionset, receipt: y.receipt })); }), // If no fails, handoff aggregated passes to supplied function; if fails, return noop cleared: (f) => __awaiter(this, void 0, void 0, function* () { return PromiseEvery(resolveQs(x)) .then(buildInq(x)) .then(i => (i[symbols_1.$$inquirySymbol] ? i.join() : i)) .then(y => (y.fail.isEmpty() ? f(y.pass) : noop())) .catch(err => console.error('err', err)); }), // If fails, handoff aggregated fails to supplied function; if no fails, return noop faulted: (f) => __awaiter(this, void 0, void 0, function* () { return PromiseEvery(resolveQs(x)) .then(buildInq(x)) .then(i => (i[symbols_1.$$inquirySymbol] ? i.join() : i)) .then(y => (y.fail.isEmpty() ? noop() : f(y.fail))); }), // If any passes, handoff aggregated passes to supplied function; if no passes, return noop suffice: (f) => __awaiter(this, void 0, void 0, function* () { return PromiseEvery(resolveQs(x)) .then(buildInq(x)) .then(i => (i[symbols_1.$$inquirySymbol] ? i.join() : i)) .then(y => (y.pass.isEmpty() ? noop() : f(y.pass))) .catch(err => console.error('err', err)); }), // If no passes, handoff aggregated fails to supplied function; if any passes, return noop scratch: (f) => __awaiter(this, void 0, void 0, function* () { return PromiseEvery(resolveQs(x)) .then(buildInq(x)) .then(i => (i[symbols_1.$$inquirySymbol] ? i.join() : i)) .then(y => (y.pass.isEmpty() ? f(y.fail) : noop())); }), // Take left function and hands off fails if any, otherwise takes right function and hands off passes to that function fork: (f, g) => __awaiter(this, void 0, void 0, function* () { return PromiseEvery(resolveQs(x)) .then(buildInq(x)) .then(i => (i[symbols_1.$$inquirySymbol] ? i.join() : i)) .then(y => (y.fail.join().length ? f(y.fail) : g(y.pass))); }), // Take left function and hands off fails if any, otherwise takes right function and hands off passes to that function fold: (f, g) => __awaiter(this, void 0, void 0, function* () { return PromiseEvery(resolveQs(x)) .then(buildInq(x)) .then(i => (i[symbols_1.$$inquirySymbol] ? i.join() : i)) .then(y => (y.pass.join().length ? f(y.pass) : g(y.fail))); }), // return a Promise containing a merged fail/pass resultset array zip: (f) => __awaiter(this, void 0, void 0, function* () { return PromiseEvery(resolveQs(x)) .then(buildInq(x)) .then(i => (i[symbols_1.$$inquirySymbol] ? i.join() : i)) .then(y => f(y.fail.join().concat(y.pass.join()))); }), // await all IOUs to resolve, then return a new Inquiry CONVERTS TO PROMISE! await: (t = Infinity) => __awaiter(this, void 0, void 0, function* () { // try: generator function. Each IOU = array in for loop as per https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/yield const timeLimit = new Promise((resolve, reject) => setTimeout(resolve, t, [ 'timeout', Fail('Promise(s) have timed out') ])); const awaitPromises = PromiseEvery(resolveQs(x)); return (Promise.race([timeLimit, awaitPromises]) // @ts-ignore .then(buildInq(x)) .then((i) => (i[symbols_1.$$inquirySymbol] ? i.join() : i)) .then((y) => InquiryPOf(y))); }), [symbols_1.$$inquirySymbol]: true }); const exportInquiryP = { subject: InquiryPSubject, of: InquiryPOf }; exports.InquiryP = exportInquiryP; //# sourceMappingURL=index.js.map