sfpm-js
Version:
A lightweight, dependency-free, forward-chaining inference engine for managing complex state and logic in a declarative way.
124 lines (107 loc) • 4.03 kB
JavaScript
/**
* @enum {Symbol}
*/
export const Operator = Object.freeze({
Equal: Symbol("Equal"),
GreaterThan: Symbol("GreaterThan"),
LessThan: Symbol("LessThan"),
GreaterThanOrEqual: Symbol("GreaterThanOrEqual"),
LessThanOrEqual: Symbol("LessThanOrEqual"),
NotEqual: Symbol("NotEqual"),
Predicate: Symbol("Predicate"),
});
export const OperatorSymbols = {
[Operator.Equal]: '===',
[Operator.NotEqual]: '!==',
[Operator.GreaterThan]: '>',
[Operator.LessThan]: '<',
[Operator.GreaterThanOrEqual]: '>=',
[Operator.LessThanOrEqual]: '<='
};
const operatorToString = (opSymbol) => Object.keys(Operator).find(key => Operator[key] === opSymbol);
const stringToOperator = (opString) => Operator[opString];
export class Criteria {
/**
* @param {string} factName
* @param {any} expectedValue
* @param {Operator} operator
*/
constructor(factName, expectedValue, operator) {
this.factName = factName;
this.expectedValue = expectedValue;
this.operator = operator;
this.predicate = null;
if (operator === Operator.Predicate) {
if (typeof expectedValue !== "function") {
throw new Error(
"For the Predicate operator, expectedValue must be a function."
);
}
this.predicate = expectedValue;
}
}
/**
* @param {import('./FactSource').FactSource} facts
* @returns {boolean}
*/
evaluate(facts) {
const actualValue = facts.getFact(this.factName);
//console.log(
// `[DEBUG] Evaluating Criteria: FactName='${this.factName}', Expected='${this.expectedValue}', //Actual='${actualValue}'`
//);
if (actualValue === undefined) {
return false;
}
if (this.operator === Operator.Predicate) {
return this.predicate(actualValue);
}
switch (this.operator) {
case Operator.Equal:
return actualValue === this.expectedValue;
case Operator.NotEqual:
return actualValue !== this.expectedValue;
case Operator.GreaterThan:
return actualValue > this.expectedValue;
case Operator.LessThan:
return actualValue < this.expectedValue;
case Operator.GreaterThanOrEqual:
return actualValue >= this.expectedValue;
case Operator.LessThanOrEqual:
return actualValue <= this.expectedValue;
default:
return false;
}
}
/**
* Serializes the Criteria instance to a JSON-compatible object.
* Throws an error if the operator is 'Predicate' as functions cannot be serialized.
* @returns {{factName: string, expectedValue: any, operator: string}}
*/
toJSON() {
if (this.operator === Operator.Predicate) {
throw new Error("Criteria with a Predicate operator cannot be serialized to JSON.");
}
return {
factName: this.factName,
expectedValue: this.expectedValue,
operator: operatorToString(this.operator)
};
}
/**
* Deserializes a JSON object or string into a Criteria instance.
* Throws an error if the operator is 'Predicate'.
* @param {string|object} json - The JSON string or object to deserialize.
* @returns {Criteria} A new instance of the Criteria class.
*/
static fromJSON(json) {
const data = typeof json === 'string' ? JSON.parse(json) : json;
const operator = stringToOperator(data.operator);
if (!operator) {
throw new Error(`Unknown operator found during deserialization: '${data.operator}'`);
}
if (operator === Operator.Predicate) {
throw new Error("Cannot deserialize a Criteria with a Predicate operator.");
}
return new Criteria(data.factName, data.expectedValue, operator);
}
}