OPA进阶-函数与虚拟文档 本文来讲讲OPA
常用的函数(function
)和虚拟文档(virtual document
),以及他们使用的场景。
我们将以实现判断配置文件为例展开。
文章目录
用到的输入(配置文件列表)为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 { "files" : [ { "type" : "posix" , "path" : "/Users/newbmiao/Documents/1.yaml" } , { "type" : "posix" , "path" : "/Users/newbmiao/Documents/2.yaml" } , { "type" : "traditional-mac" , "path" : "Macintosh HD:Users:newbmiao:Documents:3.yml" } , { "type" : "traditional-mac" , "path" : "Macintosh HD:Users:newbmiao:Documents:3.json" } ] }
函数 函数基本每个语言都会有,要说OPA
里有啥特别的地方,那就是他也实现同名函数,类似“函数重载”,但简化了许多。
他的特点是:
默认函数返回值为 true/false
可以指定函数返回值
可以存在同名函数, 但参数数目不能变
相同输入(参数)必须获得相同输出(返回值)
举例来说,如果实现判断文件是否是配置文件后缀:
下边is_config_file
就是不指定返回值
条件满足则返回 true,三个实现只要满足一个就为 true
1 2 3 4 5 6 7 8 9 10 11 is_config_file(str) { contains(str, ".yaml") } is_config_file(str) { contains(str, ".yml") } is_config_file(str) { contains(str, ".json") }
当然也可以使用else
关键字合并到一个函数:
1 2 3 4 5 6 7 8 9 is_config_file2(str) { contains(str, ".yaml") } else { contains(str, ".yml") } else { contains(str, ".json") }
那如果指定返回值怎么重载呢?
以实现不同路径格式的文件名为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 getFileName(type, str) = x { type = "posix" str = trim(str) tmp := split(str, "/") x := tmp[minus(count(tmp), 1)] } getFileName(type, str) = x { type = "traditional-mac" str = trim(str) tmp := split(str, ":") x := tmp[minus(count(tmp), 1)] }
你会发现两个函数输入和返回不是相同的么?
其实不是,仔细看他们内都做了
type = {"traditional-mac"|"posix"}
的判断
这实际类似函数声明为
1 getFileName("posix", str) = x {}
所以输入是不同的
如果你省略了type
的判断,会报错的:
1 eval_conflict_error: functions must not produce multiple outputs for same inputs
到此,将函数组合一下就可以实现判定是否为配置:
1 2 3 4 5 6 7 import input.files is_config { file := files[_] x := getFileName(file.type, file.path) is_config_file(x) }
Tips: 关于OPA
的内置函数可以查看文档:built-in-functions [1]
虚拟文档 虚拟文档是生成的文档(或者称为集合 )
他的特点是:
作为集合,输入和输出必须是有限的
可以遍历
可以查询
举例来说,同样是获取文件名:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import input.files getFileNames[x] { file := files[_] file.type = "posix" file.path = trim(file.path) tmp := split(file.path, "/") x := tmp[minus(count(tmp), 1)] } getFileNames[x] { file := files[_] file.type = "traditional-mac" file.path = trim(file.path) tmp := split(file.path, ":") x := tmp[minus(count(tmp), 1)] }
这里 getFileNames[x] {}
等价于 getFileNames[x] = x {}
就是x
作为了集合的一个结果,填充到getFileNames
这个虚拟文档中,其key
也是x
的值。
有点抽象是不?
别怕,他可以查询,我们查询看下他的结果就好理解了
comprehensions.rego
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 package example_comprehensions import input.files # object comprehensions # { <key>: <term> | <body> } group_files_by_type := {type: paths | file := files[_] type := file.type paths := [path | tmp := files[_] tmp.type == type path := tmp.path ] } # object comprehensions # { <key>: <term> | <body> } group_files_by_file_extension := {ext: paths | # set: ["yaml", "yml", "json"] extSets := {e | e = regex.find_all_string_submatch_n(".*\\.(.*)$", files[_].path, -1)[0][1] } ext := extSets[_] paths := [path | tmp := files[_] endswith(tmp.path, ext) path := tmp.path ] } # set comprehensions # { <term> | <body> } convert_all_to_posix_path_sets := {path | path1 := {p | file := files[_] file.type == "posix" p := file.path } path2 := {p | file := files[_] file.type == "traditional-mac" p := replace(replace(file.path, "Macintosh HD", ""), ":", "/") } paths := path1 | path2 path = paths[_] } # array comprehensions # [ <term> | <body> ] convert_all_to_posix_path_array := [path | path1 := [p | file := files[_] file.type == "posix" p := file.path ] path2 := [p | file := files[_] file.type == "traditional-mac" p := replace(replace(file.path, "Macintosh HD", ""), ":", "/") ] paths := array.concat(path1, path2) path = paths[_] ]
function.rego
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 package example_func import input.files is_config { file := files[_] x := getFileName(file.type, file.path) is_config_file(x) } # or # is_config_file2(x) # function's input/output can be infinite getFileName(type, str) = x { type = "posix" # this is needed, without will report error : # eval_conflict_error: functions must not produce multiple outputs for same inputs str = trim(str) tmp := split(str, "/") x := tmp[count(tmp) - 1] } getFileName(type, str) = x { type = "traditional-mac" # this is needed, without will report error str = trim(str) tmp := split(str, ":") x := tmp[count(tmp) - 1] } # Or # getFileName("posix", str) = x { # str = trim(str) # tmp := split(str, "/") # x := tmp[minus(count(tmp), 1)] # } # getFileName("traditional-mac", str) = x { # str = trim(str) # tmp := split(str, ":") # x := tmp[minus(count(tmp), 1)] # } is_config_file(str) { contains(str, ".yaml") } is_config_file(str) { contains(str, ".yml") } is_config_file(str) { contains(str, ".json") } is_config_file2(str) { contains(str, ".yaml") } else { contains(str, ".yml") } else { contains(str, ".json") }
virtualDocument.rego
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package example_virtual_doc import data.example_func.is_config_file import input.files is_config { is_config_file(getFileNames[_]) } # virtual document's input/output is finite, and is generated document, can be query, support iteration, will be output getFileNames[x] { file := files[_] file.type = "posix" file.path = trim(file.path) tmp := split(file.path, "/") x := tmp[count(tmp) - 1] } getFileNames[x] { file := files[_] file.type = "traditional-mac" file.path = trim(file.path) tmp := split(file.path, ":") x := tmp[count(tmp) - 1] }
input.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 { "files" : [ { "type" : "posix" , "path" : "/Users/newbmiao/Documents/1.yaml" } , { "type" : "posix" , "path" : "/Users/newbmiao/Documents/2.yaml" } , { "type" : "traditional-mac" , "path" : "Macintosh HD:Users:newbmiao:Documents:3.yml" } , { "type" : "traditional-mac" , "path" : "Macintosh HD:Users:newbmiao:Documents:3.json" } ] }
1 2 3 4 5 6 7 8 9 10 cd syntax opa eval -f values -i input.json -d . "data.example_virtual_doc.getFileNames" [ [ "1.yaml", "2.yaml", "3.yml", "3.json" ] ]
然后遍历 虚拟文档,结合判定is_config_file
就可实现判定:
1 2 3 4 5 6 import data.example_func.is_config_file import input.files is_config { is_config_file(getFileNames[_]) }
这里遍历getFileNames[_]
,随即作为每一个输入参数传给is_config_file
进行判断,是不是很方便!
适用场景 具体到他们的适用场景,其实比较好区分,重点只要关注两点:
如果有一个答案是肯定的,那么虚拟文档就更适合。
OPA
的函数和虚拟文档很简单、很清秀是不是?
今天就到这里,下一篇我们看看他更优雅的推导式(Comprehensions
)。