Skip to content

codehz/json-expr

Repository files navigation

@codehz/json-expr

一个可序列化为 JSON 的表达式 DSL 库,提供类型安全的表达式构建、编译和求值。

npm version

特性

  • 🎯 类型安全 - 使用 TypeScript 泛型进行完整的编译时类型推导
  • 📦 可序列化 - 编译后的表达式为纯 JSON 格式,易于传输和存储
  • 🔧 灵活的表达式 - 支持任意 JavaScript 表达式,包括函数调用和对象属性访问
  • 高性能 - 优化的表达式编译和执行
  • 🧩 可组合 - 表达式可以相互组合,形成复杂的计算树
  • 🔄 短路求值 - 支持 &&||?? 和三元表达式的控制流优化
  • 📝 内联优化 - 自动内联只被引用一次的子表达式
  • 🔗 Proxy 变量系统 - 支持链式属性访问和方法调用,如 config.timeoutuser.profile.name
  • 🎛️ Lambda 表达式 - 类型安全的数组方法支持(map、filter、reduce 等)
  • 🛡️ 错误检测 - 编译时检测未定义变量和类型错误

快速开始

安装

bun install @codehz/json-expr

基本用法

import { variable, expr, compile, evaluate, t, lambda, wrap } from "@codehz/json-expr";

// 定义类型化变量(使用 TypeScript 泛型)
const x = variable<number>();
const y = variable<number>();

// 构建表达式
const sum = expr({ x, y })("x + y");
const product = expr({ x, y })("x * y");
const result = expr({ sum, product })("sum + product");

// 编译表达式(可序列化为 JSON)
const compiled = compile(result, { x, y });
// => [["x", "y"], "$[0]+$[1]", "$[0]*$[1]", "$[2]+$[3]"]

// 执行编译后的表达式
const value = evaluate(compiled, { x: 2, y: 3 });
// => 11  (2+3 + 2*3 = 5 + 6 = 11)

// 使用模板字符串
const name = variable<string>();
const greeting = t`Hello, ${name}!`;

// 使用 lambda 表达式
const numbers = variable<number[]>();
const doubled = numbers.map(lambda<[number], number>((n) => expr({ n })("n * 2")));

// 使用 wrap 包装静态值
const pattern = wrap(/^[a-z]+$/i);
const input = variable<string>();
const isValid = pattern.test(input);

核心概念

Variable(变量)

变量是表达式中的占位符,使用 TypeScript 泛型定义其类型。

const age = variable<number>();
const name = variable<string>();
const config = variable<{
  debug: boolean;
  timeout: number;
}>();

Expression(表达式)

表达式对变量或其他表达式进行运算,使用字符串形式描述。

const x = variable<number>();
const y = variable<number>();

// 简单表达式
const sum = expr({ x, y })("x + y");

// 复杂表达式(可以使用 JS 语言特性)
const abs = expr({ x })("Math.abs(x)");
const conditional = expr({ x, y })("x > y ? x : y");
const array = expr({ x, y })("[x, y].filter(v => v > 0)");

CompiledData(编译数据)

编译后的表达式为 JSON 数组格式:

// 格式: [variableNames, expression1, expression2, ...]
// 其中 $N 用于引用之前的变量或表达式

const compiled = compile(result, { x, y });
// [["x", "y"], "$[0]+$[1]", "$[0]*$[1]", "$[2]+$[3]"]

API 参考

variable<T>(): Variable<T>

创建一个类型化变量。

参数: 无(类型通过泛型参数 T 指定)

返回值: Variable 对象,支持属性访问和方法调用

示例:

const num = variable<number>();
const str = variable<string>();
const config = variable<{ timeout: number }>();

// 支持链式属性访问
const timeout = config.timeout; // 自动转换为表达式

expr<TContext>(context: TContext): (source: string) => Expression<TContext, TResult>

创建表达式,采用柯里化设计以支持完整的类型推导。

参数:

  • context - 上下文对象,包含变量和/或其他表达式的映射

返回值: 函数,接收表达式源码字符串并返回 Expression 对象

示例:

const x = variable<number>();
const y = variable<number>();

const sum = expr({ x, y })("x + y");
const result = expr({ sum, x })("sum * x");

