Skip to content

Commit

Permalink
feat(core): Bugfixes
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewbrg committed Nov 15, 2024
1 parent 19dfdef commit 9edab51
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 82 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "rulepilot",
"version": "1.4.0",
"version": "1.4.1",
"description": "Rule parsing engine for JSON rules",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
27 changes: 17 additions & 10 deletions src/services/introspector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ interface ConditionResult {
export class Introspector {
#objectDiscovery: ObjectDiscovery = new ObjectDiscovery();

introspect(
introspect<R>(
rule: Rule,
constraint: Omit<Constraint, "operator">,
subjects: string[]
): IntrospectionResult {
): IntrospectionResult<R>[] {
// We care about all the possible values for the subjects which will satisfy
// the rule if the rule is tested against the constraint provided.

Expand Down Expand Up @@ -76,29 +76,34 @@ export class Introspector {
// At this point the search becomes as follows: What are the possible values for the
// subjects which will satisfy the rule if the rule is tested against the constraint provided.

const results = {};
const results: IntrospectionResult<R>[] = [];

// We introspect the conditions to determine the possible values for the subjects
for (const condition of conditions) {
const { values } = this.#introspectConditions(condition, constraint);
if (!values) continue;

const key = condition.result ?? "default";
const map: Map<string, Omit<Constraint, "field">[]> = new Map();

// Merge the results maintaining the uniqueness of the values
for (const [field, constraints] of values.entries()) {
if (!subjects.includes(field)) continue;
const temp = map.get(field) ?? [];
if (!temp.length) map.set(field, temp);

const set = new Set([...(results[field] ?? [])]);
for (const constraint of constraints) {
set.add({ value: constraint.value, operator: constraint.operator });
temp.push({ value: constraint.value, operator: constraint.operator });
}
}

if (set.size) {
results[key] = {};
results[key][field] = Array.from(set);
}
const s = [];
for (const [subject, values] of map.entries()) {
if (!values.length) continue;
s.push({ subject, values });
}

if (s.length) results.push({ result: key, subjects: s });
}

return results;
Expand Down Expand Up @@ -332,7 +337,9 @@ export class Introspector {
clone[type].splice(i, 1);

// If the node is now empty, we can prune it
if (Array.isArray(clone[type]) && !clone[type].length) return null;
if (Array.isArray(clone[type]) && !clone[type].length && !clone.result)
return null;

continue;
}

Expand Down
2 changes: 2 additions & 0 deletions src/services/object-discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export class ObjectDiscovery {
* @param condition The condition to check.
*/
conditionType(condition: Condition): ConditionType | null {
if (!this.isObject(condition)) return null;

if ("any" in condition) return "any";
if ("all" in condition) return "all";
if ("none" in condition) return "none";
Expand Down
12 changes: 6 additions & 6 deletions src/services/rule-pilot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,18 +98,18 @@ export class RulePilot {
* @throws RuleError if the rule is invalid
* @throws RuleTypeError if the rule is not granular
*/
introspect(
introspect<R = string>(
rule: Rule,
constraint: Omit<Constraint, "operator">,
subjects: string[]
): IntrospectionResult {
): IntrospectionResult<R>[] {
// Before we proceed with the rule, we should validate it.
const validationResult = this.validate(rule);
if (!validationResult.isValid) {
throw new RuleError(validationResult);
}

return this.#introspector.introspect(rule, constraint, subjects);
return this.#introspector.introspect<R>(rule, constraint, subjects);
}

/**
Expand Down Expand Up @@ -162,12 +162,12 @@ export class RulePilot {
* @throws RuleError if the rule is invalid
* @throws RuleTypeError if the rule is not granular
*/
static introspect(
static introspect<R = string>(
rule: Rule,
constraint: Omit<Constraint, "operator">,
subjects: string[]
): IntrospectionResult {
return RulePilot.#rulePilot.introspect(rule, constraint, subjects);
): IntrospectionResult<R>[] {
return RulePilot.#rulePilot.introspect<R>(rule, constraint, subjects);
}

/**
Expand Down
12 changes: 9 additions & 3 deletions src/types/introspection.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { Constraint } from "./rule";

export interface IntrospectionResult {
[key: string]: Omit<Constraint, "field">[];
}
type IntrospectionResultSubject = {
subject: string;
values: Omit<Constraint, "field">[];
};

export type IntrospectionResult<R> = {
result: R;
subjects: IntrospectionResultSubject[];
};
197 changes: 139 additions & 58 deletions test/introspector.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,114 +26,195 @@ describe("RulePilot introspector correctly", () => {
it("Introspects valid rules", async () => {
let sub: string[] = ["Leverage"];
let con: any = { field: "CountryIso", value: "GB" };
expect(RulePilot.introspect(valid3Json, con, sub)).toEqual({
3: {
Leverage: [
{ value: 1000, operator: ">=" },
{ value: 200, operator: "<" },
expect(RulePilot.introspect(valid3Json, con, sub)).toEqual([
{
result: 3,
subjects: [
{
subject: "Leverage",
values: [
{ operator: ">=", value: 1000 },
{ operator: "<", value: 200 },
],
},
],
},
});
]);

sub = ["Monetization"];
con = { field: "Leverage", value: 199 };
expect(RulePilot.introspect(valid3Json, con, sub)).toEqual({
3: { Monetization: [{ value: "Real", operator: "==" }] },
});
expect(RulePilot.introspect(valid3Json, con, sub)).toEqual([
{
result: 3,
subjects: [
{
subject: "Monetization",
values: [{ operator: "==", value: "Real" }],
},
],
},
]);

sub = ["Monetization"];
con = { field: "Leverage", value: 200 };
expect(RulePilot.introspect(valid3Json, con, sub)).toEqual({});
expect(RulePilot.introspect(valid3Json, con, sub)).toEqual([]);

sub = ["Leverage"];
con = { field: "Category", value: 22 };
expect(RulePilot.introspect(valid4Json, con, sub)).toEqual({
3: {
Leverage: [
{ value: 1000, operator: "==" },
{ value: 500, operator: "==" },
{ value: 200, operator: "<" },
expect(RulePilot.introspect(valid4Json, con, sub)).toEqual([
{
result: 3,
subjects: [
{
subject: "Leverage",
values: [
{ operator: "==", value: 1000 },
{ operator: "==", value: 500 },
{ operator: "<", value: 200 },
],
},
],
},
});
]);

sub = ["Category"];
con = { field: "Leverage", value: 199 };
expect(RulePilot.introspect(valid4Json, con, sub)).toEqual({
3: {
Category: [
{ value: 1000, operator: ">=" },
{ value: 22, operator: "==" },
{ value: 11, operator: "==" },
{ value: 12, operator: "==" },
expect(RulePilot.introspect(valid4Json, con, sub)).toEqual([
{
result: 3,
subjects: [
{
subject: "Category",
values: [
{ operator: ">=", value: 1000 },
{ operator: "==", value: 22 },
{ operator: "==", value: 11 },
{ operator: "==", value: 12 },
],
},
],
},
{
result: 4,
subjects: [
{
subject: "Category",
values: [{ operator: "==", value: "Islamic" }],
},
],
},
4: { Category: [{ value: "Islamic", operator: "==" }] },
});
]);

sub = ["IsUnder18"];
con = { field: "Category", value: "Islamic" };
expect(RulePilot.introspect(valid6Json, con, sub)).toEqual({});
expect(RulePilot.introspect(valid6Json, con, sub)).toEqual([]);

sub = ["IsUnder18"];
con = { field: "Category", value: 122 };
expect(RulePilot.introspect(valid6Json, con, sub)).toEqual({});
expect(RulePilot.introspect(valid6Json, con, sub)).toEqual([]);

sub = ["Monetization"];
con = { field: "Category", value: 11 };
expect(RulePilot.introspect(valid6Json, con, sub)).toEqual({});
expect(RulePilot.introspect(valid6Json, con, sub)).toEqual([]);

sub = ["Leverage"];
con = { field: "CountryIso", value: "DK" };
expect(RulePilot.introspect(valid7Json, con, sub)).toEqual({
3: {
Leverage: [
{ value: 1000, operator: "<" },
{ value: 200, operator: ">=" },
expect(RulePilot.introspect(valid7Json, con, sub)).toEqual([
{
result: 3,
subjects: [
{
subject: "Leverage",
values: [
{ operator: "<", value: 1000 },
{ operator: ">=", value: 200 },
],
},
],
},
});
]);

sub = ["Leverage"];
con = { field: "CountryIso", value: "FI" };
expect(RulePilot.introspect(valid7Json, con, sub)).toEqual({});
expect(RulePilot.introspect(valid7Json, con, sub)).toEqual([]);

sub = ["OtherType"];
con = { field: "Leverage", value: 999 };
expect(RulePilot.introspect(valid8Json, con, sub)).toEqual({
3: { OtherType: [{ value: ["Live", "Fun"], operator: "in" }] },
});
expect(RulePilot.introspect(valid8Json, con, sub)).toEqual([
{
result: 3,
subjects: [
{
subject: "OtherType",
values: [{ operator: "in", value: ["Live", "Fun"] }],
},
],
},
]);

sub = ["totalCheckoutPrice"];
con = { field: "country", value: "SE" };
expect(RulePilot.introspect(valid9Json, con, sub)).toEqual({});
expect(RulePilot.introspect(valid9Json, con, sub)).toEqual([]);

sub = ["Leverage", "Monetization"];
con = { field: "Category", value: 22 };
expect(RulePilot.introspect(subRulesValid2Json, con, sub)).toEqual({
"3": {
Leverage: [
{ value: 1000, operator: "==" },
{ value: 500, operator: "==" },
{ value: "Demo", operator: "==" },
expect(RulePilot.introspect(subRulesValid2Json, con, sub)).toEqual([
{
result: 3,
subjects: [
{
subject: "Leverage",
values: [
{ operator: "==", value: 1000 },
{ operator: "==", value: 500 },
{ operator: "==", value: "Demo" },
],
},
],
},
"12": { Monetization: [{ value: "Real", operator: "==" }] },
"13": {
Leverage: [
{ value: 1000, operator: "==" },
{ value: 500, operator: "==" },
{ value: "Demo", operator: "==" },
{
result: 15,
subjects: [
{
subject: "Leverage",
values: [
{ operator: "==", value: 1000 },
{ operator: "==", value: 500 },
{ operator: "==", value: "Demo" },
],
},
],
},
"15": {
Leverage: [
{ value: 1000, operator: "==" },
{ value: 500, operator: "==" },
{ value: "Demo", operator: "==" },
{
result: 12,
subjects: [
{
subject: "Leverage",
values: [
{ operator: ">", value: 400 },
{ operator: "==", value: "Demo" },
],
},
{
subject: "Monetization",
values: [{ operator: "==", value: "Real" }],
},
],
},
});
{
result: 13,
subjects: [
{
subject: "Leverage",
values: [
{ operator: "==", value: 1000 },
{ operator: "==", value: 500 },
{ operator: "==", value: "Demo" },
],
},
],
},
]);
});

it("Sanitizes results correctly", async () => {
Expand Down
Loading

0 comments on commit 9edab51

Please sign in to comment.