Merge parser and expressions into repository
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
ExpressionData,
|
||||
ExpressionDataInterface,
|
||||
Kind,
|
||||
kindStr,
|
||||
} from "./expressiondata";
|
||||
|
||||
export class Array implements ExpressionDataInterface {
|
||||
private v: ExpressionData[] = [];
|
||||
|
||||
constructor(...data: ExpressionData[]) {
|
||||
for (const d of data) {
|
||||
this.add(d);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly kind = Kind.Array;
|
||||
|
||||
public primitive = false;
|
||||
|
||||
coerceString(): string {
|
||||
return kindStr(this.kind);
|
||||
}
|
||||
|
||||
number(): number {
|
||||
return NaN;
|
||||
}
|
||||
|
||||
add(value: ExpressionData) {
|
||||
this.v.push(value);
|
||||
}
|
||||
|
||||
get(index: number): ExpressionData {
|
||||
return this.v[index];
|
||||
}
|
||||
|
||||
values(): ExpressionData[] {
|
||||
return this.v;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ExpressionDataInterface, Kind } from "./expressiondata";
|
||||
|
||||
export class BooleanData implements ExpressionDataInterface {
|
||||
constructor(public readonly value: boolean) {}
|
||||
|
||||
public readonly kind = Kind.Boolean;
|
||||
|
||||
public primitive = true;
|
||||
|
||||
coerceString(): string {
|
||||
if (this.value) {
|
||||
return "true";
|
||||
}
|
||||
|
||||
return "false";
|
||||
}
|
||||
|
||||
number(): number {
|
||||
if (this.value) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Dictionary } from "./dictionary";
|
||||
import { StringData } from "./string";
|
||||
|
||||
describe("dictionary", () => {
|
||||
it("pairs contains all values", () => {
|
||||
const d = new Dictionary();
|
||||
d.add("ABC", new StringData("val"));
|
||||
|
||||
expect(d.pairs()).toEqual([{ key: "ABC", value: new StringData("val") }]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
ExpressionData,
|
||||
ExpressionDataInterface,
|
||||
Kind,
|
||||
kindStr,
|
||||
Pair,
|
||||
} from "./expressiondata";
|
||||
|
||||
export class Dictionary implements ExpressionDataInterface {
|
||||
private keys: string[] = [];
|
||||
private v: ExpressionData[] = [];
|
||||
private indexMap: { [key: string]: number } = {};
|
||||
|
||||
constructor(...pairs: Pair[]) {
|
||||
for (const p of pairs) {
|
||||
this.add(p.key, p.value);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly kind = Kind.Dictionary;
|
||||
|
||||
public primitive = false;
|
||||
|
||||
coerceString(): string {
|
||||
return kindStr(this.kind);
|
||||
}
|
||||
|
||||
number(): number {
|
||||
return NaN;
|
||||
}
|
||||
|
||||
add(key: string, value: ExpressionData) {
|
||||
if (this.indexMap[key.toLowerCase()]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.keys.push(key);
|
||||
this.v.push(value);
|
||||
this.indexMap[key.toLowerCase()] = this.v.length - 1;
|
||||
}
|
||||
|
||||
get(key: string): ExpressionData | undefined {
|
||||
const index = this.indexMap[key.toLowerCase()];
|
||||
if (index === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.v[index];
|
||||
}
|
||||
|
||||
values(): ExpressionData[] {
|
||||
return this.v;
|
||||
}
|
||||
|
||||
pairs(): Pair[] {
|
||||
const result: Pair[] = [];
|
||||
|
||||
for (const key of this.keys) {
|
||||
result.push({ key, value: this.v[this.indexMap[key.toLowerCase()]] });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export function isDictionary(x: ExpressionData): x is Dictionary {
|
||||
return x.kind === Kind.Dictionary;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Dictionary } from "./dictionary";
|
||||
import { Null } from "./null";
|
||||
import { Array } from "./array";
|
||||
import { StringData } from "./string";
|
||||
import { NumberData } from "./number";
|
||||
import { BooleanData } from "./boolean";
|
||||
|
||||
export enum Kind {
|
||||
String = 0,
|
||||
Array,
|
||||
Dictionary,
|
||||
Boolean,
|
||||
Number,
|
||||
CaseSensitiveDictionary,
|
||||
Null,
|
||||
}
|
||||
|
||||
export function kindStr(k: Kind): string {
|
||||
switch (k) {
|
||||
case Kind.Array:
|
||||
return "Array";
|
||||
case Kind.Boolean:
|
||||
return "Boolean";
|
||||
case Kind.Null:
|
||||
return "Null";
|
||||
case Kind.Number:
|
||||
return "Number";
|
||||
case Kind.Dictionary:
|
||||
return "Object";
|
||||
case Kind.String:
|
||||
return "String";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
export interface ExpressionDataInterface {
|
||||
kind: Kind;
|
||||
primitive: boolean;
|
||||
|
||||
coerceString(): string;
|
||||
|
||||
number(): number;
|
||||
}
|
||||
|
||||
export type ExpressionData =
|
||||
| Array
|
||||
| Dictionary
|
||||
| StringData
|
||||
| BooleanData
|
||||
| NumberData
|
||||
| Null;
|
||||
|
||||
export type Pair = {
|
||||
key: string;
|
||||
value: ExpressionData;
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
export { Array } from "./array";
|
||||
export { BooleanData } from "./boolean";
|
||||
export { Dictionary } from "./dictionary";
|
||||
export { ExpressionData, Kind } from "./expressiondata";
|
||||
export { Null } from "./null";
|
||||
export { NumberData } from "./number";
|
||||
export { replacer } from "./replacer";
|
||||
export { reviver } from "./reviver";
|
||||
export { StringData } from "./string";
|
||||
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
ExpressionData,
|
||||
ExpressionDataInterface,
|
||||
Kind,
|
||||
} from "./expressiondata";
|
||||
|
||||
export class Null implements ExpressionDataInterface {
|
||||
constructor() {}
|
||||
|
||||
public readonly kind = Kind.Null;
|
||||
|
||||
public primitive = true;
|
||||
|
||||
coerceString(): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
number(): number {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ExpressionDataInterface, Kind } from "./expressiondata";
|
||||
|
||||
export class NumberData implements ExpressionDataInterface {
|
||||
constructor(public readonly value: number) {}
|
||||
|
||||
public readonly kind = Kind.Number;
|
||||
|
||||
public primitive = true;
|
||||
|
||||
coerceString(): string {
|
||||
if (this.value === -0) {
|
||||
return "0";
|
||||
}
|
||||
|
||||
// Workaround to limit the precision to at most 15 digits. Format the number to a string, then parse
|
||||
// it back to a number to remove trailing zeroes to prevent numbers to be converted to 1.200000000...
|
||||
return (+this.value.toFixed(15)).toString();
|
||||
}
|
||||
|
||||
number(): number {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Array } from "./array";
|
||||
import { Dictionary } from "./dictionary";
|
||||
import { Null } from "./null";
|
||||
import { NumberData } from "./number";
|
||||
import { replacer } from "./replacer";
|
||||
import { StringData } from "./string";
|
||||
|
||||
describe("replacer", () => {
|
||||
it("null", () => {
|
||||
expect(JSON.stringify(new Null(), replacer, " ")).toEqual("null");
|
||||
});
|
||||
|
||||
it("array", () => {
|
||||
expect(
|
||||
JSON.stringify(
|
||||
new Array(new StringData("a"), new StringData("b")),
|
||||
replacer,
|
||||
" "
|
||||
)
|
||||
).toEqual('[\n "a",\n "b"\n]');
|
||||
});
|
||||
|
||||
it("dictionary", () => {
|
||||
expect(
|
||||
JSON.stringify(
|
||||
new Dictionary({ key: "a", value: new NumberData(42) }),
|
||||
replacer,
|
||||
" "
|
||||
)
|
||||
).toEqual('{\n "a": 42\n}');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Array } from "./array";
|
||||
import { BooleanData } from "./boolean";
|
||||
import { Dictionary } from "./dictionary";
|
||||
import { Null } from "./null";
|
||||
import { NumberData } from "./number";
|
||||
import { StringData } from "./string";
|
||||
|
||||
/**
|
||||
* Replacer can be passed to JSON.stringify to convert an ExpressionData object into plain JSON
|
||||
*
|
||||
* See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#replacer
|
||||
*/
|
||||
export function replacer(key: string, value: any): any {
|
||||
if (value instanceof Null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value instanceof BooleanData) {
|
||||
return value.value;
|
||||
}
|
||||
|
||||
if (value instanceof NumberData) {
|
||||
return value.number();
|
||||
}
|
||||
|
||||
if (value instanceof StringData) {
|
||||
return value.coerceString();
|
||||
}
|
||||
|
||||
if (value instanceof Array) {
|
||||
return value.values();
|
||||
}
|
||||
|
||||
if (value instanceof Dictionary) {
|
||||
const pairs = value.pairs();
|
||||
|
||||
const r: any = {};
|
||||
for (const p of pairs) {
|
||||
r[p.key] = p.value;
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Array } from "./array";
|
||||
import { BooleanData } from "./boolean";
|
||||
import { Dictionary } from "./dictionary";
|
||||
import { ExpressionData } from "./expressiondata";
|
||||
import { Null } from "./null";
|
||||
import { NumberData } from "./number";
|
||||
import { reviver } from "./reviver";
|
||||
import { StringData } from "./string";
|
||||
|
||||
describe("reviver", () => {
|
||||
const tests: {
|
||||
name: string;
|
||||
data: string;
|
||||
want: ExpressionData;
|
||||
}[] = [
|
||||
{
|
||||
name: "null",
|
||||
data: "null",
|
||||
want: new Null(),
|
||||
},
|
||||
{
|
||||
name: "number",
|
||||
data: "1",
|
||||
want: new NumberData(1),
|
||||
},
|
||||
{
|
||||
name: "string",
|
||||
data: `"a"`,
|
||||
want: new StringData("a"),
|
||||
},
|
||||
{
|
||||
name: "true boolean",
|
||||
data: "true",
|
||||
want: new BooleanData(true),
|
||||
},
|
||||
{
|
||||
name: "false boolean",
|
||||
data: "false",
|
||||
want: new BooleanData(false),
|
||||
},
|
||||
{
|
||||
name: "array",
|
||||
data: `[1,2,3]`,
|
||||
want: new Array(new NumberData(1), new NumberData(2), new NumberData(3)),
|
||||
},
|
||||
{
|
||||
name: "nested array",
|
||||
data: `[1,2,[3,4],5]`,
|
||||
want: new Array(
|
||||
new NumberData(1),
|
||||
new NumberData(2),
|
||||
new Array(new NumberData(3), new NumberData(4)),
|
||||
new NumberData(5)
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "complex array",
|
||||
data: `[{"a":[true,2]},{"b":"three"},{"c":null}]`,
|
||||
want: new Array(
|
||||
new Dictionary({
|
||||
key: "a",
|
||||
value: new Array(new BooleanData(true), new NumberData(2)),
|
||||
}),
|
||||
new Dictionary({ key: "b", value: new StringData("three") }),
|
||||
new Dictionary({ key: "c", value: new Null() })
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "dictionary",
|
||||
data: `{ "object1": {} }`,
|
||||
want: new Dictionary({
|
||||
key: "object1",
|
||||
value: new Dictionary(),
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
test.each(tests)(
|
||||
"$name",
|
||||
({ data, want }: { data: string; want: ExpressionData }) => {
|
||||
expect(JSON.parse(data, reviver)).toEqual(want);
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Array as dArray } from "./array";
|
||||
import { BooleanData } from "./boolean";
|
||||
import { Dictionary } from "./dictionary";
|
||||
import { ExpressionData, Pair } from "./expressiondata";
|
||||
import { Null } from "./null";
|
||||
import { NumberData } from "./number";
|
||||
import { StringData } from "./string";
|
||||
|
||||
/**
|
||||
* Reviver can be passed to `JSON.parse` to convert plain JSON into an `ExpressionData` object.
|
||||
*
|
||||
* See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#reviver
|
||||
*/
|
||||
export function reviver(key: string, val: any): ExpressionData {
|
||||
if (val === null) {
|
||||
return new Null();
|
||||
}
|
||||
|
||||
if (typeof val === "string") {
|
||||
return new StringData(val);
|
||||
}
|
||||
|
||||
if (typeof val === "number") {
|
||||
return new NumberData(val);
|
||||
}
|
||||
|
||||
if (typeof val === "boolean") {
|
||||
return new BooleanData(val as boolean);
|
||||
}
|
||||
|
||||
if (Array.isArray(val)) {
|
||||
return new dArray(...val);
|
||||
}
|
||||
|
||||
if (typeof val === "object") {
|
||||
return new Dictionary(
|
||||
...Object.keys(val).map(
|
||||
(k) =>
|
||||
({
|
||||
key: k,
|
||||
value: val[k],
|
||||
} as Pair)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Pass through value
|
||||
return val;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ExpressionDataInterface, Kind } from "./expressiondata";
|
||||
|
||||
export class StringData implements ExpressionDataInterface {
|
||||
constructor(public readonly value: string) {}
|
||||
|
||||
public readonly kind = Kind.String;
|
||||
|
||||
public primitive = true;
|
||||
|
||||
coerceString(): string {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
number(): number {
|
||||
return Number(this.value);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user