Rego 语言

简要概述

这里做 Rego 的简要语法说明。

基础类型

Scalar Values

标量值是 Rego 中最简单的类型,可以是字符串、数字、布尔值或 null,如:

greeting   := "Hello"
max_height := 42
pi         := 3.14159
allowed    := true
location   := null

docs   := [greeting, max_height, pi, allowed, location]

Strings

字符串支持两种声明方式,第一种通过双引号包含,如果有特殊字符需通过反斜杠转译,如:

"[a-zA-Z_]\\w*"

第二种声明方式是通过反引号(`),对特殊字符既所见即所得,如:

`[a-zA-Z_]\w*`

这其实和 Go 语言字符串有相同申明方式。

Composite Values

组合值包含两种类型:Objects(对象)、Sets(集合),在简单情况下,组合值可以像标量值一样被视为常量:

cube := {"width": 3, "height": 4, "depth": 5}
docs := [greeting, max_height, pi, allowed, location]

Objects(对象,也包含数组)键值可以是任意值:

ips_by_port := {
    80: ["1.1.1.1", "1.1.1.2"],
    443: ["2.2.2.1"],
    "80": ["1.1.1.3"],
}

这里 ips_by_port[80]ips_by_port["80"] 获取是不一样的结果,当需要把 ips_by_port 输出 JSON 结构时,原先以 80 为键名的内容会被 "80" 所覆盖,因为 JSON 本身不支持以数字为属性,所以我们在定义 rego 变量是要避免使用数字类型键值,如 ips_by_port 输出 JSON 结果集为:

{
  "443": [
    "2.2.2.1"
  ],
  "80": [
    "1.1.1.3"
  ]
}

Sets(集合)是无序的唯一值的集合,如:

{1,2,3} == {3,1,2}

定义变量

变量在 Rego 中与大部分编程语言不同,可同时是输入和输出。如果为变量提供了一个值,那么该变量是一个输入型,如果没有为变量提供值,那么该变量是一个输出型。

作为输入

m := [10,6,9,8,15,3,12]

sites := [
    {"name": "prod"},
    {"name": "smoke1"},
    {"name": "dev"}
]

以上变量 msites 均作为输入型变量,这同其他语言定义。

作为输出

> m := [10,6,9,8,15,3,12]
Rule 'm' defined in package repl. Type 'show' to see rules.
> m
[
  10,
  6,
  9,
  8,
  15,
  3,
  12
]
>
> n
1 error occurred: 1:1: rego_unsafe_var_error: var n is unsafe
>
> m[n] == 3
+---+
| n |
+---+
| 5 |
+---+
>

定义一个数组 m,此时变量 n 未定义,通过查询语句 m[n] == 3 查找数组 m 中值为 “3” 的结果并写入到变量 n 中,此时变量 n 作为输出型,在看另外一个示例:

import future.keywords.if
import future.keywords.in
import future.keywords.contains

sites := [
    {"name": "prod"},
    {"name": "smoke1"},
    {"name": "dev"}
]

q contains name if {
    some site in sites
    name := site.name
}

此时变量 q 的结果为:

[
  "dev",
  "prod",
  "smoke1"
]

在使用一个未定义过的变量 x 来评估 q,此时 x 为输出,如 q[x] 结果为:

+----------+----------+
|    x     |   q[x]   |
+----------+----------+
| "dev"    | "dev"    |
| "prod"   | "prod"   |
| "smoke1" | "smoke1" |
+----------+----------+

这里由于 q 是一个集合,所以它们结果相同。

再比如 sites[i].name,这样在遍历数组时会自动把变量 i 赋值输出:

+---+---------------+
| i | sites[i].name |
+---+---------------+
| 0 | "prod"        |
| 1 | "smoke1"      |
| 2 | "dev"         |
+---+---------------+

如果以上变量 i 在下文中不会使用,我们可以用下划线代替 sites[_].name,下划线在 Rego 中为特殊含义表示永不重复变量。

推导式

推导式,提供了一种从子查询构建复合值的简洁方式,主要有三种形式,这里通过以下示例说明:

apps := [
  {
    "id": "001",
    "name": "web",
    "servers": [
      "web-0",
      "web-1",
      "web-1000",
      "web-1001",
      "web-dev"
    ]
  },
  {
    "id": "002",
    "name": "mysql",
    "servers": [
      "db-0",
      "db-1000"
    ]
  },
  {
    "id": "003",
    "name": "web",
    "servers": [
      "web-3",
      "web-prod"
    ]
  },
  {
    "id": "004",
    "name": "mongodb",
    "servers": [
      "db-dev"
    ]
  }
]

数组推导式

Array Comprehensions

[ <term> | <body> ]
> comp := [ server | apps[i].name == "web"; server := apps[i].servers[0] ]
> comp
[
  "web-0",
  "web-3"
]
>

对象推导式

Object Comprehensions

{ <key>: <term> | <body> }
> comp := { key: server | key := apps[i].id; server := apps[i].servers[0] }
> comp
{
  "001": "web-0",
  "002": "db-0",
  "003": "web-3",
  "004": "db-dev"
}
>

集合推导式

Set Comprehensions

{ <term> | <body> }
> comp := { name | name = apps[x].name }
> comp 
[
  "mongodb",
  "mysql",
  "web"
]
>

根据已有数据生成变量

在编写规则时,使用到的一些关键字需要自己引入下,如 if contains 等:

import future.keywords.if
import futere.keywords.contains

通过规则生成数组

comp[name] {
	name := apps[_].servers[0]
}
> comp
[
  "db-0",
  "db-dev",
  "web-0",
  "web-3"
]
>

通过规则生成对象

comp[id] := server {
	some i
    id := apps[i].id
	server := apps[i].servers
}
> comp
{
  "001": [
    "web-0",
    "web-1",
    "web-1000",
    "web-1001",
    "web-dev"
  ],
  "002": [
    "db-0",
    "db-1000"
  ],
  "003": [
    "web-3",
    "web-prod"
  ],
  "004": [
    "db-dev"
  ]
}
>

这里使用示例数据 apps 生成一组对象 comp 以 “id” 为键,以 “server” 为值,这个值来自后面的 {} 判断。

对已有数组的累加

使用相同的变量 comp 在通过多次规则时,它们的结果集是做增量,如下面两次叠加:

> comp[name] { name := apps[_].servers[0] }
>
> comp
[
  "db-0",
  "db-dev",
  "web-0",
  "web-3"
]
>
>
> comp[name] { name := apps[_].name }
> comp
[
  "db-0",
  "db-dev",
  "mongodb",
  "mysql",
  "web",
  "web-0",
  "web-3"
]
>

对变量的完整定义

除了部分定义集合和对象的规则外,还支持任何类型文档的完整定义,通常用于常量:

pi := 3.14159

由具有完整定义的规则生成的文档一次只能具有一个值,如果评估为同一文档生成多个值,将返回错误。

package repl

import future.keywords.if

# 定义一个用户 "bob"
user := "bob"

# 定义两个用户组,分布表示管理员与受限用户组
power_users := {"alice", "bob", "fred"}
restricted_users := {"bob", "kim"}

# 管理员可拥有 32GB 内存
max_memory := 32 if power_users[user]

# 受限用户仅可有 4GB 内存
max_memory := 4 if restricted_users[user]

TODO;

这里注意下,官方文档是说无法更改,但实际测试中可以被变更:

Rule 'max_memory' re-defined in package repl. Type 'show' to see rules.

需具体检查下原因。

用户自定义函数

支持用户自定定义函数,可以与内置函数相同的语义进行调用,它们可以访问数据文档和输入文档。

trim_and_split(s) := x {
     t := trim(s, " ")
     x := split(t, ".")
}

> trim_and_split("   foo.bar ")
[
  "foo",
  "bar"
]
>
foo([x, {"bar": y}]) := z {
    z := {x: y}
}

> foo([10, {"bar": 30}])
{
  "10": 30
}
>

模块、包与内置函数

模块组成

一个 Rego 的模块,由以下三部分组成:

  1. 固定使用一个 package 关键字申明包名称;
  2. 零个或多个 import 关键字语句导入外部模块;
  3. 零个或多个规则定义。

包名称

包名称仅能使用字符串,如需分割多个层级,则通过点号 “.",如:

package foo.bar.baz

pi := 3.14159

以上规则定义好后,会自动导出到 opa,可通过以下 api 查询:

GET /v1/data/foo/bar/baz/pi

属于相同的包名称下的模块,可以分布在不同的目录。

导入模块

如需复用第三方模块,使用关键字 import 导入,这样即可在当前模块中引用该文档导出的标识符,如需使用关键字 ifcontains 则:

package foo.bar.baz

import future.keywords.if
import future.keywords.contains

http_servers contains server if {
    some server in servers
    "http" in server.protocols
}

TODO; 关键字 datainput

备注注解

同其他语言一样,通过 “#” 实现注解。

内置函数

OPA 本身提供了很多常用的函数,可直接调用,如:

> m := [6,9,8]
Rule 'm' defined in package repl. Type 'show' to see rules.
> max(m)
9
> min(m)
6
>

具体列表查看官方文档

内置关键字

some

用于遍历数据或对象,显式声明键索引与键值局部变量,这些变量的作用范围仅限于该规则内,类似 go 语言的 for 循环,如:

> import future.keywords.in
>
> m := [10,6,9,8,15,3,12]
Rule 'm' defined in package repl. Type 'show' to see rules.
>
> some k, v in m
+---+----+
| k | v  |
+---+----+
| 0 | 10 |
| 1 | 6  |
| 2 | 9  |
| 3 | 8  |
| 4 | 15 |
| 5 | 3  |
| 6 | 12 |
+---+----+
>

with

<expr> with <target-1> as <value-1> [with <target-2> as <value-2> [...]]

TODO;

default

default <name> := <term>

为变量定义默认值,因为在判断规则不成立时,变量会被设置为 undefined,这样在使用过程中不友好,如:

> import future.keywords.if
>
> m if 3 > 10
Rule 'm' defined in package repl. Type 'show' to see rules.
> m
undefined
>
> default m := false
Rule 'm' re-defined in package repl. Type 'show' to see rules.
>
> m
false
>
> m if 3 > 10
Rule 'm' defined in package repl. Type 'show' to see rules.
>
> m
false
>

else

基本的控制流构造,对一组规则按照顺序进行评估,一旦找到匹配项,就不会继续对后续规则评估,如:

import future.keywords.if

x := 10
y := 10

name := "user1" if {
    x > y
} else := "user2" {
    x == y
} else := "user3" {
    x < y
}

future

包含 in every if contains 在使用过程中可一次性导入 import futer.keyworks 不过建议按需,如:

import future.keywords.in
import future.keywords.every
import future.keywords.if
import future.keywords.contains

future.keywords.in

对查找的内容是否在给定的对象或数组中,如:

> import future.keywords.in
>
> m := [10,6,9,8,15,3,12]
Rule 'm' defined in package repl. Type 'show' to see rules.
> m
[
  10,
  6,
  9,
  8,
  15,
  3,
  12
]
>
> n := 3
Rule 'n' defined in package repl. Type 'show' to see rules.
>
> n
3
>
> n in m
true
>

future.keywords.every

对遍历的对象或数组,在规则内必须每个都成立,此时结果才为真,如:

> import future.keywords.in
> import future.keywords.every
>
> m := [10,6,9,8,15,3,12]
Rule 'm' defined in package repl. Type 'show' to see rules.
>
> n := 3
Rule 'n' defined in package repl. Type 'show' to see rules.
>
> n in m
true
>
> every x in m {
| x >= n
| }
|
true
>

future.keywords.if

如果规则判断为真,对前变量赋值,如:

> import future.keywords.if
>
> m := [10,6,9,8,15,3,12]
Rule 'm' defined in package repl. Type 'show' to see rules.
>
> n := 3
Rule 'n' defined in package repl. Type 'show' to see rules.
>
> deny if n > m[0]
Rule 'deny' defined in package repl. Type 'show' to see rules.
>
> deny
undefined
>
> deny if n < m[0]
Rule 'deny' defined in package repl. Type 'show' to see rules.
>
> deny
true
>

future.keywords.contains

TODO;

运算符

等号运算符

等号存在三种运算:赋值(:=)、相等(==)和对比赋值(=)。建议尽量使用赋值(:=)和比较(==),以便编写和阅读更容易理解的策略。

赋值运算 “:=” 是将值分配给变量

  1. 在规则内分配变量的作用域仅限于该规则内,并且会覆盖全局变量
import future.keywords.if

x := 100

p if {
    # 这里变量 "x" 仅在该规则内有效,并忽略上面的变量 "x"
    x := 1
    x != 100
}
  1. 以下均是错误的,变量赋值过不可重复
import future.keywords.if

x := 100

p if {
    # 这里变量 "x" 为以上定义的全局变量,不能在重复赋值,所以出错
    x != 100
    x := 1
}

p if {
    x := 1
    x := 2
}
  1. 从数组中提取值并将其赋值给变量
import future.keywords.if

address := ["3 Abbey Road", "NW8 9AY", "London", "England"]

in_london if {
    [_, _, city, country] := address
    city == "London"
    country == "England"
}

相等运算 “==” 比较在规则内检查两个值是否相等

  1. 比较两个变量值是否相等
import future.keywords.if

p if {
    x := 100
    x == 100
}
  1. 两边对比的变量必须已经定义否则出错
import future.keywords.if

p if {
    x == 100
}

对比赋值 “=” 用于比较两边规则如果为真则分别对其赋值

[x, "world"] = ["hello", y]
  1. 与单独赋值(:=)区别在他们规则内无需按照顺序
import future.keywords.if

s if {
    # 这个正常执行
    x > y
    y = 41
    x = 42
}
import future.keywords.if

s if {
    # 这个会出错
    x > y
    y := 41
    x := 42
}

对比运算符

也就平常的大于、小于、等于等,如:

a  ==  b  #  `a` is equal to `b`.
a  !=  b  #  `a` is not equal to `b`.
a  <   b  #  `a` is less than `b`.
a  <=  b  #  `a` is less than or equal to `b`.
a  >   b  #  `a` is greater than `b`.
a  >=  b  #  `a` is greater than or equal to `b`.



最后修改 2023.12.13: feat: 添加概念模版 (8ce1e9c)