Skip to content

Commit

Permalink
Merge branch 'master' into fix-flatten-with-not-op
Browse files Browse the repository at this point in the history
  • Loading branch information
thomaspoignant authored May 3, 2024
2 parents a1b29af + e80a329 commit fb51553
Show file tree
Hide file tree
Showing 5 changed files with 286 additions and 3 deletions.
73 changes: 73 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,76 @@ const ast: Filter = {

assert.deepEqual(stringify(ast), 'userType eq "Employee" and emails[type eq "work" and value co "@example.com"]');
```

### expand
and you can expand an AST's `and` and `[]` nodes.

```typescript
const f = parse(`userType eq "Employee" and emails[type eq "work" or value co "@example.com"]`);

assert.deepEqual(expand(f), {
op:"or",
filters:[
{
op: "and",
filters: [
{
op:"eq",
attrPath:"userType",
compValue:"Employee"
},
{
op:"eq",
attrPath:"emails.type",
compValue:"work"
},
]
},
{
op: "and",
filters: [
{
op:"eq",
attrPath:"userType",
compValue:"Employee"
},
{
op:"co",
attrPath:"emails.value",
compValue:"@example.com"
}
]
}
]
});
```

### flatten
and you can flatten an AST's nodes.

```typescript
const f = parse(
`userType eq "Employee" and (userName eq "bob" and email ew "@example.com")`
);

assert.deepEqual(flatten(f), {
op: "and",
filters: [
{
op: "eq",
attrPath: "userType",
compValue: "Employee",
},
{
op: "eq",
attrPath: "userName",
compValue: "bob",
},
{
op: "ew",
attrPath: "email",
compValue: "@example.com",
},
],
});
```
52 changes: 52 additions & 0 deletions src/expand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Filter, OrExp } from "./index";
import { valfilter } from "./flatten";

const isOr = (f: Filter): f is OrExp => f.op === "or";

function baseProduct<T>(previousProduct: T[][], b: T[]): T[][] {
const result = [];

for (const x of previousProduct) {
for (const y of b) {
result.push([...x, y]);
}
}

return result;
}

function product<T>(arrays: T[][]): T[][] {
const [first, ...rest] = arrays;

let result = first.map((v) => [v]);

for (const array of rest) {
result = baseProduct(result, array);
}

return result;
}

export const expand = (f: Filter): Filter => {
switch (f.op) {
case "[]":
return valfilter(f.valFilter, f.attrPath);
case "or":
return { ...f, filters: f.filters.map(expand) };
case "and": {
const expandedChildren = f.filters.map(expand);
const hasOr = expandedChildren.some(isOr);

if (!hasOr) return { op: "and", filters: expandedChildren };

const distributedAnds: Filter[] = product(
expandedChildren.map((child) =>
child.op === "or" ? child.filters : [child]
)
).map((filters) => expand({ op: "and", filters }));

return { op: "or", filters: distributedAnds };
}
}
return f;
};
11 changes: 9 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as toknizer from "./parser";
import * as tester from "./tester";
import * as fl from "./flatten";
export { expand } from './expand';
export { stringify } from "./stringify";

/** Filter is filter ast object. There is extends [Operation] */
Expand Down Expand Up @@ -32,10 +33,16 @@ export interface Suffix extends Operation {
op: "pr";
attrPath: AttrPath;
}
export interface LogExp extends Operation {
op: "and" | "or";
export interface AndExp extends Operation {
op: "and";
filters: Filter[];
}
export interface OrExp extends Operation {
op: "or";
filters: Filter[];
}
export type LogExp = AndExp | OrExp;

export type AttrPath = string; // [URL ":"]?attrName("."subAttr)*

export const Tester = tester.Tester;
Expand Down
84 changes: 84 additions & 0 deletions test/expand.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { eq, and, or, pr } from "./test_util";
import { Filter, parse, expand } from "../src";
import { assert } from "chai";

function to_s(f: Filter): any {
switch (f.op) {
case "or":
case "and":
return `${f.op}(${f.filters.map(to_s).join(" ")})`;
case "eq":
return `${f.attrPath}=${f.compValue}`;
default:
return JSON.stringify(f);
}
}
const test = (text: string, expected: Filter) => {
it(text, () => {
const actual = expand(parse(text));
assert.equal(to_s(actual), to_s(expected));
});
};
const make: (num: number) => [Filter, string] = (num) => [
eq(`n${num}`, num),
`n${num} eq ${num}`,
];
const [a1, e1] = make(1);
const [a2, e2] = make(2);
const [a3, e3] = make(3);
const [a4, e4] = make(4);
const [a5, e5] = make(5);
const [a6, e6] = make(6);
const [a7, e7] = make(7);

describe("expand", () => {
describe("no-ops", () => {
test(e1, eq("n1", 1));
test("value pr", pr("value"));
test(`${e1} and ${e2}`, and(a1, a2));
test(`${e1} or ${e2}`, or(a1, a2));
});

describe("value paths", () => {
test(`p1[${e1}]`, eq("p1.n1", 1));
test(`p1[p2[${e1}]]`, eq("p1.p2.n1", 1));
});

describe("distributing 'and's", () => {
test(`(${e1} and ${e2}) or ${e3}`, or(and(a1, a2), a3));
test(`(${e1} or ${e2}) and ${e3}`, or(and(a1, a3), and(a2, a3)));
test(`${e1} and (${e2} or ${e3})`, or(and(a1, a2), and(a1, a3)));
test(
`(${e1} or ${e2}) and (${e3} or ${e4})`,
or(and(a1, a3), and(a1, a4), and(a2, a3), and(a2, a4))
);
test(
`${e1} and (${e2} or ${e3}) and ${e4} and (${e5} or ${e6}) and ${e7}`,
or(
and(a1, a2, a4, a5, a7),
and(a1, a2, a4, a6, a7),
and(a1, a3, a4, a5, a7),
and(a1, a3, a4, a6, a7)
)
);
});

describe("complex", () => {
test(
`${e1} and (${e2} and (${e3} or p1[${e4}]))`,
or(and(a1, and(a2, a3)), and(a1, and(a2, eq("p1.n4", 4))))
);
test(
`${e1} and (p1[${e2} or ${e3}])`,
or(and(a1, eq("p1.n2", 2)), and(a1, eq("p1.n3", 3)))
);
test(
`${e1} and (${e2} or (${e3} and (${e4} or ${e5})))`,
or(and(a1, a2), or(and(a1, and(a3, a4)), and(a1, and(a3, a5))))
);
test(
`${e1} or (${e2} and (${e3} or ${e4}))`,
or(a1, or(and(a2, a3), and(a2, a4)))
);
});
});
69 changes: 68 additions & 1 deletion test/readme.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { assert } from "chai";
import { Filter, filter, parse, stringify } from "../src";
import { Filter, filter, parse, stringify, expand, flatten } from "../src";

describe("readme", () => {
it("filter", () => {
Expand Down Expand Up @@ -76,4 +76,71 @@ describe("readme", () => {

assert.deepEqual(stringify(ast), 'userType eq "Employee" and emails[type eq "work" and value co "@example.com"]');
});
it("expand", () => {
const f = parse(
`userType eq "Employee" and emails[type eq "work" or value co "@example.com"]`
);

assert.deepEqual(expand(f), {
op: "or",
filters: [
{
op: "and",
filters: [
{
op: "eq",
attrPath: "userType",
compValue: "Employee",
},
{
op: "eq",
attrPath: "emails.type",
compValue: "work",
},
],
},
{
op: "and",
filters: [
{
op: "eq",
attrPath: "userType",
compValue: "Employee",
},
{
op: "co",
attrPath: "emails.value",
compValue: "@example.com",
},
],
},
],
});
});
it("flatten", () => {
const f = parse(
`userType eq "Employee" and (userName eq "bob" and email ew "@example.com")`
);

assert.deepEqual(flatten(f), {
op: "and",
filters: [
{
op: "eq",
attrPath: "userType",
compValue: "Employee",
},
{
op: "eq",
attrPath: "userName",
compValue: "bob",
},
{
op: "ew",
attrPath: "email",
compValue: "@example.com",
},
],
});
});
});

0 comments on commit fb51553

Please sign in to comment.