compile<TResult>(expression: Expression<any, TResult>, variables: Record<string, Variable<any>> | Variable<any>[], options?: CompileOptions): CompiledData

将表达式树编译为可序列化的 JSON 结构。

参数:

  • expression - 要编译的表达式
  • variables - 表达式中使用的所有变量映射或数组
  • options - 编译选项(可选)
    • inline?: boolean - 是否启用内联优化,将只被引用一次的子表达式内联到使用位置(默认:true)

返回值: CompiledData 数组

示例:

const x = variable<number>();
const y = variable<number>();
const sum = expr({ x, y })("x + y");
const product = expr({ x, y })("x * y");
const result = expr({ sum, product })("sum + product");

const compiled = compile(result, { x, y });
// [["x", "y"], "$[0]+$[1]", "$[0]*$[1]", "$[2]+$[3]"]

// 禁用内联优化
const noInline = compile(result, { x, y }, { inline: false });
// [["x", "y"], "$[0]+$[1]", "$[0]*$[1]", "$[2]+$[3]"]

evaluate<TResult>(data: CompiledData, values: Record<string, unknown>): TResult

执行编译后的表达式。

参数:

  • data - 编译后的表达式数据
  • values - 变量值映射,对应编译数据中的变量名顺序

返回值: 表达式计算结果

示例:

const x = variable<number>();
const y = variable<number>();
const sum = expr({ x, y })("x + y");
const product = expr({ x, y })("x * y");
const result = expr({ sum, product })("sum + product");

const compiled = compile(result, { x, y });
const value = evaluate(compiled, { x: 5, y: 3 });
// => 23  ((5+3) + (5*3) = 8 + 15 = 23)

t(strings: TemplateStringsArray, ...values: unknown[]): Proxify<string>

使用标签模板函数创建包含变量的字符串表达式。

参数:

  • strings - 模板字符串的静态部分
  • values - 模板中插值的变量和表达式

返回值: 字符串类型的 Proxy Expression

示例:

const name = variable<string>();
const count = variable<number>();

const greeting = t`Hello, ${name}!`;
const message = t`You have ${count} items.`;

const compiled = compile(greeting, { name });
const result = evaluate(compiled, { name: "Alice" });
// => "Hello, Alice!"

lambda<Args, R>(builder: LambdaBuilder<Args, R>): Lambda<Args, R>

创建类型安全的 lambda 表达式,用于数组方法(map、filter、reduce 等)。

参数:

  • builder - Lambda 构建函数,接收参数代理,返回函数体表达式

返回值: Lambda 表达式,可在数组方法中使用

示例:

import { lambda } from "@codehz/json-expr";

// 单参数 lambda
const numbers = variable<number[]>();
const doubled = numbers.map(lambda<[number], number>((n) => expr({ n })("n * 2")));

const compiled = compile(doubled, { numbers });
const result = evaluate(compiled, { numbers: [1, 2, 3] });
// => [2, 4, 6]

// 多参数 lambda(reduce)
const sum = numbers.reduce(
  lambda<[number, number], number>((acc, val) => expr({ acc, val })("acc + val")),
  0
);

// 捕获外部变量
const multiplier = variable<number>();
const scaled = numbers.map(lambda<[number], number>((n) => expr({ n, multiplier })("n * multiplier")));

wrap<T>(value: T): Proxify<T>

将静态值包装为 Proxy Expression,使其可以像 Variable 一样调用方法和访问属性。

参数:

  • value - 要包装的静态值(支持原始值、对象、数组、Date、RegExp、BigInt、URL、Map、Set、TypedArray 等)

返回值: Proxy Expression,可以继续链式调用

示例:

// 包装 RegExp
const pattern = wrap(/^[a-z]+$/i);
const input = variable<string>();
const isValid = pattern.test(input);

const compiled = compile(isValid, { input });
evaluate(compiled, { input: "hello" }); // => true
evaluate(compiled, { input: "hello123" }); // => false

// 包装 Date
const now = wrap(new Date("2024-01-01"));
const year = now.getFullYear();

// 包装数组
const staticNumbers = wrap([1, 2, 3, 4, 5]);
const x = variable<number>();
const doubled = staticNumbers.map(lambda((n: number) => expr({ n, x })("n * x")));

