Go每日一库之175:goexpr (通用表达式引擎)

发布时间 2023-09-29 21:54:14作者: 阿瑞娜

goexpr是一个golang实现的通用表达式引擎(expression engine),支持自定义操作数(operand),操作符(operator)以及函数(function)。

体验地址

1. 快速开始

下面是一个简单的例子,表达式(expression)采用接近自然语言的组织方式,阅读起来更容易理解:

country in ("中国") and city in ("北京", "上海) and (temperature greater 10 or wind less 5) and contains(title, "porn") match true
import (
	"log"

	"git.woa.com/goexpr/goexpr"
	"git.woa.com/goexpr/goexpr/operand"
)

func main(){
	expr, err := goexpr.NewExpression(`country in ("中国") and wind greater 0`)
	if err != nil {
		log.Fatalf("expression compile failed: %+v", err)
	}

	data := map[string]operand.Operand{
		"country": operand.String("中国"),
		"wind": operand.Integer(1),
	}

	log.Printf("result: %+v", expr.Evaluate(data))
}

以上代码输出

> result: true

2. 概念

本节主要详细介绍goexpr的一些概念,理解这些概念有助于对goexpr进行扩展,也有助于互相交流。

2.1 表达式(Expression)

表达式是goexpr的求值单元,一个基本的表达式由两部分组成:操作符(operator), 操作数(operand)。表达式的求值结果是一个bool值。

        操作符
          ↓
country match "中国"
   ↑            ↑
 操作数        操作数

左操作数只允许是一个,右操作数允许是一个列表,这取决于操作符的要求。比如in操作符要求有多个右操作数。

country in ("中国", "美国")

操作数可以是字面量(literal),也可以是变量(variable),也可以是函数(function)。

country match china or datezone() match "+8"

2.2 函数(Function)

函数返回一个操作数(operand),可以用在表达式的操作数部分。函数的参数可以任何操作数。

       变量参数  字面量参数 函数参数
          ↓        ↓        ↓
contains(title, "porn", upper(name)) match true
   ↑
 函数名

2.3 逻辑运算符(LogicalOperator)和括号(Bracket)

逻辑运算符和括号可以将表达式组合起来形成更复杂的表达式。

country match "中国" and (city match "北京" or city match "上海")

3. 参考手册

本节详细列出了内置的一些操作数和函数

3.1 内置操作数

  • 字符串类型,使用"或'包裹的任何字符,如:"中国", '美国'
  • 正则表达式,使用/包裹的任何字符串,如:/^123$/
  • 布尔类型,字面量true和false
  • 整数,任何可以被转为有效数字的非括号包裹的字面量,如123
  • 浮点数,任何可以被转为有效浮点数的字面量,如3.14

3.2 内置函数

name desc demo
contains(s, subs...) 检查字符串是否全部包含在s中 contains("hello world", "hello", "world") match true
contains_any(s, subs...) 检查字符串中的任意一个是否包含在s中 contains_any("hello world", "world", "golang") match true
count(s, sub) 检查字符串中包含子串的次数 count("hello world", "hello") match 1
join(sep, subs...) 连接字符串 join(",", "hello", "world") match "hello world"
to_lower(s) 转为小写 to_lower("HELLO") match "hello"
to_upper(s) 转为大写 to_upper("hello) match "HELLO"
trim_space(s) 去掉首尾空格 trim_space(" hello ") match "hello"
trim(s, t) 去掉首尾指定的字符串 trim(" hello ", " ") match "hello"
replace(s, old, new) 替换字符串 replace("hello world", "hello", "hi") match "hi world"

3.3 内置运算符

内置的运算符包含两类:

3.3.1 比较运算符

name desc demo
match 检查左操作数是否与右操作数匹配 conutry match "cn"
greater 检查左操作数是否大于右操作数 wind greater 10
less 检查做操作数是否小于右操作数 weather less 30
in 检查左操作数是否处于右操作数列表中 country in ("cn", "us")

每个比较运算符前面都可以用not来修饰,例如:not match, not greater, not in

3.3.2 逻辑运算符

name desc demo
and 与 country match "cn" and city match "beijing"
or 或 country match "cn" or country match "us"

4. 高级主题

本节介绍了goexpr的扩展方法。包括数据类型,函数,操作符的扩展。

4.1 扩展数据类型

可以有两种方法来扩展数据类型。

4.1.1 通过指定数据类型来实现

表达式在求值的时候,如果左操作数在自定义的操作数列表里,那么左边的操作数就会被看做自定义数据类型名,并且是一个变量类型的操作数,右边的操作数中非内置类型的操作数会被传递给自定义数据类型的构造函数来进行构造。

比如,我们可以自定义一个代表版本号的数据类型:

version in (1.1.0, "222")

表达式在构造的时候,发现version是自定义操作数,就会将1.1.0这个字面量传递给version的构造函数来构造操作数。但是"222"不会传递,因为它是有效的内置字面量。同时在最终求值的时候,version会被当做一个变量操作数。

实现一个操作数需要实现如下接口:

type Operand interface {
    Match(Operand) bool
    Greater(Operand) bool
    Less(Operand) bool
    Value() string
    Type() Type
}

然后注册一个构造函数。构造函数需要符合如下约束:

type Factory func(string) (Operand, error)

实现操作数及其构造函数之后,进行注册:

goexpr.RegisterOperand(operand.NewVersion, "version")

4.1.2 通过扩展字面量的方式

可以通过扩展引号(Quotaion)的方式来扩展字面量。内置的字符串,正则表达式也是通过这种方式实现的。

首先我们需要实现一个操作数(Operand),如何实现可以参考4.1.1章节。然后进行注册。注册的时候需要一个rune类型的字符作为括号的起始标识,注册之后,遇到此引号包裹的字面量,就会用注册的构造函数来进行构造。

比如,你可以实现用$包裹的数据为货币类型。

goexpr.RegisterLiteral('$', "dollar", NewDollar)

那么在表达式中出现用$包裹的字面量,就会被构造为Dollar类型:

accout greater $200$

4.2 扩展函数

除了内置函数之外,goexpr允许自定义函数,来实现更强大的功能,自定义函数必须满足如下定义:

  1. 输入参数可以拥有任何数量的operand.Operand类型参数。
  2. 返回值必须是一个operand.Operand。

下面都是合法的函数:

func Join(s operand.Operand, parts ...operand.Operand) operand.Operand
func Trim(s operand.Operand, t operand.Operand) operand.Operand

比如想要实现contains:

func Contains(s operand.Operand, substrs ...operand.Operand) operand.Operand {
	for _, substr := range substrs {
		if !strings.Contains(s.Value(), substr.Value()) {
			return operand.Boolean(false)
		}
	}
	return operand.Boolean(true)
}

然后进行注册

function.Register(Contains, "contains")

之后在表达式里就可以使用该函数了

contains("hello", "e") match true

4.3 扩展操作符

如果不喜欢内置的操作符风格,或者想自定于自己的操作符,那么就可以通过如下方式实现。

一个操作符必须符合如下约束:

type Operator = func(l operand.Operand, rs ...operand.Operand) bool

l为左操作数,rs为右操作数列表。比如我们想使用数学符号来做操作符,==表示match,>表示greater,<表示less。可以使用如下方式实现:

goexpr.RegisterOperator("==", false, evaluator.OperatorMatch)
goexpr.RegisterOperator(">", false, evaluator.OperatorGreater)
goexpr.RegisterOperator("<", false, evaluator.OperatorLess)

当然,也可以自己实现,比如实现>=和<=。