UNPKG

self-assert

Version:

A small TypeScript library for designing models with built-in validity.

1,496 lines (1,413 loc) 46.2 kB
'use strict'; class RuleLabel { id; description; static fromJson({ id, description }) { return new this(id, description); } constructor(id, description) { this.id = id; this.description = description; } isLabeledAs(aBrokenRuleLabel) { return aBrokenRuleLabel.hasLabel(this.id, this.description); } hasLabel(assertionId, assertionDescription) { return this.hasLabelId(assertionId) && this.hasDescription(assertionDescription); } hasLabelId(assertionId) { return this.id === assertionId; } hasDescription(description) { return this.description === description; } getId() { return this.id; } getDescription() { return this.description; } } /** * Provides a way to handle multiple failed rules, * by their labels. * * @see {@link RuleLabel} * @category Rules */ class RulesBroken extends Error { brokenRules; /** @category Creation */ static fromJson(rulesBrokenAsJson) { const brokenRules = rulesBrokenAsJson.brokenRules.map((ruleAsJson) => RuleLabel.fromJson(ruleAsJson)); return new this(brokenRules); } /** @category Creation */ constructor(brokenRules) { super(); this.brokenRules = brokenRules; } /** @category Inspection */ hasRuleBrokenWith(labelId, labelDescription) { return this.brokenRules.some((rule) => rule.hasLabel(labelId, labelDescription)); } /** @category Inspection */ hasOnlyOneRuleBrokenWith(labelId, labelDescription) { return (this.brokenRules.length === 1 && this.brokenRules[0].hasLabel(labelId, labelDescription)); } /** @category Inspection */ forEachRuleBroken(closure) { return this.brokenRules.forEach(closure); } } /** * Runs all rules and throws an error if any has failed. * The failed rules are included in the error. * * @category Rules */ class Ruleset { assertions; rules; /** * Evaluates all assertions **synchronously** and throws an error if any has failed. * * @throws {@link RulesBroken} if any rule has failed. * * @example * {@includeCode ../../../../examples/snippets/rules.ts#ruleset-ensureAll} */ static ensureAll(...assertions) { new this(assertions.flat(), []).ensure(); } /** * Evaluates all rules **asynchronously** and throws an error if any has failed. * * @throws {@link RulesBroken} if any rule has failed * * @example * {@includeCode ../../../../examples/snippets/rules.ts#email-unique,ruleset-workOn} */ static workOn(...rules) { return new this([], rules.flat()).mustHold(); } constructor(assertions, rules) { this.assertions = assertions; this.rules = rules; } async mustHold() { const brokenRules = await this.brokenRules(); this.throwIfNotEmpty(brokenRules); } ensure() { const brokenRules = this.failedAssertions(); this.throwIfNotEmpty(brokenRules); } failedAssertions() { const failed = []; this.assertions.forEach((assertion) => assertion.collectFailureInto(failed)); return failed; } async brokenRules() { const brokenRules = []; for (const rule of this.rules) { await rule.collectFailureInto(brokenRules); } return brokenRules; } throwIfNotEmpty(brokenRules) { if (brokenRules.length > 0) throw new RulesBroken(brokenRules); } } /** * Represents the evaluation of a rule on a given value. * * It can also be created using the {@link Rule.evaluateFor} method. * * @template ValueType The type of value the rule applies to. * * @example * {@includeCode ../../../../examples/snippets/rules.ts#evaluateFor,rule-evaluation} * * @category Rules */ class RuleEvaluation { rule; value; constructor(rule, value) { this.rule = rule; this.value = value; } /** * @category Rule evaluation * @see {@link Rule.doesHold} */ doesHold() { return this.rule.doesHold(this.value); } /** * @category Rule evaluation * @see {@link Rule.hasFailed} */ hasFailed() { return this.rule.hasFailed(this.value); } /** * @category Rule evaluation * @see {@link Rule.mustHold} */ mustHold() { return this.rule.mustHold(this.value); } /** * @category Rule evaluation * @see {@link Rule.collectFailureInto} */ collectFailureInto(failed) { return this.rule.collectFailureInto(failed, this.value); } isLabeledAs(aBrokenRuleLabel) { return this.rule.isLabeledAs(aBrokenRuleLabel); } hasLabel(anId, aDescription) { return this.rule.hasLabel(anId, aDescription); } hasDescription(aDescription) { return this.rule.hasDescription(aDescription); } hasLabelId(anId) { return this.rule.hasLabelId(anId); } getId() { return this.rule.getId(); } getDescription() { return this.rule.getDescription(); } } /** * Represents a validation rule in the problem domain. * * This is the base class for all rules. * - Use {@link Assertion} for rules that can be evaluated **synchronously**. * - Use {@link Inquiry} for rules that need to be evaluated **asynchronously**. * * Rules are identified by a unique identifier (`labelId`) and a human-readable description. * These identifiers are meant to be meaningful within the domain, * and can be used to route or display validation errors. * * @template PredicateReturnType The return type of the predicate functions. * Specifically, `true` or `Promise<true>`. * @template ValueType The type of value this rule applies to. * * @category Rules * @categoryDescription Rule evaluation * Related to the definition of business rules and their evaluation. * @categoryDescription Rule definition * Related to the definition of requirements for business rules. */ class Rule { label; requirements; constructor(label) { this.label = label; this.requirements = []; } /** * Adds a necessary requirement for the rule to hold. * * @example * Add a requirement for the rule to hold * {@includeCode ../../../../examples/snippets/rules.ts#require} * * * @returns `this` for chaining * @category Rule definition */ require(aConditionToBeMet) { this.requirements.push(aConditionToBeMet); return this; } /** * Prepares a {@link RuleEvaluation} for the given value. * * This is the same as `new RuleEvaluation(rule, value)`. * * @example * {@includeCode ../../../../examples/snippets/rules.ts#evaluateFor} * * @category Rule evaluation */ evaluateFor(aValue) { return new RuleEvaluation(this, aValue); } isLabeledAs(aBrokenRuleLabel) { return aBrokenRuleLabel.hasLabel(this.label.getId(), this.label.getDescription()); } hasLabel(anId, aDescription) { return this.label.hasLabel(anId, aDescription); } hasDescription(aDescription) { return this.label.hasDescription(aDescription); } hasLabelId(anId) { return this.label.hasLabelId(anId); } getId() { return this.label.getId(); } getDescription() { return this.label.getDescription(); } } /** * Represents a validation rule in the problem domain. * * An `Assertion` expresses a condition that must hold for a given value. * These rules are defined using one or more predicate functions, added via * {@link Rule.require require}. The assertion is considered to "hold" when all the conditions evaluate to `true`. * * @see * - {@link Requirements} provides a list of built-in requirements. * * @template ValueType The type of value this assertion applies to. * * @example * Basic usage * {@includeCode ../../../../examples/snippets/rules.ts#assertion-basic-usage} * * @category Rules */ class Assertion extends Rule { /** @category Creation */ static fromJson(assertionAsJson) { return new this(RuleLabel.fromJson(assertionAsJson)); } /** @category Creation */ static labeled(id, description) { const label = new RuleLabel(id, description); return new this(label); } static requiring(id, description, aConditionToBeMet) { return this.labeled(id, description).require(aConditionToBeMet); } constructor(label) { super(label); } doesHold(value) { return this.requirements.every((condition) => condition(value)); } hasFailed(value) { return !this.doesHold(value); } mustHold(value) { Ruleset.ensureAll(this.evaluateFor(value)); } collectFailureInto(failed, value) { if (this.hasFailed(value)) { failed.push(this.label); } } } /** * Represents a rule that needs to be evaluated **asynchronously**. * * @example * {@includeCode ../../../../examples/snippets/rules.ts#inquiry} * * @category Rules */ class Inquiry extends Rule { /** @category Creation */ static labeled(anId, aDescription) { return new this(new RuleLabel(anId, aDescription)); } static requiring(anId, aDescription, aCondition) { return this.labeled(anId, aDescription).require(aCondition); } constructor(label) { super(label); } async doesHold(value) { for (const requirement of this.requirements) { if (!(await requirement(value))) { return false; } } return true; } async hasFailed(value) { return !(await this.doesHold(value)); } async mustHold(value) { if (await this.hasFailed(value)) { throw new RulesBroken([this.label]); } } async collectFailureInto(failed, value) { if (await this.hasFailed(value)) { failed.push(this.label); } } } class LogicalRequirement { /** * Combines multiple conditions using logical AND * @function @category Composition */ static and = (...conditions) => (value) => conditions.every((condition) => condition(value)); /** * Combines multiple conditions using logical OR * @function @category Composition */ static or = (...conditions) => (value) => conditions.some((condition) => condition(value)); /** * Negates a condition * @function @category Composition */ static not = (condition) => (value) => !condition(value); /** * Returns a predicate that checks if the value is equal to the expected value. * @function @category Comparison */ static identical = (expected) => this.isIn(expected); /** * Returns a predicate that checks if the value is not equal to the forbidden value. * @function @category Comparison */ static differentFrom = (forbiddenValue) => this.not(this.identical(forbiddenValue)); /** * Returns a predicate that checks if the value is in the allowed set. * @function @category Comparison */ static isIn = (...allowedSet) => (value) => allowedSet.includes(value); /** * Returns a predicate that checks if the value is not in the forbidden set. * @function @category Comparison */ static isNotIn = (...forbiddenSet) => this.not(this.isIn(...forbiddenSet)); } const { and: and$1, or, not: not$2, identical: identical$1 } = LogicalRequirement; class NumbersRequirements { /** * Returns a predicate that is `true` if the value is greater than the given number * @function @category Numbers */ static greaterThan = (aNumber) => (value) => value > aNumber; /** * Returns a predicate that is `true` if the value is greater than or equal to the given number * @function @category Numbers */ static greaterThanOrEqual = (aNumber) => or(this.greaterThan(aNumber), identical$1(aNumber)); /** * Returns a predicate that is `true` if the value is less than the given number * @function @category Numbers */ static lessThan = (aNumber) => not$2(this.greaterThanOrEqual(aNumber)); /** * Returns a predicate that is `true` if the value is less than or equal to the given number * @function @category Numbers */ static lessThanOrEqual = (aNumber) => not$2(this.greaterThan(aNumber)); /** * Returns a predicate that is `true` if the value is between two numbers (inclusive) * @function @category Numbers */ static between = (min, max) => and$1(this.greaterThanOrEqual(min), this.lessThanOrEqual(max)); /** * Returns a predicate that is `true` if the value is an integer * @function @category Numbers */ static isInteger = Number.isInteger; /** * Returns a predicate that is `true` if the value is a float * @function @category Numbers */ static isFloat = and$1(Number.isFinite, not$2(this.isInteger)); /** * Returns a predicate that is `true` if the value is positive * @function @category Numbers */ static isPositive = this.greaterThan(0); /** * Returns a predicate that is `true` if the value is negative * @function @category Numbers */ static isNegative = this.lessThan(0); /** * Returns a predicate that is `true` if the value is a positive integer * @function @category Numbers */ static isPositiveInteger = and$1(this.isInteger, this.isPositive); /** * Returns a predicate that is `true` if the value is a negative integer * @function @category Numbers */ static isNegativeInteger = and$1(this.isInteger, this.isNegative); /** * Returns a predicate that is `true` if the value is an integer between two numbers (inclusive) * @function @category Numbers */ static isIntegerBetween = (min, max) => and$1(this.isInteger, this.between(min, max)); } const { identical, not: not$1, and } = LogicalRequirement; class ListsRequirements { /** * Returns a predicate that holds when the list has exactly the given number of elements * @function @category Lists */ static hasExactly = (aNumber) => (list) => identical(aNumber)(list.length); /** * Returns a predicate that holds when the list has no elements * @function @category Lists */ static isEmpty = this.hasExactly(0); /** * Returns a predicate that holds when the list has at least one element * @function @category Lists */ static isNotEmpty = not$1(this.isEmpty); /** * Returns a predicate that holds when the list has more than the given number of elements * @function @category Lists */ static hasMoreThan = (aNumber) => (list) => NumbersRequirements.greaterThan(aNumber)(list.length); /** * Returns a predicate that holds when the list has at most the given number of elements * @function @category Lists */ static hasAtMost = (aNumber) => not$1(this.hasMoreThan(aNumber)); /** * Returns a predicate that holds when the list has less than the given number of elements * @function @category Lists */ static hasLessThan = (aNumber) => and(this.hasAtMost(aNumber), not$1(this.hasExactly(aNumber))); /** * Returns a predicate that holds when the list has at least the given number of elements * @function @category Lists */ static hasAtLeast = (aNumber) => not$1(this.hasLessThan(aNumber)); /** * Returns a predicate that holds when the list contains the given element * @function @category Lists */ static includes = (element) => (list) => list.includes(element); /** * Opposite of {@link includes} * @function @category Lists */ static doesNotInclude = (element) => not$1(this.includes(element)); /** * Returns a predicate that holds when all elements of the list satisfy the given condition * @function @category Lists */ static allSatisfy = (predicate) => (list) => { for (const element of list) { if (!predicate(element)) { return false; } } return true; }; /** * Returns a predicate that holds when any element of the list satisfies the given condition * @function @category Lists */ static anySatisfy = (predicate) => (list) => { for (const element of list) { if (predicate(element)) { return true; } } return false; }; /** * Returns a predicate that holds when no element of the list satisfies the given condition. * Opposite of {@link anySatisfy} * @function @category Lists */ static noneSatisfy = (predicate) => not$1(this.anySatisfy(predicate)); } /* eslint-disable @typescript-eslint/no-misused-spread */ const { not } = LogicalRequirement; class StringsRequirements { /** * A predicate that evaluates to `true` if the string is empty or contains only whitespace characters. * @function @category Strings * @see {@link isNotBlank} */ static isBlank = (value) => ListsRequirements.isEmpty(value.trim()); /** * Opposite of {@link isBlank}. * @function @category Strings */ static isNotBlank = not(this.isBlank); } /** * A collection of common rule requirements. * * It also provides a way to compose requirements using the `and`, `or` and `not` functions. * * @namespace * * @category Rules * @categoryDescription Composition * Methods for composing requirements. Example: * {@includeCode ../../../../examples/snippets/requirements.ts#composition} */ const Requirements = { /** * A predicate that always evaluates to `true`. */ hold: () => true, /** * A predicate that always evaluates to `false`. */ fail: () => false, ...LogicalRequirement, ...NumbersRequirements, ...ListsRequirements, ...StringsRequirements, /** * @hidden * @privateRemarks * This is to avoid TypeDoc from showing the prototype in the docs */ prototype: Object.prototype, }; /** * Provides an assistant to guide the completion of a model. * * A `DraftAssistant` encapsulates the logic needed to: * * - track the current state of a form field or group of fields, * - validate the model being built, * - handle and route failed assertions, * - notify observers (viewers) of changes or validation failures. * * Assistants can be nested and composed to build complex models. * * @typeParam Model The type of the model the assistant helps to create. * @typeParam ContainerModel The type of the container model the assistant works on. * * @remarks * Originally, this class was named `ModelCreator`. Later, it was renamed to `FormCompletionAssistant`, * employing the metaphor of an assistant guiding form completion. This could have led to confusion, * since the class has more use cases than just form completion. * * It can, for example, be used in a backend context to validate an object before persisting it. * * @category Draft assistants */ class DraftAssistant { labelIds; modelFromContainer; initialModel; /** * This object is used as a **token** for an invalid model. * @internal */ static INVALID_MODEL = new Object(); /** * @category Model creation */ static isInvalidModel(potentialModel) { return potentialModel === DraftAssistant.INVALID_MODEL; } /** * @returns A default model getter from a container for the top-level assistant. * Since there is no container to get the model from, it throws an error. */ static topLevelModelFromContainer() { return () => { throw new Error("No container to get model from"); }; } model; brokenRules; viewers; constructor(labelIds, modelFromContainer, initialModel) { this.labelIds = labelIds; this.modelFromContainer = modelFromContainer; this.initialModel = initialModel; this.model = this.initialModel; this.viewers = []; this.removeBrokenRules(); } /** * Executes a closure depending on whether the model is valid or not after creating it. * * @template ReturnType - The type of the value returned by the closures. * @param validModelClosure - A closure that will be called with the created model * if it's valid. * @param invalidModelClosure - A closure that will be called if the model is invalid. * @returns The return value of the closure that was called. * * @category Model creation */ withCreatedModelDo(validModelClosure, invalidModelClosure) { const createdModel = this.createModel(); if (this.constructor.isInvalidModel(createdModel)) return invalidModelClosure(); return validModelClosure(createdModel); } /** @category Model creation */ getModel() { return this.model; } /** @category Model creation */ setModel(newModel) { this.model = newModel; this.notifyViewersOnChange(newModel); } /** * Resets the model to its initial value. * @category Model creation */ resetModel() { this.model = this.initialModel; this.notifyViewersOnChange(this.model); } /** * Sets the model from its container. * @category Model creation */ setModelFrom(containerModel) { return this.setModel(this.modelFromContainer(containerModel)); } /** * Adds a viewer to the list of observers. * @category Viewers */ accept(aViewer) { this.viewers.push(aViewer); } /** * Removes a viewer from the list of observers. * @category Viewers */ removeViewer(aViewer) { this.viewers = this.viewers.filter((viewer) => viewer !== aViewer); } /** * @returns The number of viewers currently observing the assistant. * @category Viewers */ numberOfViewers() { return this.viewers.length; } /** * Adds a rule to the list of broken rules. * @category Rules */ addBrokenRule(aBrokenRuleLabel) { if (this.hasBrokenRule(aBrokenRuleLabel)) return; this.brokenRules.push(aBrokenRuleLabel); this.forEachViewer((viewer) => viewer.onFailure?.(aBrokenRuleLabel)); } /** * Adds a list of rules to the list of broken rules. * @category Rules */ addBrokenRules(brokenRules) { brokenRules.forEach((failure) => { this.addBrokenRule(failure); }); } /** * @returns `true` if the list of broken rules is not empty * @category Rules */ hasBrokenRules() { return this.brokenRules.length > 0; } /** * Opposite of {@link hasBrokenRules}. * @category Rules */ doesNotHaveBrokenRules() { return !this.hasBrokenRules(); } /** * @returns The descriptions of the broken rules * @category Rules */ brokenRulesDescriptions() { return this.brokenRules .map((brokenRule) => brokenRule.getDescription()) .filter((description) => description !== ""); } /** * @returns `true` if this assistant handles the given `Assertion`. * @category Rules */ handles(aRule) { return this.labelIds.some((labelId) => aRule.hasLabelId(labelId)); } /** * Adds an assertion id to the list of handled assertions. * @category Rules */ addLabelId(aLabelId) { this.labelIds.push(aLabelId); } /** @category Rules */ hasBrokenRule(aBrokenRuleLabel) { return this.brokenRules.some((brokenRule) => brokenRule.isLabeledAs(aBrokenRuleLabel)); } /** * @returns `true` if this assistant has only one failed assertion that * is identified as the given `assertionId`. * * @remarks * Used mostly for testing. * * @category Rules */ hasOnlyOneRuleBrokenIdentifiedAs(assertionId) { return (this.brokenRules.length === 1 && this.brokenRules[0].hasLabelId(assertionId)); } /** @category Rules */ removeBrokenRules() { this.brokenRules = []; this.forEachViewer((viewer) => viewer.onFailuresReset?.()); } forEachViewer(action) { this.viewers.forEach(action); } notifyViewersOnChange(aModel) { this.forEachViewer((viewer) => viewer.onDraftChanged?.(aModel)); } } /** * An assistant designed to manage a single field or a simple * piece of data within a larger form or model. * * @category Draft assistants */ class FieldDraftAssistant extends DraftAssistant { assertions; static handling(assertionId, modelFromContainer, initialModel = "") { return this.handlingAll([assertionId], modelFromContainer, initialModel); } static handlingAll(assertionIds, modelFromContainer, initialModel = "") { return this.requiringAll(assertionIds.map((id) => Assertion.labeled(id, "(placeholder)")), modelFromContainer, initialModel); } static requiring(assertion, modelFromContainer, initialModel = "") { return this.requiringAll([assertion], modelFromContainer, initialModel); } static requiringAll(assertions, modelFromContainer, initialModel = "") { return new this(assertions, modelFromContainer, initialModel); } constructor(assertions, modelFromContainer, initialModel) { const ids = assertions.map((assertion) => assertion.getId()); super(ids, modelFromContainer, initialModel); this.assertions = assertions; } createModel() { this.removeBrokenRules(); return this.model; } /** * Checks if the current draft verifies all assertions. * If not, it adds them to the list of failed assertions. */ review() { this.removeBrokenRules(); const failures = []; this.assertions.forEach((assertion) => { assertion.collectFailureInto(failures, this.model); }); this.addBrokenRules(failures); } } /** * Assists in the creation of complex models by coordinating multiple inner `DraftAssistant`. * * It uses a {@link CreationClosure} function to combine the models created by its * assistants into a single composed model. * * @template ComposedModels - An array of types representing the types of the models created by the inner assistants, * in the same order as the `assistants` array. * * @category Draft assistants */ class SectionDraftAssistant extends DraftAssistant { assistants; creationClosure; static with(assistants, creationClosure, modelFromContainer, assertionIds) { return new this(assistants, creationClosure, modelFromContainer, assertionIds); } static topLevelContainerWith(assistants, creationClosure, assertionIds = []) { return this.with(assistants, creationClosure, this.topLevelModelFromContainer(), assertionIds); } constructor(assistants, creationClosure, modelFromContainer, assertionIds) { /** @ts-expect-error See {@link DraftAssistant.INVALID_MODEL} */ super(assertionIds, modelFromContainer, DraftAssistant.INVALID_MODEL); this.assistants = assistants; this.creationClosure = creationClosure; } createModel() { this.removeBrokenRules(); const models = this.createComposedModels(); try { super.setModel(this.creationClosure(...models)); } catch (error) { super.resetModel(); this.handleError(error); } return this.model; } setModel(newModel) { super.setModel(newModel); this.assistants.forEach((assistant) => assistant.setModelFrom(newModel)); } resetModel() { super.resetModel(); this.assistants.forEach((assistant) => assistant.resetModel()); } /** * @category Error handling */ handleError(possibleCreateModelError) { if (possibleCreateModelError instanceof RulesBroken) return this.routeBrokenRulesOf(possibleCreateModelError); throw possibleCreateModelError; } /** * @category Error handling */ routeBrokenRulesOf(aRulesBrokenError) { aRulesBrokenError.forEachRuleBroken((brokenRule) => this.routeBrokenRule(brokenRule)); } /** * @category Error handling */ routeBrokenRule(brokenRule) { if (this.handles(brokenRule)) this.addBrokenRule(brokenRule); else this.routeNotHandledByThisBrokenRule(brokenRule); } routeNotHandledByThisBrokenRule(brokenRule) { const assistantsHandlingRule = this.assistantsHandling(brokenRule); if (assistantsHandlingRule.length === 0) this.addBrokenRule(brokenRule); else this.addBrokenRuleToAll(assistantsHandlingRule, brokenRule); } addBrokenRuleToAll(assistantsHandlingAssertion, brokenRule) { assistantsHandlingAssertion.forEach((assistant) => assistant.addBrokenRule(brokenRule)); } assistantsHandling(assertion) { return this.assistants.filter((assistant) => assistant.handles(assertion)); } createComposedModels() { // @ts-expect-error TypeScript can't infer the tuple type directly. return this.assistants.map((assistant) => assistant.createModel()); } } /** * Provides an assistant for the completion of an integer field, * represented by a string. * * @category Draft assistants */ class IntegerDraftAssistant extends SectionDraftAssistant { static defaultAssertionDescription = "Invalid integer"; static for(assertionId, modelFromContainer) { const assertionIds = assertionId === "" ? [] : [assertionId]; /** @ts-expect-error @see {@link https://github.com/microsoft/TypeScript/issues/5863 #5863} */ return this.with([this.createNumberAssistant()], (numberAsString) => this.createInteger(assertionId, numberAsString), modelFromContainer, assertionIds); } static forTopLevel(assertionId) { return this.for(assertionId, this.topLevelModelFromContainer()); } static createInteger(assertionId, numberAsString) { Ruleset.ensureAll(this.createAssertionFor(assertionId, numberAsString)); return Number(numberAsString); } static createNumberAssistant() { return FieldDraftAssistant.handling("", (number) => number.toString()); } static createAssertionFor(assertionId, numberAsString) { return Assertion.requiring(assertionId, this.defaultAssertionDescription, () => /^[-+]?(\d+)$/.test(numberAsString)); } innerAssistant() { return this.assistants[0]; } setInnerModel(newModel) { this.innerAssistant().setModel(newModel); } getInnerModel() { return this.innerAssistant().getModel(); } } var domain; // This constructor is used to store event handlers. Instantiating this is // faster than explicitly calling `Object.create(null)` to get a "clean" empty // object (tested with v8 v4.9). function EventHandlers() {} EventHandlers.prototype = Object.create(null); function EventEmitter() { EventEmitter.init.call(this); } // nodejs oddity // require('events') === require('events').EventEmitter EventEmitter.EventEmitter = EventEmitter; EventEmitter.usingDomains = false; EventEmitter.prototype.domain = undefined; EventEmitter.prototype._events = undefined; EventEmitter.prototype._maxListeners = undefined; // By default EventEmitters will print a warning if more than 10 listeners are // added to it. This is a useful default which helps finding memory leaks. EventEmitter.defaultMaxListeners = 10; EventEmitter.init = function() { this.domain = null; if (EventEmitter.usingDomains) { // if there is an active domain, then attach to it. if (domain.active && !(this instanceof domain.Domain)) { this.domain = domain.active; } } if (!this._events || this._events === Object.getPrototypeOf(this)._events) { this._events = new EventHandlers(); this._eventsCount = 0; } this._maxListeners = this._maxListeners || undefined; }; // Obviously not all Emitters should be limited to 10. This function allows // that to be increased. Set to zero for unlimited. EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { if (typeof n !== 'number' || n < 0 || isNaN(n)) throw new TypeError('"n" argument must be a positive number'); this._maxListeners = n; return this; }; function $getMaxListeners(that) { if (that._maxListeners === undefined) return EventEmitter.defaultMaxListeners; return that._maxListeners; } EventEmitter.prototype.getMaxListeners = function getMaxListeners() { return $getMaxListeners(this); }; // These standalone emit* functions are used to optimize calling of event // handlers for fast cases because emit() itself often has a variable number of // arguments and can be deoptimized because of that. These functions always have // the same number of arguments and thus do not get deoptimized, so the code // inside them can execute faster. function emitNone(handler, isFn, self) { if (isFn) handler.call(self); else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].call(self); } } function emitOne(handler, isFn, self, arg1) { if (isFn) handler.call(self, arg1); else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].call(self, arg1); } } function emitTwo(handler, isFn, self, arg1, arg2) { if (isFn) handler.call(self, arg1, arg2); else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].call(self, arg1, arg2); } } function emitThree(handler, isFn, self, arg1, arg2, arg3) { if (isFn) handler.call(self, arg1, arg2, arg3); else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].call(self, arg1, arg2, arg3); } } function emitMany(handler, isFn, self, args) { if (isFn) handler.apply(self, args); else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].apply(self, args); } } EventEmitter.prototype.emit = function emit(type) { var er, handler, len, args, i, events, domain; var doError = (type === 'error'); events = this._events; if (events) doError = (doError && events.error == null); else if (!doError) return false; domain = this.domain; // If there is no 'error' event listener then throw. if (doError) { er = arguments[1]; if (domain) { if (!er) er = new Error('Uncaught, unspecified "error" event'); er.domainEmitter = this; er.domain = domain; er.domainThrown = false; domain.emit('error', er); } else if (er instanceof Error) { throw er; // Unhandled 'error' event } else { // At least give some kind of context to the user var err = new Error('Uncaught, unspecified "error" event. (' + er + ')'); err.context = er; throw err; } return false; } handler = events[type]; if (!handler) return false; var isFn = typeof handler === 'function'; len = arguments.length; switch (len) { // fast cases case 1: emitNone(handler, isFn, this); break; case 2: emitOne(handler, isFn, this, arguments[1]); break; case 3: emitTwo(handler, isFn, this, arguments[1], arguments[2]); break; case 4: emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]); break; // slower default: args = new Array(len - 1); for (i = 1; i < len; i++) args[i - 1] = arguments[i]; emitMany(handler, isFn, this, args); } return true; }; function _addListener(target, type, listener, prepend) { var m; var events; var existing; if (typeof listener !== 'function') throw new TypeError('"listener" argument must be a function'); events = target._events; if (!events) { events = target._events = new EventHandlers(); target._eventsCount = 0; } else { // To avoid recursion in the case that type === "newListener"! Before // adding it to the listeners, first emit "newListener". if (events.newListener) { target.emit('newListener', type, listener.listener ? listener.listener : listener); // Re-assign `events` because a newListener handler could have caused the // this._events to be assigned to a new object events = target._events; } existing = events[type]; } if (!existing) { // Optimize the case of one listener. Don't need the extra array object. existing = events[type] = listener; ++target._eventsCount; } else { if (typeof existing === 'function') { // Adding the second element, need to change to array. existing = events[type] = prepend ? [listener, existing] : [existing, listener]; } else { // If we've already got an array, just append. if (prepend) { existing.unshift(listener); } else { existing.push(listener); } } // Check for listener leak if (!existing.warned) { m = $getMaxListeners(target); if (m && m > 0 && existing.length > m) { existing.warned = true; var w = new Error('Possible EventEmitter memory leak detected. ' + existing.length + ' ' + type + ' listeners added. ' + 'Use emitter.setMaxListeners() to increase limit'); w.name = 'MaxListenersExceededWarning'; w.emitter = target; w.type = type; w.count = existing.length; emitWarning(w); } } } return target; } function emitWarning(e) { typeof console.warn === 'function' ? console.warn(e) : console.log(e); } EventEmitter.prototype.addListener = function addListener(type, listener) { return _addListener(this, type, listener, false); }; EventEmitter.prototype.on = EventEmitter.prototype.addListener; EventEmitter.prototype.prependListener = function prependListener(type, listener) { return _addListener(this, type, listener, true); }; function _onceWrap(target, type, listener) { var fired = false; function g() { target.removeListener(type, g); if (!fired) { fired = true; listener.apply(target, arguments); } } g.listener = listener; return g; } EventEmitter.prototype.once = function once(type, listener) { if (typeof listener !== 'function') throw new TypeError('"listener" argument must be a function'); this.on(type, _onceWrap(this, type, listener)); return this; }; EventEmitter.prototype.prependOnceListener = function prependOnceListener(type, listener) { if (typeof listener !== 'function') throw new TypeError('"listener" argument must be a function'); this.prependListener(type, _onceWrap(this, type, listener)); return this; }; // emits a 'removeListener' event iff the listener was removed EventEmitter.prototype.removeListener = function removeListener(type, listener) { var list, events, position, i, originalListener; if (typeof listener !== 'function') throw new TypeError('"listener" argument must be a function'); events = this._events; if (!events) return this; list = events[type]; if (!list) return this; if (list === listener || (list.listener && list.listener === listener)) { if (--this._eventsCount === 0) this._events = new EventHandlers(); else { delete events[type]; if (events.removeListener) this.emit('removeListener', type, list.listener || listener); } } else if (typeof list !== 'function') { position = -1; for (i = list.length; i-- > 0;) { if (list[i] === listener || (list[i].listener && list[i].listener === listener)) { originalListener = list[i].listener; position = i; break; } } if (position < 0) return this; if (list.length === 1) { list[0] = undefined; if (--this._eventsCount === 0) { this._events = new EventHandlers(); return this; } else { delete events[type]; } } else { spliceOne(list, position); } if (events.removeListener) this.emit('removeListener', type, originalListener || listener); } return this; }; // Alias for removeListener added in NodeJS 10.0 // https://nodejs.org/api/events.html#events_emitter_off_eventname_listener EventEmitter.prototype.off = function(type, listener){ return this.removeListener(type, listener); }; EventEmitter.prototype.removeAllListeners = function removeAllListeners(type) { var listeners, events; events = this._events; if (!events) return this; // not listening for removeListener, no need to emit if (!events.removeListener) { if (arguments.length === 0) { this._events = new EventHandlers(); this._eventsCount = 0; } else if (events[type]) { if (--this._eventsCount === 0) this._events = new EventHandlers(); else delete events[type]; } return this; } // emit removeListener for all listeners on all events if (arguments.length === 0) { var keys = Object.keys(events); for (var i = 0, key; i < keys.length; ++i) { key = keys[i]; if (key === 'removeListener') continue; this.removeAllListeners(key); } this.removeAllListeners('removeListener'); this._events = new EventHandlers(); this._eventsCount = 0; return this; } listeners = events[type]; if (typeof listeners === 'function') { this.removeListener(type, listeners); } else if (listeners) { // LIFO order do { this.removeListener(type, listeners[listeners.length - 1]); } while (listeners[0]); } return this; }; EventEmitter.prototype.listeners = function listeners(type) { var evlistener; var ret; var events = this._events; if (!events) ret = []; else { evlistener = events[type]; if (!evlistener) ret = []; else if (typeof evlistener === 'function') ret = [evlistener.listener || evlistener]; else ret = unwrapListeners(evlistener); } return ret; }; EventEmitter.listenerCount = function(emitter, type) { if (typeof emitter.listenerCount === 'function') { return emitter.listenerCount(type); } else { return listenerCount.call(emitter, type); } }; EventEmitter.prototype.listenerCount = listenerCount; function listenerCount(type) { var events = this._events; if (events) { var evlistener = events[type]; if (typeof evlistener === 'function') { return 1; } else if (evlistener) { return evlistener.length; } } return 0; } EventEmitter.prototype.eventNames = function eventNames() { return this._eventsCount > 0 ? Reflect.ownKeys(this._events) : []; }; // About 1.5x faster than the two-arg version of Array#splice(). function spliceOne(list, index) { for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) list[i] = list[k]; list.pop(); } function arrayClone(arr, i) { var copy = new Array(i); while (i--) copy[i] = arr[i]; return copy; } function unwrapListeners(arr) { var ret = new Array(arr.length); for (var i = 0; i < ret.length; ++i) { ret[i] = arr[i].listener || arr[i]; } return ret; } /** * Observes changes to a {@link DraftAssistant} by emitting structured events. * * `DraftPublisher` provides an alternative to the {@link DraftViewer} interface for reacting to changes in a draft. * Instead of relying on callbacks, it follows an event-driven approach using `EventEmitter`. * * This allows consumers to subscribe to {@link PublisherEvents}. * * This can be especially useful when integrating with frameworks or systems already based on events. * * @example * ```ts * const assistant = SectionDraftAssistant.handling(...); * const publisher = DraftPublisher.for(assistant); * * publisher.on("draft:updated", (model) => { * console.log("Draft changed:", model); * }); * ``` * * @template Model The type of model the assistant works with. * * @category Draft assistants */ class DraftPublisher extends EventEmitter { static for(anAssistant) { const instance = new this(); anAssistant.accept(instance); return instance; } onDraftChanged(aModel) { this.emit("draft:updated", aModel); } onFailure(aFailedAsserion) { this.emit("assertions:added", aFailedAsserion); } onFailuresReset() { this.emit("assertions:reset"); } } exports.Assertion = Assertion; exports.DraftAssistant = DraftAssistant; exports.DraftPublisher = DraftPublisher; exports.FieldDraftAssistant = FieldDraftAssistant; exports.Inquiry = Inquiry; exports.IntegerDraftAssistant = IntegerDraftAssistant; exports.Requirements = Requirements; exports.Rule = Rule; exports.RuleEvaluation = RuleEvaluation; exports.RuleLabel = RuleLabel; exports.RulesBroken = RulesBroken; exports.Ruleset = Ruleset; exports.SectionDraftAssistant = SectionDraftAssistant; //# sourceMappingURL=index.cjs.map