// 包装对象
const config = wrap({ port: 8080, host: "localhost" });
const port = config.port; // 直接访问属性

// 包装 Map
const map = wrap(
  new Map([
    ["a", 1],
    ["b", 2],
  ])
);
const key = variable<string>();
const value = map.get(key);

// 链式调用
const text = wrap("  hello world  ");
const result = text.trim().toUpperCase().replace("HELLO", "HI");
// => "HI WORLD"

高级用法

包装静态值(wrap)

wrap() 函数可以将任意静态值转换为 Proxy Expression,使其可以像 Variable 一样调用方法和访问属性。这在需要对常量值执行操作时非常有用。

基本用法:

// 不使用 wrap(传统方式)
interface Validator {
  match(text: string, pattern: RegExp): boolean;
}
const validator = variable<Validator>();
const result = validator.match("hello", /^[a-z]+$/i);

// 使用 wrap(推荐方式)
const pattern = wrap(/^[a-z]+$/i);
const input = variable<string>();
const result = pattern.test(input);

支持的类型:

// 原始值
const num = wrap(42);
const str = wrap("hello");
const bool = wrap(true);

// Date 和 RegExp
const date = wrap(new Date("2024-01-01"));
const year = date.getFullYear();

const regex = wrap(/\d+/g);
const text = variable<string>();
const matches = text.match(regex);

// BigInt
const bigNum = wrap(123456789n);
const x = variable<bigint>();
const sum = expr({ bigNum, x })("bigNum + x");

// URL
const url = wrap(new URL("https://example.com/path"));
const host = url.hostname;
const port = url.port;

// Map 和 Set
const map = wrap(
  new Map([
    ["key1", 100],
    ["key2", 200],
  ])
);
const key = variable<string>();
const value = map.get(key);

const set = wrap(new Set([1, 2, 3]));
const num = variable<number>();
const has = set.has(num);

// TypedArray
const arr = wrap(new Uint8Array([10, 20, 30]));
const index = variable<number>();
const value = expr({ arr, index })("arr[index]");

// 数组和对象
const numbers = wrap([1, 2, 3, 4, 5]);
const multiplier = variable<number>();
const scaled = numbers.map(lambda((n: number) => expr({ n, multiplier })("n * multiplier")));

const config = wrap({ port: 8080, host: "localhost" });
const port = config.port;

链式调用:

const text = wrap("  Hello, World!  ");
const result = text.trim().toLowerCase().replace("world", "universe");
// => "hello, universe!"

与 variable 结合:

const staticData = wrap({ users: ["alice", "bob", "charlie"] });
const index = variable<number>();
const username = expr({ staticData, index })("staticData.users[index]");

const compiled = compile(username, { index });
evaluate(compiled, { index: 1 }); // => "bob"

Proxy 变量系统

variable() 创建的变量是 Proxy 对象,支持链式属性访问和方法调用,所有操作都会自动转换为表达式。

属性访问:

const config = variable<{
  timeout: number;
  retries: number;
  database: {
    host: string;
    port: number;
  };
}>();

// 链式属性访问
const timeout = config.timeout; // 自动转换为表达式
const dbHost = config.database.host; // 支持嵌套访问

const compiled = compile(timeout, { config });
const result = evaluate(compiled, {
  config: { timeout: 5000, retries: 3, database: { host: "localhost", port: 5432 } },
});
// => 5000

方法调用:

const calculator = variable<{
  add(a: number, b: number): number;
  multiply(x: number, y: number): number;
}>();

// 方法调用
const sum = calculator.add(1, 2);
const product = calculator.multiply(5, 3);

// 链式方法调用
const builder = variable<{
  setName(name: string): typeof builder;
  build(): { name: string };
}>();
const result = builder.setName("test").build();

// 编译并执行
const compiled = compile(sum, { calculator });
const value = evaluate(compiled, {
  calculator: {
    add: (a, b) => a + b,
    multiply: (x, y) => x * y,
  },
});
// => 3

数组方法:

数组变量支持所有标准数组方法,并自动处理类型推导:

const numbers = variable<number[]>();
const users = variable<{ id: number; name: string }[]>();

