self-assert
Version:
A small TypeScript library for designing models with built-in validity.
1,496 lines (1,413 loc) • 46.2 kB
JavaScript
'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