// map
const doubled = numbers.map((n) => expr({ n })("n * 2"));

// filter
const activeUsers = users.filter((u) => expr({ u })("u.active"));

// reduce
const sum = numbers.reduce(
  lambda<[number, number], number>((acc, val) => expr({ acc, val })("acc + val")),
  0
);

// find, some, every, sort 等
const firstMatch = users.find((u) => expr({ u })("u.id === 1"));
const hasAdmins = users.some((u) => expr({ u })("u.role === 'admin'"));
const allActive = users.every((u) => expr({ u })("u.active"));
const sorted = numbers.toSorted(lambda<[number, number], number>((a, b) => expr({ a, b })("a - b")));

内置全局对象

表达式中可以直接使用以下内置对象(无需在上下文中定义):

  • Math, JSON, Date, RegExp
  • Number, String, Boolean, Array, Object
  • undefined, NaN, Infinity
  • isNaN, isFinite, parseInt, parseFloat
const x = variable<number>();

const sqrtExpr = expr({ x })("Math.sqrt(x)");
const compiled = compile(sqrtExpr, { x });
const result = evaluate(compiled, { x: 16 });
// => 4

支持的运算符和语法

算术运算符:

  • +, -, *, /, %, ** (幂运算)

比较运算符:

  • ==, ===, !=, !==, <, >, <=, >=

逻辑运算符:

  • &&, ||, !, ?? (空值合并)

位运算符:

  • &, |, ^, ~, <<, >>, >>>

其他运算符:

  • ? : (三元表达式)
  • in (属性存在检查)
  • instanceof (类型检查)
  • typeof (类型检测)
  • ?. (可选链)
  • ?.() (可选调用)
  • ?.[] (可选元素访问)

语法特性:

  • 对象字面量:{ key: value, ... }
  • 数组字面量:[element1, element2, ...]
  • 箭头函数:(param) => expression
  • 函数调用:func(arg1, arg2, ...)
  • 成员访问:obj.prop, obj["prop"], arr[0]
  • 模板字面量(通过 t 标签函数)
  • 分组括号:(expression)

条件表达式

const score = variable<number>();
const gradeExpr = expr({ score })("score >= 90 ? 'A' : score >= 80 ? 'B' : score >= 70 ? 'C' : 'F'");

const compiled = compile(gradeExpr, { score });
const grade = evaluate(compiled, { score: 85 });
// => "B"

数组和对象操作

const numbers = variable<number[]>();

const sumExpr = expr({ numbers })("numbers.reduce((a, b) => a + b, 0)");

const compiled = compile(sumExpr, { numbers });
const sum = evaluate(compiled, { numbers: [1, 2, 3, 4, 5] });
// => 15

链式表达式组合

const a = variable<number>();
const b = variable<number>();

const sum = expr({ a, b })("a + b");
const product = expr({ a, b })("a * b");
const difference = expr({ a, b })("a - b");

const complex = expr({ sum, product, difference })("sum * product - difference");

const compiled = compile(complex, { a, b });
const result = evaluate(compiled, { a: 2, b: 3 });
// => (2+3) * (2*3) - (2-3) = 5 * 6 - (-1) = 30 + 1 = 31

短路求值(控制流优化)

编译器支持为 &&||?? 和三元表达式生成短路求值代码,避免不必要的计算:

const a = variable<boolean>();
const b = variable<boolean>();

// 逻辑或短路
const orExpr = expr({ a, b })("a || b");
const compiled = compile(orExpr, { a, b });

// 当 a 为 true 时,b 不会被求值
// 编译数据包含控制流节点:
// [["a", "b"], ["br", "$[0]", 1], "$[1]", ["phi"]]

// 空值合并
const x = variable<number | null>();
const y = variable<number>();
const coalesce = expr({ x, y })("x ?? y");

// 三元表达式
const condition = variable<boolean>();
const result = variable<number>();
const alternative = variable<number>();
const ternary = expr({ condition, result, alternative })("condition ? result : alternative");

自动内联优化

编译器自动将只被引用一次的子表达式内联到使用位置,减少中间计算:

const x = variable<number>();
const y = variable<number>();

const sum = expr({ x, y })("x + y");
const product = expr({ x, y })("x * y");
const result = expr({ sum, product })("sum + product");

// 自动内联后,编译结果为:
// [["x", "y"], "($[0]+$[1])+($[0]*$[1])"]
// 而不是 [["x", "y"], "$[0]+$[1]", "$[0]*$[1]", "$[2]+$[3]"]

const compiled = compile(result, { x, y });
const value = evaluate(compiled, { x: 2, y: 3 });
// => 11

直接编译对象和数组

compile 函数支持直接编译包含 Proxy 的对象和数组:

const x = variable<number>();
const y = variable<number>();
const sum = expr({ x, y })("x + y");

// 编译对象
const objCompiled = compile({ result: sum, original: { x, y } }, { x, y });
const objResult = evaluate(objCompiled, { x: 10, y: 20 });
// => { result: 30, original: { x: 10, y: 20 } }

// 编译数组
const arrCompiled = compile([x, sum, 100], { x, y });
const arrResult = evaluate(arrCompiled, { x: 5, y: 3 });
// => [5, 8, 100]

序列化和传输

编译后的数据可以轻松进行 JSON 序列化,适合网络传输或持久化存储:

// 编译表达式
const compiled = compile(result, { x, y });

// 序列化
const json = JSON.stringify(compiled);
// "[["x","y"],"$[0]+$[1]","$[0]*$[1]","$[2]+$[3]"]"

// 存储或传输...

// 反序列化
const deserialized = JSON.parse(json);

// 执行
const value = evaluate(deserialized, { x: 5, y: 3 });

编译数据格式

V1 格式(基础表达式)

基础格式为 JSON 数组:[variableNames, ...expressions]

// 输入
const sum = expr({ x, y })("x + y");
const compiled = compile(sum, { x, y });

// 输出
// [["x", "y"], "$[0]+$[1]"]
//  $[0] 引用 x,$[1] 引用 y

V2 格式(控制流节点)

启用短路求值时,生成包含控制流节点的格式:

// 输入
const result = expr({ a, b })("a || b");
const compiled = compile(result, { a, b });

// 输出
// [
//   ["a", "b"],
//   ["br", "$[0]", 1],  // 如果 $[0] 为 truthy,跳过 1 条指令
//   "$[1]",             // 否则求值 $[1]
//   ["phi"]           // 取最近求值结果
// ]

控制流节点类型:

  • ["br", condition, offset] - 条件跳转,条件为真时跳过 offset 条指令
  • ["jmp", offset] - 无条件跳转,跳过 offset 条指令
  • ["phi"] - 取最近求值结果(用于合并分支)

错误处理

编译时错误

编译器会检测并报告以下错误:

const x = variable<number>();
const y = variable<number>();

// 错误:引用未定义的变量
const invalid = expr({ x, y })("x + y + z");
compile(invalid, { x, y });
// => Error: Undefined variable(s): z

// 错误:变量名冲突
const xy = variable<number>();
const conflict = expr({ xy, x })("xy + x");
// 正确处理:编译器能区分 xy 和 x
const compiled = compile(conflict, { xy, x });
// => [["xy", "x"], "$[0]+$[1]"]

运行时错误

求值器会验证输入并报告运行时错误:

const x = variable<number>();
const y = variable<number>();

const sum = expr({ x, y })("x + y");
const compiled = compile(sum, { x, y });

// 错误:缺少必需变量
evaluate(compiled, { x: 2 });
// => Error: Missing required variable: y

// 错误:无效的编译数据
evaluate([], { x: 1 });
// => Error: Invalid compiled data: must have at least variable names

类型安全

项目充分利用 TypeScript 的类型系统进行编译时检查和类型推导:

const x = variable<number>();
const y = variable<string>();

// 类型错误会在编译时捕获
// const invalid = expr({ x, y })("z + y"); // Error: 'z' not in context
const valid = expr({ x })("-x"); // 编译器推导为 number

实际应用示例

动态表单验证规则

const formData = variable<{
  username: string;
  password: string;
  confirmPassword: string;
  age: number;
}>();

// 创建验证规则表达式
const isUsernameValid = expr({ formData })("formData.username.length >= 3 && formData.username.length <= 20");

const isPasswordValid = expr({ formData })("formData.password.length >= 8 && /[A-Z]/.test(formData.password)");

const doPasswordsMatch = expr({ formData })("formData.password === formData.confirmPassword");

const isAgeValid = expr({ formData })("formData.age >= 18 && formData.age <= 120");

const isFormValid = expr({
  isUsernameValid,
  isPasswordValid,
  doPasswordsMatch,
  isAgeValid,
})("isUsernameValid && isPasswordValid && doPasswordsMatch && isAgeValid");

// 编译一次,多次执行
const compiled = compile(isFormValid, { formData });

// 在表单输入时实时验证
evaluate(compiled, {
  formData: {
    username: "john_doe",
    password: "Secure123",
    confirmPassword: "Secure123",
    age: 25,
  },
}); // => true

数据转换管道

const rawData = variable<any[]>();
const config = variable<{
  minValue: number;
  maxValue: number;
  transform: (x: number) => number;
}>();

// 构建数据处理管道
const filtered = rawData.filter(
  lambda<[any], boolean>((item) =>
    expr({ item, config })("item.value >= config.minValue && item.value <= config.maxValue")
  )
);

const transformed = filtered.map(
  lambda<[any], number>((item) => expr({ item, config })("config.transform(item.value)"))
);

const sorted = transformed.toSorted(lambda<[number, number], number>((a, b) => expr({ a, b })("a - b")));

const pipeline = compile(sorted, { rawData, config });

// 执行数据处理
const result = evaluate(pipeline, {
  rawData: [{ value: 10 }, { value: 5 }, { value: 20 }, { value: 15 }],
  config: { minValue: 8, maxValue: 18, transform: (x: number) => x * 2 },
});
// => [10, 20, 30] (5 被过滤,10*2=20, 15*2=30, 20 被过滤)

规则引擎

// 定义规则条件
const user = variable<{
  age: number;
  role: string;
  balance: number;
}>();

const isEligible = expr({ user })(
  "(user.age >= 18 && user.age <= 65) && (user.role === 'premium' || user.balance > 10000)"
);

const discountRate = expr({ user, isEligible })("isEligible ? (user.role === 'premium' ? 0.2 : 0.1) : 0");

const rule = compile(discountRate, { user });

// 应用规则
const discount = evaluate(rule, {
  user: { age: 30, role: "premium", balance: 5000 },
});
// => 0.2 (20% 折扣)

性能考虑

  • 编译时间:编译过程涉及依赖分析和拓扑排序,通常快速完成
  • 执行时间:表达式通过 new Function() 编译为原生 JavaScript,执行性能接近原生代码
  • 内存占用:编译数据为纯 JSON,占用空间小,适合在网络上传输
  • 缓存机制:求值器缓存已编译的函数,重复执行时性能更优

最佳实践

  1. 编译一次,多次执行:对于重复使用的表达式,先编译后多次求值

    const compiled = compile(expression, variables);
    // 缓存 compiled,多次调用 evaluate
    evaluate(compiled, values1);
    evaluate(compiled, values2);
  2. 利用短路求值:短路求值已默认启用,对于条件表达式可以避免不必要的计算

  3. 利用自动内联:编译器会自动内联只引用一次的子表达式,无需手动优化

  4. 优先使用 Proxy 链式调用:对于对象属性访问,使用 config.timeoutexpr({ config })("config.timeout") 更简洁且类型更安全

项目结构

src/
├── index.ts          # 导出入口
├── variable.ts       # variable<T>() 函数
├── expr.ts           # expr() 函数
├── template.ts       # t() 标签模板函数
├── lambda.ts         # lambda() 函数(数组方法支持)
├── compile.ts        # 编译器(内联优化、短路求值)
├── evaluate.ts       # 运行时求值
├── parser.ts         # 表达式 AST 解析器
├── type-parser.ts    # TypeScript 类型级表达式解析
├── proxy-variable.ts # Proxy 变量实现
├── proxy-metadata.ts # Proxy 元数据管理
└── types.ts          # 类型定义(Variable、Expression、Lambda 等)

开发

安装依赖

bun install

运行测试

bun test

代码检查

bun run lint
bun run type-check

代码格式化

bun run format

许可证

MIT

贡献

欢迎提交 Issue 和 Pull Request!

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published