Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -0,0 +1,13 @@
|
||||
# value
|
||||
|
||||
本领域的职责与行为见主题说明;函数签名和调用关系从源码读取。
|
||||
|
||||
## 说明
|
||||
|
||||
- [Adapter 运行配置验证](doc/adapter-local.md)
|
||||
- [Adapter 运行配置验证](doc/adapter-remote.md)
|
||||
- [Adapter 运行配置验证](doc/api.md)
|
||||
- [Adapter 运行配置验证](doc/app.md)
|
||||
- [Adapter 运行配置验证](doc/port.md)
|
||||
|
||||
[调用流程](link.md)由 `dever inspect . --links` 生成。
|
||||
@@ -0,0 +1,29 @@
|
||||
setting {
|
||||
offset: Int
|
||||
prefix: Text
|
||||
token: Secret
|
||||
}
|
||||
|
||||
port.calculate(value: Int) (answer: Int) {
|
||||
answer = helper(value)
|
||||
}
|
||||
|
||||
port.echo(value: Text) (answer: Text) {
|
||||
answer = setting.prefix + value
|
||||
}
|
||||
|
||||
port.sign(value: Text) (answer: Int?) {
|
||||
digest = dever.crypto.hmacSha256(setting.token, dever.bytes.fromText(value))
|
||||
answer = dever.bytes.at(digest, 0)
|
||||
}
|
||||
|
||||
port.notify(value: Int) () {
|
||||
check(value + setting.offset)
|
||||
}
|
||||
|
||||
helper(value: Int) (answer: Int) {
|
||||
answer = value + setting.offset
|
||||
}
|
||||
|
||||
check(value: Int < 0) () { fail(app.Problem.Missing("negative")) }
|
||||
check(value: other) () {}
|
||||
@@ -0,0 +1,20 @@
|
||||
port.calculate(value: Int) (answer: Int) {
|
||||
answer = value + 100
|
||||
}
|
||||
|
||||
port.echo(value: Text) (answer: Text) {
|
||||
answer = wait(run(label(value)))
|
||||
}
|
||||
|
||||
port.sign(value: Text) (answer: Int?) {
|
||||
answer = dever.bytes.at(dever.bytes.fromText("R"), 0)
|
||||
}
|
||||
|
||||
port.notify(value: Int) () {
|
||||
check(value)
|
||||
}
|
||||
|
||||
check(value: Int < 0) () { fail(app.Problem.Missing("negative")) }
|
||||
check(value: other) () {}
|
||||
|
||||
label(value: Text) (answer: Text) { answer = "remote:" + value }
|
||||
@@ -0,0 +1,17 @@
|
||||
cmd calculate = app.calculate
|
||||
|
||||
cmd echo = app.echo
|
||||
|
||||
cmd sign = app.sign
|
||||
|
||||
cmd worker = app.worker
|
||||
|
||||
cmd nested = app.nested
|
||||
|
||||
cmd mapped = app.mapped
|
||||
|
||||
cmd parallel = app.parallel
|
||||
|
||||
cmd scheduled = app.scheduled
|
||||
|
||||
cmd cancelled = app.cancelled
|
||||
@@ -0,0 +1,44 @@
|
||||
type Problem {
|
||||
error Missing(message: Text)
|
||||
error Denied(code: Int)
|
||||
}
|
||||
|
||||
calculate(value: Int) (answer: Int) {
|
||||
answer = port.calculate(value)
|
||||
}
|
||||
|
||||
echo(value: Text) (answer: Text) {
|
||||
answer = port.echo(value)
|
||||
}
|
||||
|
||||
sign(value: Text) (answer: Int?) {
|
||||
answer = port.sign(value)
|
||||
}
|
||||
|
||||
worker(value: Int) (answer: Int) {
|
||||
answer = wait(run(port.calculate(value)))
|
||||
}
|
||||
|
||||
nested(value: Int) (answer: Int) {
|
||||
answer = wait(run(worker(value)))
|
||||
}
|
||||
|
||||
mapped(value: Int) (answer: Int) {
|
||||
answer = sum(each(port.calculate, [value, value]))
|
||||
}
|
||||
|
||||
parallel(value: Int) (answer: Int) {
|
||||
parallelEach(port.notify, [value, value], 2)
|
||||
answer = port.calculate(value)
|
||||
}
|
||||
|
||||
scheduled(value: Int) (answer: Int) {
|
||||
answer = wait(run(port.calculate(value)))
|
||||
parallelEach(port.notify, [value, value], 2)
|
||||
}
|
||||
|
||||
cancelled(value: Int) (answer: Int) {
|
||||
pending = run(port.notify(value))
|
||||
stop(pending)
|
||||
answer = port.calculate(value)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
# Adapter 运行配置验证
|
||||
|
||||
验证同一原生产物按启动配置选择实现。
|
||||
|
||||
## demo.value.adapter.local.Setting
|
||||
|
||||
仅选中本实现时读取配置,Secret 不参与公开输出。
|
||||
|
||||
字段:
|
||||
- `offset`:数值偏移
|
||||
- `prefix`:文本前缀
|
||||
- `token`:签名测试输入
|
||||
|
||||
## demo.value.adapter.local.port.calculate
|
||||
|
||||
覆盖配置、选择和调用生命周期。
|
||||
|
||||
输入:
|
||||
- `value`:输入值
|
||||
输出:
|
||||
- `answer`:结果
|
||||
|
||||
## demo.value.adapter.local.port.echo
|
||||
|
||||
覆盖配置、选择和调用生命周期。
|
||||
|
||||
输入:
|
||||
- `value`:输入值
|
||||
输出:
|
||||
- `answer`:结果
|
||||
|
||||
## demo.value.adapter.local.port.sign
|
||||
|
||||
覆盖配置、选择和调用生命周期。
|
||||
|
||||
输入:
|
||||
- `value`:输入值
|
||||
输出:
|
||||
- `answer`:结果
|
||||
|
||||
## demo.value.adapter.local.port.notify
|
||||
|
||||
覆盖配置、选择和调用生命周期。
|
||||
|
||||
输入:
|
||||
- `value`:输入值
|
||||
输出:无
|
||||
|
||||
## demo.value.adapter.local.helper
|
||||
|
||||
覆盖配置、选择和调用生命周期。
|
||||
|
||||
输入:
|
||||
- `value`:输入值
|
||||
输出:
|
||||
- `answer`:结果
|
||||
|
||||
## demo.value.adapter.local.check
|
||||
|
||||
检查并行回调确实读取配置并保留错误身份。
|
||||
|
||||
输入:
|
||||
- `value`:输入值
|
||||
输出:无
|
||||
@@ -0,0 +1,55 @@
|
||||
# Adapter 运行配置验证
|
||||
|
||||
验证同一原生产物按启动配置选择实现。
|
||||
|
||||
## demo.value.adapter.remote.port.calculate
|
||||
|
||||
覆盖配置、选择和调用生命周期。
|
||||
|
||||
输入:
|
||||
- `value`:输入值
|
||||
输出:
|
||||
- `answer`:结果
|
||||
|
||||
## demo.value.adapter.remote.port.echo
|
||||
|
||||
覆盖配置、选择和调用生命周期。
|
||||
|
||||
输入:
|
||||
- `value`:输入值
|
||||
输出:
|
||||
- `answer`:结果
|
||||
|
||||
## demo.value.adapter.remote.port.sign
|
||||
|
||||
覆盖配置、选择和调用生命周期。
|
||||
|
||||
输入:
|
||||
- `value`:输入值
|
||||
输出:
|
||||
- `answer`:结果
|
||||
|
||||
## demo.value.adapter.remote.port.notify
|
||||
|
||||
覆盖配置、选择和调用生命周期。
|
||||
|
||||
输入:
|
||||
- `value`:输入值
|
||||
输出:无
|
||||
|
||||
## demo.value.adapter.remote.check
|
||||
|
||||
覆盖配置、选择和调用生命周期。
|
||||
|
||||
输入:
|
||||
- `value`:输入值
|
||||
输出:无
|
||||
|
||||
## demo.value.adapter.remote.label
|
||||
|
||||
异步候选拥有输入文本,配置选择不会改变输出协议。
|
||||
|
||||
输入:
|
||||
- `value`:输入值
|
||||
输出:
|
||||
- `answer`:结果
|
||||
@@ -0,0 +1,39 @@
|
||||
# Adapter 运行配置验证
|
||||
|
||||
验证同一原生产物按启动配置选择实现。
|
||||
|
||||
### calculate
|
||||
|
||||
验证实际编译运行结果。
|
||||
|
||||
### echo
|
||||
|
||||
验证实际编译运行结果。
|
||||
|
||||
### sign
|
||||
|
||||
验证实际编译运行结果。
|
||||
|
||||
### worker
|
||||
|
||||
验证实际编译运行结果。
|
||||
|
||||
### nested
|
||||
|
||||
验证实际编译运行结果。
|
||||
|
||||
### mapped
|
||||
|
||||
验证实际编译运行结果。
|
||||
|
||||
### parallel
|
||||
|
||||
验证实际编译运行结果。
|
||||
|
||||
### scheduled
|
||||
|
||||
验证实际编译运行结果。
|
||||
|
||||
### cancelled
|
||||
|
||||
验证取消后的配置生命周期。
|
||||
@@ -0,0 +1,92 @@
|
||||
# Adapter 运行配置验证
|
||||
|
||||
验证同一原生产物按启动配置选择实现。
|
||||
|
||||
## demo.value.Problem
|
||||
|
||||
保持完整的业务错误合同。
|
||||
|
||||
分支:
|
||||
- `error Missing(message: Text)`:输入不可用
|
||||
- `error Denied(code: Int)`:拒绝原因
|
||||
|
||||
## demo.value.calculate
|
||||
|
||||
覆盖配置、选择和调用生命周期。
|
||||
|
||||
输入:
|
||||
- `value`:输入值
|
||||
输出:
|
||||
- `answer`:结果
|
||||
|
||||
## demo.value.echo
|
||||
|
||||
覆盖配置、选择和调用生命周期。
|
||||
|
||||
输入:
|
||||
- `value`:输入值
|
||||
输出:
|
||||
- `answer`:结果
|
||||
|
||||
## demo.value.sign
|
||||
|
||||
覆盖配置、选择和调用生命周期。
|
||||
|
||||
输入:
|
||||
- `value`:输入值
|
||||
输出:
|
||||
- `answer`:结果
|
||||
|
||||
## demo.value.worker
|
||||
|
||||
覆盖配置、选择和调用生命周期。
|
||||
|
||||
输入:
|
||||
- `value`:输入值
|
||||
输出:
|
||||
- `answer`:结果
|
||||
|
||||
## demo.value.nested
|
||||
|
||||
覆盖配置、选择和调用生命周期。
|
||||
|
||||
输入:
|
||||
- `value`:输入值
|
||||
输出:
|
||||
- `answer`:结果
|
||||
|
||||
## demo.value.mapped
|
||||
|
||||
覆盖配置、选择和调用生命周期。
|
||||
|
||||
输入:
|
||||
- `value`:输入值
|
||||
输出:
|
||||
- `answer`:结果
|
||||
|
||||
## demo.value.parallel
|
||||
|
||||
覆盖配置、选择和调用生命周期。
|
||||
|
||||
输入:
|
||||
- `value`:输入值
|
||||
输出:
|
||||
- `answer`:结果
|
||||
|
||||
## demo.value.scheduled
|
||||
|
||||
覆盖配置、选择和调用生命周期。
|
||||
|
||||
输入:
|
||||
- `value`:输入值
|
||||
输出:
|
||||
- `answer`:结果
|
||||
|
||||
## demo.value.cancelled
|
||||
|
||||
取消持有配置的子任务后仍可读取父调用的配置。
|
||||
|
||||
输入:
|
||||
- `value`:输入值
|
||||
输出:
|
||||
- `answer`:结果
|
||||
@@ -0,0 +1,42 @@
|
||||
# Adapter 运行配置验证
|
||||
|
||||
验证同一原生产物按启动配置选择实现。
|
||||
|
||||
## demo.value.port.calculate
|
||||
|
||||
覆盖配置、选择和调用生命周期。
|
||||
|
||||
输入:
|
||||
- `value`:输入值
|
||||
输出:
|
||||
- `answer`:结果
|
||||
允许失败:`app.Problem`
|
||||
|
||||
## demo.value.port.echo
|
||||
|
||||
覆盖配置、选择和调用生命周期。
|
||||
|
||||
输入:
|
||||
- `value`:输入值
|
||||
输出:
|
||||
- `answer`:结果
|
||||
允许失败:`app.Problem`
|
||||
|
||||
## demo.value.port.sign
|
||||
|
||||
覆盖配置、选择和调用生命周期。
|
||||
|
||||
输入:
|
||||
- `value`:输入值
|
||||
输出:
|
||||
- `answer`:结果
|
||||
允许失败:`app.Problem`
|
||||
|
||||
## demo.value.port.notify
|
||||
|
||||
覆盖配置、选择和调用生命周期。
|
||||
|
||||
输入:
|
||||
- `value`:输入值
|
||||
输出:无
|
||||
允许失败:`app.Problem`
|
||||
@@ -0,0 +1,7 @@
|
||||
calculate(value: Int) (answer: Int) fails app.Problem
|
||||
|
||||
echo(value: Text) (answer: Text) fails app.Problem
|
||||
|
||||
sign(value: Text) (answer: Int?) fails app.Problem
|
||||
|
||||
notify(value: Int) () fails app.Problem
|
||||
@@ -0,0 +1,10 @@
|
||||
# wire
|
||||
|
||||
本领域的职责与行为见主题说明;函数签名和调用关系从源码读取。
|
||||
|
||||
## 说明
|
||||
|
||||
- [HTTP 编解码入口](doc/api.md)
|
||||
- [HTTP 编解码性能样例](doc/app.md)
|
||||
|
||||
[调用流程](link.md)由 `dever inspect . --links` 生成。
|
||||
@@ -0,0 +1,3 @@
|
||||
public post echo = app.echo
|
||||
|
||||
public post count = app.count
|
||||
@@ -0,0 +1,5 @@
|
||||
type Payload { text: Text, value: Int }
|
||||
|
||||
echo(payload: Payload) (answer: Payload) { answer = payload }
|
||||
|
||||
count(values: List<Int>) (answer: Int) { answer = length(values) }
|
||||
@@ -0,0 +1,11 @@
|
||||
# HTTP 编解码入口
|
||||
|
||||
绑定无数据库、无认证的公开请求。
|
||||
|
||||
### echo
|
||||
|
||||
通过 JSON 正文接收并返回记录。
|
||||
|
||||
### count
|
||||
|
||||
通过 JSON 正文接收整数数组并返回长度。
|
||||
@@ -0,0 +1,29 @@
|
||||
# HTTP 编解码性能样例
|
||||
|
||||
比较固定结构请求与响应的完整生成入口。
|
||||
|
||||
## bench.wire.Payload
|
||||
|
||||
固定字段用于验证结构预算可由类型证明。
|
||||
|
||||
字段:
|
||||
- `text`:原样返回的文本
|
||||
- `value`:原样返回的数字
|
||||
|
||||
## bench.wire.echo
|
||||
|
||||
原样返回请求中的具体记录。
|
||||
|
||||
输入:
|
||||
- `payload`:请求记录
|
||||
输出:
|
||||
- `answer`:相同记录
|
||||
|
||||
## bench.wire.count
|
||||
|
||||
接收可变长度数组,读取长度,单独测量输入解码成本。
|
||||
|
||||
输入:
|
||||
- `values`:待计数的整数
|
||||
输出:
|
||||
- `answer`:元素数量
|
||||
@@ -0,0 +1,11 @@
|
||||
# greeting
|
||||
|
||||
本领域的职责与行为见主题说明;函数签名和调用关系从源码读取。
|
||||
|
||||
## 说明
|
||||
|
||||
- [应用命令样例](doc/api.md)
|
||||
- [应用命令样例](doc/app.md)
|
||||
- [应用命令样例](doc/domain.md)
|
||||
|
||||
[调用流程](link.md)由 `dever inspect . --links` 生成。
|
||||
@@ -0,0 +1,17 @@
|
||||
cmd greet = app.greet
|
||||
|
||||
cmd double = app.double
|
||||
|
||||
cmd echo = app.echo
|
||||
|
||||
cmd many = app.many
|
||||
|
||||
cmd divide = app.divide
|
||||
|
||||
cmd pair = app.pair
|
||||
|
||||
cmd empty = app.empty
|
||||
|
||||
cmd positive = app.positive
|
||||
|
||||
cmd maybe = app.maybe
|
||||
@@ -0,0 +1,29 @@
|
||||
type Profile {
|
||||
id: Id
|
||||
name: Text
|
||||
scores: List<Int>
|
||||
ratio: Float?
|
||||
}
|
||||
|
||||
type Problem { error Rejected(message: Text) }
|
||||
|
||||
greet(name: Text) (message: Text) {
|
||||
message = utility.decorate(domain.greet(name))
|
||||
}
|
||||
|
||||
double(value: Int) (answer: Int) { answer = domain.double(value) }
|
||||
|
||||
echo(value: Profile) (answer: Profile) { answer = value }
|
||||
|
||||
many(rows: List<Profile>) (answer: List<Profile>) { answer = rows }
|
||||
|
||||
divide(value: Float, divisor: Float) (answer: Float) { answer = value / divisor }
|
||||
|
||||
pair(value: Int) (first: Int, second: Int) { first = value
|
||||
second = value + 1 }
|
||||
|
||||
empty() () {}
|
||||
|
||||
positive(value: Int) (answer: Int) { answer = domain.positive(value) }
|
||||
|
||||
maybe(value: Int?) (answer: Int?) { answer = value }
|
||||
@@ -0,0 +1,39 @@
|
||||
# 应用命令样例
|
||||
|
||||
验证应用调用边界与类型化命令执行。
|
||||
|
||||
### greet
|
||||
|
||||
将命令绑定到本领域能力。
|
||||
|
||||
### double
|
||||
|
||||
将命令绑定到本领域能力。
|
||||
|
||||
### echo
|
||||
|
||||
将命令绑定到本领域能力。
|
||||
|
||||
### many
|
||||
|
||||
将命令绑定到本领域能力。
|
||||
|
||||
### divide
|
||||
|
||||
将命令绑定到本领域能力。
|
||||
|
||||
### pair
|
||||
|
||||
将命令绑定到本领域能力。
|
||||
|
||||
### empty
|
||||
|
||||
将命令绑定到本领域能力。
|
||||
|
||||
### positive
|
||||
|
||||
将命令绑定到本领域能力。
|
||||
|
||||
### maybe
|
||||
|
||||
验证可空标量从命令解码结果借用后仍按值返回。
|
||||
@@ -0,0 +1,101 @@
|
||||
# 应用命令样例
|
||||
|
||||
验证应用调用边界与类型化命令执行。
|
||||
|
||||
## demo.greeting.Profile
|
||||
|
||||
定义命令传输的业务数据。
|
||||
|
||||
字段:
|
||||
- `id`:输入或输出的业务值
|
||||
- `name`:输入或输出的业务值
|
||||
- `scores`:输入或输出的业务值
|
||||
- `ratio`:输入或输出的业务值
|
||||
|
||||
## demo.greeting.Problem
|
||||
|
||||
定义命令传输的业务数据。
|
||||
|
||||
分支:
|
||||
- `error Rejected(message: Text)`:输入或输出的业务值
|
||||
|
||||
## demo.greeting.greet
|
||||
|
||||
完成命令对应的业务处理。
|
||||
|
||||
输入:
|
||||
- `name`:输入或输出的业务值
|
||||
输出:
|
||||
- `message`:输入或输出的业务值
|
||||
|
||||
## demo.greeting.double
|
||||
|
||||
完成命令对应的业务处理。
|
||||
|
||||
输入:
|
||||
- `value`:输入或输出的业务值
|
||||
输出:
|
||||
- `answer`:输入或输出的业务值
|
||||
|
||||
## demo.greeting.echo
|
||||
|
||||
完成命令对应的业务处理。
|
||||
|
||||
输入:
|
||||
- `value`:输入或输出的业务值
|
||||
输出:
|
||||
- `answer`:输入或输出的业务值
|
||||
|
||||
## demo.greeting.many
|
||||
|
||||
完成命令对应的业务处理。
|
||||
|
||||
输入:
|
||||
- `rows`:输入或输出的业务值
|
||||
输出:
|
||||
- `answer`:输入或输出的业务值
|
||||
|
||||
## demo.greeting.divide
|
||||
|
||||
完成命令对应的业务处理。
|
||||
|
||||
输入:
|
||||
- `value`:输入或输出的业务值
|
||||
- `divisor`:输入或输出的业务值
|
||||
输出:
|
||||
- `answer`:输入或输出的业务值
|
||||
|
||||
## demo.greeting.pair
|
||||
|
||||
完成命令对应的业务处理。
|
||||
|
||||
输入:
|
||||
- `value`:输入或输出的业务值
|
||||
输出:
|
||||
- `first`:输入或输出的业务值
|
||||
- `second`:输入或输出的业务值
|
||||
|
||||
## demo.greeting.empty
|
||||
|
||||
完成命令对应的业务处理。
|
||||
|
||||
输入:无
|
||||
输出:无
|
||||
|
||||
## demo.greeting.positive
|
||||
|
||||
完成命令对应的业务处理。
|
||||
|
||||
输入:
|
||||
- `value`:输入或输出的业务值
|
||||
输出:
|
||||
- `answer`:输入或输出的业务值
|
||||
|
||||
## demo.greeting.maybe
|
||||
|
||||
保持可空整数的完整值,不给空输入填入默认值。
|
||||
|
||||
输入:
|
||||
- `value`:可空整数
|
||||
输出:
|
||||
- `answer`:原始值
|
||||
@@ -0,0 +1,30 @@
|
||||
# 应用命令样例
|
||||
|
||||
验证应用调用边界与类型化命令执行。
|
||||
|
||||
## demo.greeting.domain.greet
|
||||
|
||||
完成命令对应的业务处理。
|
||||
|
||||
输入:
|
||||
- `name`:输入或输出的业务值
|
||||
输出:
|
||||
- `message`:输入或输出的业务值
|
||||
|
||||
## demo.greeting.domain.double
|
||||
|
||||
完成命令对应的业务处理。
|
||||
|
||||
输入:
|
||||
- `value`:输入或输出的业务值
|
||||
输出:
|
||||
- `answer`:输入或输出的业务值
|
||||
|
||||
## demo.greeting.domain.positive
|
||||
|
||||
完成命令对应的业务处理。
|
||||
|
||||
输入:
|
||||
- `value`:输入或输出的业务值
|
||||
输出:
|
||||
- `answer`:输入或输出的业务值
|
||||
@@ -0,0 +1,6 @@
|
||||
greet(name: Text) (message: Text) { message = "Hello, " + name }
|
||||
|
||||
double(value: Int) (answer: Int) { answer = value * 2 }
|
||||
|
||||
positive(value: Int < 0) (answer: Int) { fail(app.Problem.Rejected("negative")) }
|
||||
positive(value: other) (answer: Int) { answer = value }
|
||||
@@ -0,0 +1,9 @@
|
||||
# utility
|
||||
|
||||
本领域的职责与行为见主题说明;函数签名和调用关系从源码读取。
|
||||
|
||||
## 说明
|
||||
|
||||
- [应用命令样例](doc/app.md)
|
||||
|
||||
[调用流程](link.md)由 `dever inspect . --links` 生成。
|
||||
@@ -0,0 +1 @@
|
||||
decorate(value: Text) (answer: Text) { answer = "[" + value + "]" }
|
||||
@@ -0,0 +1,12 @@
|
||||
# 应用命令样例
|
||||
|
||||
验证应用调用边界与类型化命令执行。
|
||||
|
||||
## demo.utility.decorate
|
||||
|
||||
完成命令对应的业务处理。
|
||||
|
||||
输入:
|
||||
- `value`:输入或输出的业务值
|
||||
输出:
|
||||
- `answer`:输入或输出的业务值
|
||||
@@ -0,0 +1,52 @@
|
||||
add(value: Int, state: Int) (next: Int) pure { next = value + state }
|
||||
|
||||
type Message { Value(value: Int?) }
|
||||
|
||||
count(value: Message, state: Int) (next: Int) pure { next = state + 1 }
|
||||
|
||||
until(value: Int, state: Int) (next: Int, stop: Bool) pure { next = state + value
|
||||
stop = value >= 2 }
|
||||
|
||||
delayed(value: Int, state: Int) (next: Int) { dever.task.sleep(0)
|
||||
next = state + value }
|
||||
|
||||
sendOne(value: Int, target: Channel<Int>) () { send(target, value) }
|
||||
|
||||
bad(value: Int) () { dever.task.sleep(-1) }
|
||||
|
||||
consumeBad(values: AsyncStream<Int>) () { each(bad, values) }
|
||||
|
||||
failed(value: dever.time.SleepResult.Done) (answer: Bool) recover("观察流失败") { answer = false }
|
||||
failed(value: dever.time.SleepResult.Failed(message)) (answer: Bool) recover("观察流失败") { answer = length(message) > 0 }
|
||||
|
||||
cleanup() (answer: Bool) { values = stream([1, 2])
|
||||
failure = failed(result(consumeBad(values)))
|
||||
remaining = reduce(add, values, 0)
|
||||
answer = failure and remaining == 0 }
|
||||
|
||||
creation() (values: AsyncStream<Int>) { values = dever.task.ticks(1) }
|
||||
|
||||
invalid() () { close(dever.task.ticks(0)) }
|
||||
|
||||
verify() (answer: Bool) {
|
||||
values = stream([1, 2, 3])
|
||||
alias = values
|
||||
total = reduce(add, values, 0)
|
||||
empty = reduce(add, alias, 0)
|
||||
limited = stream([1, 2, 3])
|
||||
prefix = reduceUntil(until, limited, 0)
|
||||
after = reduce(add, limited, 0)
|
||||
async_total = reduce(delayed, stream([1, 2, 3]), 0)
|
||||
bytes_total = reduce(add, stream(dever.bytes.fromText("AB")), 0)
|
||||
channel_values = channel(Message, 2)
|
||||
send(channel_values, Message.Value(null))
|
||||
send(channel_values, Message.Value(2))
|
||||
close(channel_values)
|
||||
items = reduce(count, stream(channel_values), 0)
|
||||
target = channel(Int, 2)
|
||||
each(sendOne, stream([10, 20]), target)
|
||||
first = receive(target)
|
||||
second = receive(target)
|
||||
ticks = reduceUntil(until, dever.task.ticks(1), 0)
|
||||
answer = total == 6 and empty == 0 and prefix == 3 and after == 0 and async_total == 6 and bytes_total == 131 and items == 2 and first == 10 and second == 20 and ticks == 3
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
# account
|
||||
|
||||
本领域的职责与行为见主题说明;函数签名和调用关系从源码读取。
|
||||
|
||||
## 说明
|
||||
|
||||
- [认证基准接口](doc/api-admin.md)
|
||||
- [认证基准命令](doc/api.md)
|
||||
- [Provider 与角色基准](doc/app.md)
|
||||
- [身份租户类型](doc/model-tenant.md)
|
||||
- [认证基准账号](doc/model.md)
|
||||
|
||||
[调用流程](link.md)由 `dever inspect . --links` 生成。
|
||||
@@ -0,0 +1 @@
|
||||
cmd seed = app.seed
|
||||
@@ -0,0 +1 @@
|
||||
get permissions = app.permissions
|
||||
@@ -0,0 +1,22 @@
|
||||
type Identity { id: Text
|
||||
user_id: model.id?
|
||||
tenant_id: model.tenant.id?
|
||||
}
|
||||
|
||||
verify(claims: dever.auth.Claims) (verified: Identity) {
|
||||
account = model.first({ where = subject == claims.subject and session == claims.session and active == true })
|
||||
verified = identity(account)
|
||||
}
|
||||
|
||||
identity(account: model.Account) (verified: Identity) {
|
||||
verified = Identity { id = account.subject, user_id = account.id, tenant_id = null }
|
||||
}
|
||||
identity(account: null) (verified: Identity) { fail(dever.api.Error.Unauthorized) }
|
||||
|
||||
seed(subject: Text, session: Text, active: Bool) (account: model.Account) {
|
||||
account = model.create({ subject = subject, session = session, active = active })
|
||||
}
|
||||
|
||||
permissions() (values: List<dever.auth.Permission>) {
|
||||
values = dever.auth.currentPermissions()
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# 认证基准接口
|
||||
|
||||
受保护入口使生成器建立真实权限目录和认证分派。
|
||||
|
||||
### permissions
|
||||
|
||||
读取当前身份的角色授权。
|
||||
@@ -0,0 +1,7 @@
|
||||
# 认证基准命令
|
||||
|
||||
独立测试入口,不创建生产账号。
|
||||
|
||||
### seed
|
||||
|
||||
建立基准账号。
|
||||
@@ -0,0 +1,43 @@
|
||||
# Provider 与角色基准
|
||||
|
||||
保留真实 Dever 条款分派、SQL decoder 和标准库记录转换。
|
||||
|
||||
## bench.account.Identity
|
||||
|
||||
可信身份不携带权限缓存。
|
||||
|
||||
字段:
|
||||
- `id`:身份标识
|
||||
- `user_id`:账号 ID
|
||||
- `tenant_id`:租户 ID
|
||||
|
||||
## bench.account.verify
|
||||
|
||||
从数据库重新验证会话。
|
||||
|
||||
输入:`claims`:签名已验证的声明
|
||||
输出:`verified`:身份
|
||||
|
||||
## bench.account.identity
|
||||
|
||||
缺失、已禁用或会话不匹配的账号均拒绝。
|
||||
|
||||
输入:`account`:账号
|
||||
输出:`verified`:身份
|
||||
|
||||
## bench.account.seed
|
||||
|
||||
在计时前准备独立数据库。
|
||||
|
||||
输入:
|
||||
- `subject`:身份
|
||||
- `session`:会话
|
||||
- `active`:启用状态
|
||||
输出:`account`:账号
|
||||
|
||||
## bench.account.permissions
|
||||
|
||||
读取当前用户的实际角色授权,输出所有记录字段。
|
||||
|
||||
输入:无
|
||||
输出:`values`:有效权限
|
||||
@@ -0,0 +1,9 @@
|
||||
# 身份租户类型
|
||||
|
||||
用于保持身份记录的名义 ID 合同;本基准不建立租户。
|
||||
|
||||
## bench.account.model.tenant.Tenant
|
||||
|
||||
独立的租户 ID 类型。
|
||||
|
||||
字段:`name`:名称
|
||||
@@ -0,0 +1,12 @@
|
||||
# 认证基准账号
|
||||
|
||||
独立基准数据,真实读取账号、会话和启用状态。
|
||||
|
||||
## bench.account.model.Account
|
||||
|
||||
Provider 只查询全局身份表。
|
||||
|
||||
字段:
|
||||
- `subject`:身份
|
||||
- `session`:会话
|
||||
- `active`:启用状态
|
||||
@@ -0,0 +1,5 @@
|
||||
global type Account {
|
||||
subject: Text unique
|
||||
session: Text
|
||||
active: Bool
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
global type Tenant { name: Text }
|
||||
@@ -0,0 +1,24 @@
|
||||
# account
|
||||
|
||||
本领域的职责与行为见主题说明;函数签名和调用关系从源码读取。
|
||||
|
||||
## 说明
|
||||
|
||||
- [任务权限入口](doc/api-admin-jobs.md)
|
||||
- [请求元数据入口](doc/api-admin-metadata.md)
|
||||
- [自动资源接口](doc/api-admin-profile.md)
|
||||
- [角色接口](doc/api-admin-roles.md)
|
||||
- [Secret 输入验收](doc/api-admin-secret.md)
|
||||
- [身份认证接口](doc/api-admin-session.md)
|
||||
- [认证测试命令](doc/api.md)
|
||||
- [身份任务](doc/app-jobs.md)
|
||||
- [请求元数据业务](doc/app-metadata.md)
|
||||
- [角色业务验收](doc/app-roles.md)
|
||||
- [敏感 HTTP 输入](doc/app-secret.md)
|
||||
- [会话认证验收](doc/app-session.md)
|
||||
- [可信任务执行](doc/job.md)
|
||||
- [自动 REST 数据](doc/model-profile.md)
|
||||
- [租户身份类型](doc/model-tenant.md)
|
||||
- [全局会话账号](doc/model.md)
|
||||
|
||||
[调用流程](link.md)由 `dever inspect . --links` 生成。
|
||||
@@ -0,0 +1,5 @@
|
||||
cmd seed = app.seed
|
||||
|
||||
cmd set_deadline = app.setDeadline
|
||||
|
||||
cmd revoke = app.revoke
|
||||
@@ -0,0 +1,5 @@
|
||||
post job_submit = app.jobSubmit
|
||||
|
||||
cmd job_submit = app.jobSubmit
|
||||
|
||||
cmd job_count = app.jobCount
|
||||
@@ -0,0 +1,3 @@
|
||||
public post metadata = app.metadata
|
||||
|
||||
public post metadata_fail = app.metadataFail
|
||||
@@ -0,0 +1 @@
|
||||
rest model.profile
|
||||
@@ -0,0 +1,25 @@
|
||||
authenticated get permissions = app.permissions
|
||||
|
||||
authenticated get current_permissions = app.currentPermissions
|
||||
|
||||
authenticated get roles = app.roles
|
||||
|
||||
authenticated get role = app.role
|
||||
|
||||
authenticated get user_roles = app.userRoles
|
||||
|
||||
authenticated get is_owner = app.isOwner
|
||||
|
||||
post save_role = app.saveRole
|
||||
|
||||
post grant_role = app.grantRole
|
||||
|
||||
post revoke_role = app.revokeRole
|
||||
|
||||
post disable_role = app.disableRole
|
||||
|
||||
post delete_role = app.deleteRole
|
||||
|
||||
post role_fail = app.roleFail
|
||||
|
||||
get protected_me = app.me
|
||||
@@ -0,0 +1 @@
|
||||
public post probe = app.probe
|
||||
@@ -0,0 +1,11 @@
|
||||
authenticated get me = app.me
|
||||
|
||||
authenticated post me = app.me
|
||||
|
||||
public get health = app.health
|
||||
|
||||
public post login = app.login
|
||||
|
||||
public post logout = app.logout
|
||||
|
||||
public post login_fail = app.loginFail
|
||||
@@ -0,0 +1,11 @@
|
||||
type Work { subject: Text }
|
||||
|
||||
jobSubmit(subject: Text, key: Text) (id: Id) {
|
||||
id = dever.job.enqueue(job.perform, Work { subject = subject }, key)
|
||||
}
|
||||
|
||||
jobApply(input: Work) () {
|
||||
created = seed(input.subject, "from-job")
|
||||
}
|
||||
|
||||
jobCount() (total: Int) { total = model.count() }
|
||||
@@ -0,0 +1,22 @@
|
||||
type Metadata {
|
||||
request_id: Text
|
||||
method: Text
|
||||
path: Text
|
||||
address: Text
|
||||
header: Text?
|
||||
cookie: Text?
|
||||
}
|
||||
|
||||
metadata() (value: Metadata) {
|
||||
id = dever.api.requestId()
|
||||
dever.api.setHeader("X-Request-Id", id)
|
||||
dever.api.setCookie("theme", "dark", dever.api.secureCookieOptions())
|
||||
options = dever.api.CookieOptions { path = "/", domain = null, same_site = dever.api.SameSite.Strict, secure = true, http_only = true, max_age = 60 }
|
||||
dever.api.setCookie("strict", "yes", options)
|
||||
value = Metadata { request_id = id, method = dever.api.method(), path = dever.api.path(), address = dever.api.clientAddress(), header = dever.api.header("X-Test"), cookie = dever.api.cookie("theme") }
|
||||
}
|
||||
|
||||
metadataFail() () {
|
||||
value = metadata()
|
||||
fail(dever.api.Error.Conflict)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
permissions() (values: List<dever.auth.Permission>) {
|
||||
values = dever.auth.permissions()
|
||||
}
|
||||
|
||||
currentPermissions() (values: List<dever.auth.Permission>) {
|
||||
values = dever.auth.currentPermissions()
|
||||
}
|
||||
|
||||
roles() (values: List<dever.auth.Role>) {
|
||||
values = dever.auth.roles()
|
||||
}
|
||||
|
||||
role(id: Text) (value: dever.auth.Role?) {
|
||||
value = dever.auth.role(id)
|
||||
}
|
||||
|
||||
userRoles(user_id: Int) (values: List<dever.auth.Role>) {
|
||||
values = dever.auth.userRoles(user_id)
|
||||
}
|
||||
|
||||
isOwner(user_id: Int) (value: Bool) {
|
||||
value = dever.auth.isOwner(user_id)
|
||||
}
|
||||
|
||||
saveRole(id: Text, name: Text, all_permissions: Bool, permission_keys: List<Text>) () {
|
||||
dever.auth.saveRole(id, name, all_permissions, permission_keys)
|
||||
}
|
||||
|
||||
grantRole(user_id: Int, role_id: Text) () {
|
||||
dever.auth.grantRole(user_id, role_id)
|
||||
}
|
||||
|
||||
revokeRole(user_id: Int, role_id: Text) () {
|
||||
dever.auth.revokeRole(user_id, role_id)
|
||||
}
|
||||
|
||||
disableRole(role_id: Text) () {
|
||||
dever.auth.disableRole(role_id)
|
||||
}
|
||||
|
||||
deleteRole(role_id: Text) () {
|
||||
dever.auth.deleteRole(role_id)
|
||||
}
|
||||
|
||||
roleFail(id: Text) () {
|
||||
dever.auth.saveRole(id, "failed", false, [])
|
||||
fail(dever.api.Error.Conflict)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
type Credentials { key: Secret }
|
||||
|
||||
probe(input: Credentials) (size: Int) {
|
||||
digest = dever.crypto.hmacSha256(input.key, dever.bytes.fromText("fixture"))
|
||||
size = length(digest)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
type Identity {
|
||||
id: Text
|
||||
user_id: model.id?
|
||||
tenant_id: model.tenant.id?
|
||||
}
|
||||
|
||||
verify(claims: dever.auth.Claims) (verified: Identity) {
|
||||
account = model.first({ where = subject == claims.subject and session == claims.session and active == true and expires_at > dever.time.unixMillis() })
|
||||
verified = identity(account)
|
||||
}
|
||||
|
||||
identity(account: model.Account) (verified: Identity) {
|
||||
verified = Identity { id = account.subject, user_id = account.id, tenant_id = null }
|
||||
}
|
||||
identity(account: null) (verified: Identity) { fail(dever.api.Error.Unauthorized) }
|
||||
|
||||
seed(subject: Text, session: Text) (account: model.Account) {
|
||||
account = model.create({ subject = subject, session = session, active = true, expires_at = dever.time.unixMillis() + 600000 })
|
||||
}
|
||||
|
||||
setDeadline(id: model.id, expires: Int) (deadline: Int) {
|
||||
account = model.update(id, { expires_at = expires })
|
||||
deadline = account.expires_at
|
||||
}
|
||||
|
||||
revoke(id: model.id) (disabled: Bool) {
|
||||
account = model.update(id, { active = false })
|
||||
disabled = not account.active
|
||||
}
|
||||
|
||||
me() (profile: Profile) {
|
||||
profile = Profile { id = dever.auth.id(), session = dever.auth.session(), site = dever.site.key(), user_id = dever.auth.userId(), tenant_id = dever.auth.tenantId() }
|
||||
}
|
||||
|
||||
health() (ready: Bool) { ready = true }
|
||||
|
||||
type Profile { id: Text
|
||||
session: Text
|
||||
site: Text
|
||||
user_id: model.id?
|
||||
tenant_id: model.tenant.id?
|
||||
}
|
||||
|
||||
login(subject: Text, session: Text) () {
|
||||
verified = verify(dever.auth.Claims { subject = subject, session = session, tenant = null, site = dever.site.key() })
|
||||
dever.auth.issueCookie(verified.id, session, null)
|
||||
}
|
||||
|
||||
logout() () { dever.auth.clearCookie() }
|
||||
|
||||
loginFail(subject: Text, session: Text) () {
|
||||
login(subject, session)
|
||||
fail(dever.api.Error.Conflict)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
# 任务权限入口
|
||||
|
||||
只有受保护入口才能以 User 身份提交任务。
|
||||
|
||||
### submit
|
||||
|
||||
权限成功后保存可信上下文。
|
||||
|
||||
### system
|
||||
|
||||
本机命令拥有明确 System 身份。
|
||||
|
||||
### count
|
||||
|
||||
本机读取业务结果。
|
||||
@@ -0,0 +1,11 @@
|
||||
# 请求元数据入口
|
||||
|
||||
公开 POST 也遵守 Cookie 写入的同源要求。
|
||||
|
||||
### metadata
|
||||
|
||||
成功时发布头与 Cookie。
|
||||
|
||||
### metadata_fail
|
||||
|
||||
失败时丢弃暂存响应字段。
|
||||
@@ -0,0 +1,7 @@
|
||||
# 自动资源接口
|
||||
|
||||
生成受保护的列表、详情、创建、替换与删除接口。
|
||||
|
||||
### resource
|
||||
|
||||
显式选择同领域 Model topic。
|
||||
@@ -0,0 +1,55 @@
|
||||
# 角色接口
|
||||
|
||||
写入仅允许受保护 API;读取允许已认证用户。
|
||||
|
||||
### permissions
|
||||
|
||||
已认证读取当前站点。
|
||||
|
||||
### current_permissions
|
||||
|
||||
已认证读取当前站点。
|
||||
|
||||
### roles
|
||||
|
||||
已认证读取当前站点。
|
||||
|
||||
### role
|
||||
|
||||
已认证读取当前站点。
|
||||
|
||||
### user_roles
|
||||
|
||||
已认证读取当前站点。
|
||||
|
||||
### is_owner
|
||||
|
||||
已认证读取当前站点。
|
||||
|
||||
### save_role
|
||||
|
||||
执行前核验精确角色权限。
|
||||
|
||||
### grant_role
|
||||
|
||||
执行前核验精确角色权限。
|
||||
|
||||
### revoke_role
|
||||
|
||||
执行前核验精确角色权限。
|
||||
|
||||
### disable_role
|
||||
|
||||
执行前核验精确角色权限。
|
||||
|
||||
### delete_role
|
||||
|
||||
执行前核验精确角色权限。
|
||||
|
||||
### role_fail
|
||||
|
||||
执行前核验精确角色权限。
|
||||
|
||||
### protected_me
|
||||
|
||||
只有持有本入口权限的可信用户才能读取。
|
||||
@@ -0,0 +1,7 @@
|
||||
# Secret 输入验收
|
||||
|
||||
验证合法敏感输入不被输出 codec 错误拒绝。
|
||||
|
||||
### probe
|
||||
|
||||
通过 JSON 输入 Secret record。
|
||||
@@ -0,0 +1,27 @@
|
||||
# 身份认证接口
|
||||
|
||||
authenticated 仅验证身份;角色授权入口仍使用独立的权限检查。
|
||||
|
||||
### read
|
||||
|
||||
未认证请求不能执行 App。
|
||||
|
||||
### write
|
||||
|
||||
Cookie 非安全方法必须进行同源检查。
|
||||
|
||||
### health
|
||||
|
||||
允许匿名访问的单个声明。
|
||||
|
||||
### login
|
||||
|
||||
登录入口验证凭据之后签发 Cookie。
|
||||
|
||||
### logout
|
||||
|
||||
匿名也能清除旧 Cookie,但不能绕过同源检查。
|
||||
|
||||
### login_fail
|
||||
|
||||
业务失败时不发布 Cookie。
|
||||
@@ -0,0 +1,15 @@
|
||||
# 认证测试命令
|
||||
|
||||
测试命令只操作自有临时数据库。
|
||||
|
||||
### seed
|
||||
|
||||
创建测试账号。
|
||||
|
||||
### set_deadline
|
||||
|
||||
调整本测试数据库的会话截止时间。
|
||||
|
||||
### revoke
|
||||
|
||||
禁用测试账号。
|
||||
@@ -0,0 +1,32 @@
|
||||
# 身份任务
|
||||
|
||||
排队身份由入口建立,payload 只包含业务名称。
|
||||
|
||||
## demo.account.Work
|
||||
|
||||
保存业务输入。
|
||||
|
||||
字段:`subject`:名称
|
||||
|
||||
## demo.account.jobSubmit
|
||||
|
||||
提交任务,不能用输入替代执行者。
|
||||
|
||||
输入:
|
||||
- `subject`:名称
|
||||
- `key`:幂等键
|
||||
输出:`id`:任务 ID
|
||||
|
||||
## demo.account.jobApply
|
||||
|
||||
通过已有账号业务保存任务结果。
|
||||
|
||||
输入:`input`:业务数据
|
||||
输出:无
|
||||
|
||||
## demo.account.jobCount
|
||||
|
||||
从独立命令读取已提交结果数量。
|
||||
|
||||
输入:无
|
||||
输出:`total`:账号数量
|
||||
@@ -0,0 +1,29 @@
|
||||
# 请求元数据业务
|
||||
|
||||
读取直接请求信息,并暂存成功响应的头和 Cookie。
|
||||
|
||||
## demo.account.Metadata
|
||||
|
||||
输出不含身份令牌或敏感请求头。
|
||||
|
||||
字段:
|
||||
- `request_id`:请求标识
|
||||
- `method`:方法
|
||||
- `path`:路径
|
||||
- `address`:直接客户端 IP
|
||||
- `header`:自定义测试头
|
||||
- `cookie`:普通 Cookie
|
||||
|
||||
## demo.account.metadata
|
||||
|
||||
读取当前请求,并为成功响应设置元数据。
|
||||
|
||||
输入:无
|
||||
输出:`value`:元数据
|
||||
|
||||
## demo.account.metadataFail
|
||||
|
||||
业务失败时必须丢弃之前暂存的元数据。
|
||||
|
||||
输入:无
|
||||
输出:无
|
||||
@@ -0,0 +1,101 @@
|
||||
# 角色业务验收
|
||||
|
||||
通过 Dever 标准库操作当前站点角色,用真实 HTTP 验证事务和权限。
|
||||
|
||||
## demo.account.permissions
|
||||
|
||||
复用当前入口的可信上下文和数据库事务。
|
||||
|
||||
输入:无
|
||||
输出:`values`:授权结果
|
||||
|
||||
## demo.account.currentPermissions
|
||||
|
||||
复用当前入口的可信上下文和数据库事务。
|
||||
|
||||
输入:无
|
||||
输出:`values`:授权结果
|
||||
|
||||
## demo.account.roles
|
||||
|
||||
复用当前入口的可信上下文和数据库事务。
|
||||
|
||||
输入:无
|
||||
输出:`values`:授权结果
|
||||
|
||||
## demo.account.role
|
||||
|
||||
复用当前入口的可信上下文和数据库事务。
|
||||
|
||||
输入:
|
||||
- `id`:授权参数
|
||||
输出:`value`:授权结果
|
||||
|
||||
## demo.account.userRoles
|
||||
|
||||
复用当前入口的可信上下文和数据库事务。
|
||||
|
||||
输入:
|
||||
- `user_id`:授权参数
|
||||
输出:`values`:授权结果
|
||||
|
||||
## demo.account.isOwner
|
||||
|
||||
复用当前入口的可信上下文和数据库事务。
|
||||
|
||||
输入:
|
||||
- `user_id`:授权参数
|
||||
输出:`value`:授权结果
|
||||
|
||||
## demo.account.saveRole
|
||||
|
||||
复用当前入口的可信上下文和数据库事务。
|
||||
|
||||
输入:
|
||||
- `id`:授权参数
|
||||
- `name`:授权参数
|
||||
- `all_permissions`:授权参数
|
||||
- `permission_keys`:授权参数
|
||||
输出:无
|
||||
|
||||
## demo.account.grantRole
|
||||
|
||||
复用当前入口的可信上下文和数据库事务。
|
||||
|
||||
输入:
|
||||
- `user_id`:授权参数
|
||||
- `role_id`:授权参数
|
||||
输出:无
|
||||
|
||||
## demo.account.revokeRole
|
||||
|
||||
复用当前入口的可信上下文和数据库事务。
|
||||
|
||||
输入:
|
||||
- `user_id`:授权参数
|
||||
- `role_id`:授权参数
|
||||
输出:无
|
||||
|
||||
## demo.account.disableRole
|
||||
|
||||
复用当前入口的可信上下文和数据库事务。
|
||||
|
||||
输入:
|
||||
- `role_id`:授权参数
|
||||
输出:无
|
||||
|
||||
## demo.account.deleteRole
|
||||
|
||||
复用当前入口的可信上下文和数据库事务。
|
||||
|
||||
输入:
|
||||
- `role_id`:授权参数
|
||||
输出:无
|
||||
|
||||
## demo.account.roleFail
|
||||
|
||||
复用当前入口的可信上下文和数据库事务。
|
||||
|
||||
输入:
|
||||
- `id`:授权参数
|
||||
输出:无
|
||||
@@ -0,0 +1,16 @@
|
||||
# 敏感 HTTP 输入
|
||||
|
||||
输入 record 可以包含 Secret,但不能生成该 record 的输出序列化器。
|
||||
|
||||
## demo.account.Credentials
|
||||
|
||||
密钥只能传给密码学函数。
|
||||
|
||||
字段:`key`:测试密钥
|
||||
|
||||
## demo.account.probe
|
||||
|
||||
只返回摘要的长度。
|
||||
|
||||
输入:`input`:密钥输入
|
||||
输出:`size`:摘要字节数
|
||||
@@ -0,0 +1,101 @@
|
||||
# 会话认证验收
|
||||
|
||||
Token 验证之后仍读取账号与会话状态;认证入口不能绕过此函数。
|
||||
|
||||
## demo.account.Identity
|
||||
|
||||
身份字段不携带权限,两个 ID 保持各自 Model 类型。
|
||||
|
||||
字段:
|
||||
- `id`:身份标识
|
||||
- `user_id`:用户 ID
|
||||
- `tenant_id`:租户 ID
|
||||
|
||||
## demo.account.verify
|
||||
|
||||
只读取全局库和当前时钟,校验账号、会话与服务端截止时间。
|
||||
|
||||
输入:`claims`:已签名的声明
|
||||
输出:`verified`:可信身份
|
||||
|
||||
## demo.account.identity
|
||||
|
||||
未找到有效会话时显式拒绝。
|
||||
|
||||
输入:`account`:匹配的账号
|
||||
输出:`verified`:可信身份
|
||||
|
||||
## demo.account.seed
|
||||
|
||||
只通过测试 CMD 准备临时数据库。
|
||||
|
||||
输入:
|
||||
- `subject`:账号标识
|
||||
- `session`:会话标识
|
||||
输出:`account`:新账号
|
||||
|
||||
## demo.account.setDeadline
|
||||
|
||||
仅供测试 CMD 调整截止时间,不依赖等待或修改 Token。
|
||||
|
||||
输入:
|
||||
- `id`:账号 ID
|
||||
- `expires`:新的截止时间
|
||||
输出:`deadline`:保存后的截止时间
|
||||
|
||||
## demo.account.revoke
|
||||
|
||||
禁用账号后,原有 Token 也必须被 verify 拒绝。
|
||||
|
||||
输入:`id`:账号 ID
|
||||
输出:`disabled`:是否已禁用
|
||||
|
||||
## demo.account.me
|
||||
|
||||
验证认证发生在调用业务之前。
|
||||
|
||||
输入:无
|
||||
输出:`profile`:当前可信身份
|
||||
|
||||
## demo.account.health
|
||||
|
||||
公开接口不因浏览器旧 Cookie 无法访问。
|
||||
|
||||
输入:无
|
||||
输出:`ready`:是否可用
|
||||
|
||||
## demo.account.Profile
|
||||
|
||||
身份数据来自隐藏上下文,业务参数不能覆盖它。
|
||||
|
||||
字段:
|
||||
- `id`:身份标识
|
||||
- `session`:会话标识
|
||||
- `site`:当前站点
|
||||
- `user_id`:用户 ID
|
||||
- `tenant_id`:租户 ID
|
||||
|
||||
## demo.account.login
|
||||
|
||||
测试凭据匹配后暂存 Cookie;使用同一全局会话验证函数。
|
||||
|
||||
输入:
|
||||
- `subject`:账号标识
|
||||
- `session`:测试会话凭据
|
||||
输出:无
|
||||
|
||||
## demo.account.logout
|
||||
|
||||
清除当前站点 Cookie,仍需同源检查。
|
||||
|
||||
输入:无
|
||||
输出:无
|
||||
|
||||
## demo.account.loginFail
|
||||
|
||||
在暂存 Cookie 后失败,验证响应不能发布尚未提交的凭据。
|
||||
|
||||
输入:
|
||||
- `subject`:账号标识
|
||||
- `session`:测试会话凭据
|
||||
输出:无
|
||||
@@ -0,0 +1,14 @@
|
||||
# 可信任务执行
|
||||
|
||||
worker 必须重新验证入队者。
|
||||
|
||||
### database
|
||||
|
||||
任务和账号共用连接。
|
||||
|
||||
## demo.account.job.perform
|
||||
|
||||
调用同领域业务。
|
||||
|
||||
输入:`input`:业务数据
|
||||
输出:无
|
||||
@@ -0,0 +1,26 @@
|
||||
# 自动 REST 数据
|
||||
|
||||
REST 只暴露公开字段,私有备注保持数据库内部值。
|
||||
|
||||
## demo.account.model.profile.Status
|
||||
|
||||
持久化选择项使用稳定名称,显示标签不参与存储。
|
||||
|
||||
分支:
|
||||
- `Active`:启用
|
||||
- `Disabled`:禁用
|
||||
|
||||
## demo.account.model.profile.Profile
|
||||
|
||||
公开名称和可选简介,私有备注不可由请求提交。创建键用请求方法和可信身份构成,仅创建时写入;唯一约束让原生测试验证 PUT 确实保留该值。
|
||||
|
||||
字段:
|
||||
- `private user_id: model.id`:可信记录所有者
|
||||
- `name`:名称
|
||||
- `bio`:简介
|
||||
- `rank`:默认排序
|
||||
- `tag`:允许显式清空的默认标签
|
||||
- `uid`:不可由客户端写入的生成标识
|
||||
- `status`:状态
|
||||
- `private note: Text?`:内部备注
|
||||
- `private creation_key: Text`:仅创建时写入的内部唯一键
|
||||
@@ -0,0 +1,9 @@
|
||||
# 租户身份类型
|
||||
|
||||
本例仅验证非租户身份,返回的 tenant_id 必须为空。
|
||||
|
||||
## demo.account.model.tenant.Tenant
|
||||
|
||||
保留租户 ID 的名义类型。
|
||||
|
||||
字段:`name`:名称
|
||||
@@ -0,0 +1,13 @@
|
||||
# 全局会话账号
|
||||
|
||||
用于原生认证验收;业务 verify 只读取 global Model。
|
||||
|
||||
## demo.account.model.Account
|
||||
|
||||
身份和会话状态由数据库决定。
|
||||
|
||||
字段:
|
||||
- `subject`:账号标识
|
||||
- `session`:会话标识
|
||||
- `active`:是否允许认证
|
||||
- `expires_at`:会话截止时间的 Unix 毫秒数
|
||||
@@ -0,0 +1,5 @@
|
||||
database default
|
||||
|
||||
job perform(input: app.Work) () retry(1) timeout(1000) {
|
||||
app.jobApply(input)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
global type Account {
|
||||
subject: Text unique
|
||||
session: Text
|
||||
active: Bool
|
||||
expires_at: Int
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
type Status {
|
||||
Active = "启用"
|
||||
Disabled = "禁用"
|
||||
}
|
||||
|
||||
type Profile {
|
||||
private owner user_id: model.id = dever.auth.userId()
|
||||
name: Text(1, 32) unique replace = text.lower(input) search = text.trim(input)
|
||||
bio: Text?
|
||||
rank: Int default 7
|
||||
tag: Text? default "automatic"
|
||||
uid: Uuid generated unique
|
||||
status: Status default Status.Active
|
||||
private note: Text? create = dever.api.header("x-source") replace = dever.api.header("x-source")
|
||||
private creation_key: Text unique create = dever.api.method() + ":" + dever.auth.id()
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
global type Tenant { name: Text }
|
||||
@@ -0,0 +1,12 @@
|
||||
# document
|
||||
|
||||
本领域的职责与行为见主题说明;函数签名和调用关系从源码读取。
|
||||
|
||||
## 说明
|
||||
|
||||
- [原始 JSON 资源接口](doc/api-admin-content.md)
|
||||
- [原始 JSON 输出入口](doc/api-admin-encode.md)
|
||||
- [原始 JSON 输出对照](doc/app.md)
|
||||
- [REST 原始 JSON 字段](doc/model.md)
|
||||
|
||||
[调用流程](link.md)由 `dever inspect . --links` 生成。
|
||||
@@ -0,0 +1 @@
|
||||
rest model
|
||||
@@ -0,0 +1 @@
|
||||
public post encode = app.encode
|
||||
@@ -0,0 +1,3 @@
|
||||
encode(value: Json) (answer: Text) {
|
||||
answer = dever.json.toText(dever.json.encode(value))
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# 原始 JSON 资源接口
|
||||
|
||||
复用认证后的自动 REST 输入边界。
|
||||
|
||||
### resource
|
||||
|
||||
创建和替换都验证整个请求的 JSON 预算。
|
||||
@@ -0,0 +1,7 @@
|
||||
# 原始 JSON 输出入口
|
||||
|
||||
让输出对照经过应用函数的正式检查与生成。
|
||||
|
||||
### encode
|
||||
|
||||
返回输入 Json 的文本表示。
|
||||
@@ -0,0 +1,12 @@
|
||||
# 原始 JSON 输出对照
|
||||
|
||||
从真实 Dever 函数生成已有 Json 的编码路径。
|
||||
|
||||
## demo.document.encode
|
||||
|
||||
返回已经验证的 Json,不在基准中另写输出实现。
|
||||
|
||||
输入:
|
||||
- `value`:已经验证的内容
|
||||
输出:
|
||||
- `answer`:JSON 原文
|
||||
@@ -0,0 +1,11 @@
|
||||
# REST 原始 JSON 字段
|
||||
|
||||
验证必填与可空 JSON 字段的输入边界。
|
||||
|
||||
## demo.document.model.Document
|
||||
|
||||
JSON 内容保留原始数字和内部空白。
|
||||
|
||||
字段:
|
||||
- `value`:必填内容
|
||||
- `extra`:可空内容
|
||||
@@ -0,0 +1,4 @@
|
||||
global type Document {
|
||||
value: Json
|
||||
extra: Json?
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
type Holder { values: List<Int> }
|
||||
|
||||
identity(values: List<Int>) (result: List<Int>) { result = values }
|
||||
|
||||
measure(values: List<Int>) (result: Int) { result = length(values) }
|
||||
|
||||
compare(values: List<Int>, size: Int) (same: Bool) { same = length(values) == size }
|
||||
|
||||
selected(keep: true, values: List<Int>) (result: List<Int>) { result = values }
|
||||
selected(keep: other, values: List<Int>) (result: List<Int>) { result = [] }
|
||||
|
||||
readField(holder: Holder) (result: List<Int>) {
|
||||
result = selected(true, holder.values)
|
||||
}
|
||||
|
||||
verify() (valid: Bool) {
|
||||
values = [1, 2, 3]
|
||||
direct = compare(values, measure(values))
|
||||
holder = Holder { values = [4, 5] }
|
||||
field = compare(holder.values, measure(holder.values))
|
||||
items = [6]
|
||||
forwarded = compare(identity(items), measure(identity(items)))
|
||||
consumed = Holder { values = [7, 8, 9, 10] }
|
||||
moved = readField(consumed)
|
||||
reused = Holder { values = [11, 12] }
|
||||
borrowed = readField(reused)
|
||||
preserved = compare(reused.values, measure(borrowed))
|
||||
repeated = Holder { values = [13, 14, 15] }
|
||||
duplicated = compare(repeated.values, measure(selected(true, repeated.values)))
|
||||
valid = direct and field and forwarded and length(moved) == 4 and preserved and duplicated
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
increment(item: Int) (answer: Int) {
|
||||
answer = item + 1
|
||||
}
|
||||
|
||||
add(item: Int, state: Int) (next: Int) {
|
||||
next = state + item
|
||||
}
|
||||
|
||||
stopAdd(item: Int, state: Int) (next: Int, stop: Bool) {
|
||||
next = state + item
|
||||
stop = item == 128
|
||||
}
|
||||
|
||||
total(bytes: Bytes) (answer: Int) {
|
||||
answer = reduce(add, bytes, 0)
|
||||
}
|
||||
|
||||
incremented(bytes: Bytes) (answer: List<Int>) {
|
||||
answer = each(increment, bytes)
|
||||
}
|
||||
|
||||
encode(text: Text) (answer: Bytes) {
|
||||
answer = dever.bytes.fromText(text)
|
||||
}
|
||||
|
||||
decode(bytes: Bytes) (answer: Text) {
|
||||
answer = dever.bytes.toText(bytes)
|
||||
}
|
||||
|
||||
convert(values: List<Int>) (answer: Bytes) {
|
||||
answer = dever.bytes.fromInts(values)
|
||||
}
|
||||
|
||||
middle(bytes: Bytes) (answer: Bytes) {
|
||||
answer = dever.bytes.slice(bytes, 1, length(bytes) - 1)
|
||||
}
|
||||
|
||||
join(bytes: Bytes) (answer: Bytes) {
|
||||
answer = dever.bytes.concat(bytes, bytes)
|
||||
}
|
||||
|
||||
copyBytes(bytes: Bytes) (answer: Bytes) {
|
||||
answer = dever.bytes.slice(bytes, 0, length(bytes))
|
||||
}
|
||||
|
||||
identity(bytes: Bytes) (answer: Bytes) { answer = bytes }
|
||||
|
||||
roundtrip(text: Text) (answer: Text) {
|
||||
answer = dever.bytes.toText(dever.bytes.fromText(text))
|
||||
}
|
||||
|
||||
bufferFailed(result: dever.bytes.BufferResult.Ready(bytes)) (answer: Bool) recover("verify expected conversion failures") {
|
||||
answer = false
|
||||
}
|
||||
bufferFailed(result: dever.bytes.BufferResult.Failed(message)) (answer: Bool) recover("verify expected conversion failures") {
|
||||
answer = message == "byte value must be between 0 and 255" or message == "invalid byte slice range"
|
||||
}
|
||||
|
||||
decodeFailed(result: dever.bytes.DecodeResult.Decoded(text)) (answer: Bool) recover("verify expected conversion failures") {
|
||||
answer = false
|
||||
}
|
||||
decodeFailed(result: dever.bytes.DecodeResult.Failed(message)) (answer: Bool) recover("verify expected conversion failures") {
|
||||
answer = message == "invalid UTF-8"
|
||||
}
|
||||
|
||||
lengthOrZero(bytes: Bytes) (answer: Int) {
|
||||
answer = length(bytes)
|
||||
}
|
||||
lengthOrZero(bytes: null) (answer: Int) {
|
||||
answer = -1
|
||||
}
|
||||
|
||||
verify() (answer: Bool) {
|
||||
bytes = convert([0, 127, 128, 255])
|
||||
answer = length(bytes) == 4 and dever.bytes.length(bytes) == 4
|
||||
answer = answer and dever.bytes.at(bytes, 0) == 0 and dever.bytes.at(bytes, 3) == 255
|
||||
answer = answer and dever.bytes.at(bytes, -1) == null and dever.bytes.at(bytes, 4) == null
|
||||
answer = answer and dever.bytes.at(bytes, 9223372036854775807) == null
|
||||
answer = answer and total(bytes) == 510 and incremented(bytes) == [1, 128, 129, 256]
|
||||
answer = answer and reduceUntil(stopAdd, bytes, 1) == 256
|
||||
answer = answer and total(convert([])) == 0 and length(incremented(convert([]))) == 0
|
||||
answer = answer and reduce(add, bytes, -510) == 0
|
||||
answer = answer and reduce(add, bytes, 9223372036854775297) == 9223372036854775807
|
||||
answer = answer and reduce(add, convert([]), 9223372036854775807) == 9223372036854775807
|
||||
answer = answer and lengthOrZero(convert([])) == 0 and lengthOrZero(null) == -1
|
||||
answer = answer and middle(bytes) == convert([127, 128])
|
||||
answer = answer and dever.bytes.slice(bytes, 4, 4) == convert([])
|
||||
answer = answer and bufferFailed(result(convert([-1]))) and bufferFailed(result(convert([256])))
|
||||
answer = answer and bufferFailed(result(dever.bytes.slice(bytes, 2, 1)))
|
||||
answer = answer and bufferFailed(result(dever.bytes.slice(bytes, -1, 0)))
|
||||
answer = answer and bufferFailed(result(dever.bytes.slice(bytes, 0, 5)))
|
||||
answer = answer and decodeFailed(result(decode(convert([255]))))
|
||||
answer = answer and decodeFailed(result(decode(convert([192, 128]))))
|
||||
answer = answer and decodeFailed(result(decode(convert([237, 160, 128]))))
|
||||
answer = answer and decodeFailed(result(decode(convert([226, 130]))))
|
||||
text = "雪🙂"
|
||||
answer = answer and length(encode(text)) == 7 and roundtrip(text) == text
|
||||
answer = answer and roundtrip("") == ""
|
||||
texts = ["雪🙂", "", "a"]
|
||||
encoded = each(dever.bytes.fromText, texts)
|
||||
answer = answer and texts == ["雪🙂", "", "a"]
|
||||
answer = answer and each(dever.bytes.toText, encoded) == texts
|
||||
answer = answer and each(dever.bytes.toText, each(dever.bytes.fromText, ["雪🙂", "", "a"])) == texts
|
||||
answer = answer and length(each(dever.bytes.fromText, [])) == 0
|
||||
answer = answer and each(dever.bytes.toText, each(dever.bytes.fromText, text.split("雪,\u{0},🙂", ","))) == ["雪", "\u{0}", "🙂"]
|
||||
answer = answer and each(dever.bytes.toText, each(dever.bytes.fromText, text.split("雪🙂", ""))) == ["雪", "🙂"]
|
||||
answer = answer and each(dever.bytes.toText, each(dever.bytes.fromText, text.split(",x,", ","))) == ["", "x", ""]
|
||||
answer = answer and length(each(dever.bytes.fromText, text.split("", ""))) == 0
|
||||
answer = answer and each(dever.bytes.toText, [convert([0]), convert([97])]) == ["\u{0}", "a"]
|
||||
answer = answer and encode(decode(convert([97, 0, 98]))) == convert([97, 0, 98])
|
||||
combined = join(bytes)
|
||||
answer = answer and combined == convert([0, 127, 128, 255, 0, 127, 128, 255])
|
||||
answer = answer and bytes == convert([0, 127, 128, 255])
|
||||
prefix = "雪\u{0}🙂"
|
||||
saved_prefix = prefix
|
||||
expected_prefix = convert([233, 155, 170, 0, 240, 159, 153, 130])
|
||||
prefixed_bytes = prefixed(bytes)
|
||||
answer = answer and prefixed_bytes == dever.bytes.concat(expected_prefix, bytes)
|
||||
answer = answer and prefixed(convert([])) == expected_prefix
|
||||
answer = answer and dever.bytes.concat(dever.bytes.fromText(""), bytes) == bytes
|
||||
answer = answer and dever.bytes.concat(dever.bytes.fromText(prefix), bytes) == prefixed_bytes
|
||||
answer = answer and prefix == saved_prefix and bytes == convert([0, 127, 128, 255])
|
||||
seed = convert([1, 2])
|
||||
saved = seed
|
||||
joined = reduce(joinPart, [convert([0, 255]), convert([]), convert([3])], seed)
|
||||
answer = answer and joined == convert([1, 2, 0, 255, 3]) and saved == convert([1, 2])
|
||||
answer = answer and reduce(joinPart, [], bytes) == bytes
|
||||
answer = answer and reduce(joinPart, [bytes, bytes], convert([])) == combined
|
||||
answer = answer and reduce(joinPart, stream([convert([1]), convert([]), convert([255])]), convert([])) == convert([1, 255])
|
||||
answer = answer and dever.bytes.toText(reduce(joinPart, [encode("a"), encode("雪")], encode("开始"))) == "开始a雪"
|
||||
answer = answer and reduce(joinPart, [], encode("")) == convert([])
|
||||
answer = answer and joinedText("雪\u{0}🙂", seed) == dever.bytes.concat(seed, expected_prefix)
|
||||
answer = answer and joinedText("", seed) == seed and seed == saved
|
||||
answer = answer and joinedReplaced("雪🙂", seed) == dever.bytes.concat(dever.bytes.concat(seed, encode("雪🙂")), seed)
|
||||
}
|
||||
|
||||
joinPart(item: Bytes, state: Bytes) (answer: Bytes) { answer = dever.bytes.concat(state, item) }
|
||||
|
||||
prefixed(bytes: Bytes) (answer: Bytes) {
|
||||
answer = dever.bytes.concat(dever.bytes.fromText("雪\u{0}🙂"), bytes)
|
||||
}
|
||||
|
||||
suffixOnce() (bytes: Bytes) {
|
||||
dever.io.println("suffix")
|
||||
bytes = convert([0, 255])
|
||||
}
|
||||
|
||||
concatOrderEntry() (answer: Bool) {
|
||||
bytes = dever.bytes.concat(dever.bytes.fromText("雪\u{0}🙂"), suffixOnce())
|
||||
answer = bytes == convert([233, 155, 170, 0, 240, 159, 153, 130, 0, 255])
|
||||
}
|
||||
|
||||
concatArgumentEntry() (answer: Bytes) {
|
||||
answer = dever.bytes.concat(dever.bytes.fromText("雪\u{0}"), convert([256]))
|
||||
}
|
||||
|
||||
concatLeftErrorEntry() (answer: Bytes) {
|
||||
answer = dever.bytes.concat(dever.bytes.fromText(decode(convert([255]))), convert([256]))
|
||||
}
|
||||
|
||||
invalidEntry() (answer: Text) {
|
||||
answer = decode(convert([255]))
|
||||
}
|
||||
|
||||
invalidListEntry() (answer: List<Text>) {
|
||||
answer = each(dever.bytes.toText, [convert([97]), convert([255])])
|
||||
}
|
||||
|
||||
overflowEntry() (answer: Int) {
|
||||
answer = reduce(add, convert([1]), 9223372036854775807)
|
||||
}
|
||||
|
||||
argumentEntry() (answer: dever.bytes.DecodeResult) {
|
||||
answer = result(decode(convert([256])))
|
||||
}
|
||||
|
||||
joinedText(text: Text, seed: Bytes) (answer: Bytes) {
|
||||
parts = each(dever.bytes.fromText, text.split(text, ""))
|
||||
answer = reduce(joinPart, parts, seed)
|
||||
}
|
||||
|
||||
joinedReplaced(text: Text, seed: Bytes) (answer: Bytes) {
|
||||
parts = each(dever.bytes.fromText, text.split(text, ""))
|
||||
first = reduce(joinPart, parts, seed)
|
||||
parts = [seed]
|
||||
answer = reduce(joinPart, parts, first)
|
||||
}
|
||||
|
||||
joinedSeedErrorEntry() (answer: Bytes) {
|
||||
answer = joinedSeedError("雪🙂")
|
||||
}
|
||||
|
||||
joinedSeedError(text: Text) (answer: Bytes) {
|
||||
parts = each(dever.bytes.fromText, text.split(text, ""))
|
||||
answer = reduce(joinPart, parts, convert([256]))
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
verify() (answer: Bool) pure {
|
||||
value = dever.time.parseDatetime("2024-02-29T12:34:56.12+08:00")
|
||||
formatted = dever.time.formatDatetime(value)
|
||||
date = dever.time.formatDate(dever.time.parseDate("2024-02-29"))
|
||||
clock = dever.time.formatTime(dever.time.parseTime("12:34:56.000"))
|
||||
epoch = dever.time.parseDatetime("1970-01-01T00:00:00Z")
|
||||
span = dever.time.duration(1)
|
||||
later = dever.time.add(epoch, span)
|
||||
earlier = dever.time.subtract(epoch, span)
|
||||
answer = formatted == "2024-02-29T04:34:56.120Z" and date == "2024-02-29" and clock == "12:34:56" and dever.time.difference(later, epoch) == span and dever.time.formatDatetime(earlier) == "1969-12-31T23:59:59.999Z"
|
||||
}
|
||||
|
||||
clocks() (answer: Bool) {
|
||||
current = dever.time.now()
|
||||
round_trip = dever.time.parseDatetime(dever.time.formatDatetime(current))
|
||||
unix = dever.time.unixMillis()
|
||||
first = dever.time.monotonicNanos()
|
||||
second = dever.time.monotonicNanos()
|
||||
answer = current == round_trip and unix > 0 and second >= first
|
||||
}
|
||||
|
||||
invalid() (value: Date) { value = dever.time.parseDate("2023-02-29") }
|
||||
@@ -0,0 +1,56 @@
|
||||
type Result { Done
|
||||
error Failed(message: Text) }
|
||||
|
||||
type Message { Value(value: Int?) }
|
||||
|
||||
produce(values: Channel<Int>) () { send(values, 10)
|
||||
send(values, 20)
|
||||
close(values) }
|
||||
|
||||
handled(value: null) (answer: Bool) recover("处理接收错误") { answer = false }
|
||||
handled(value: Result.Done) (answer: Bool) recover("处理接收错误") { answer = false }
|
||||
handled(value: Result.Failed(message)) (answer: Bool) recover("处理接收错误") { answer = message == "reason" }
|
||||
|
||||
nested(value: null) (answer: Bool) { answer = false }
|
||||
nested(value: Channel<Int>) (answer: Bool) { answer = receive(value) == 7 }
|
||||
|
||||
verify() (answer: Bool) {
|
||||
values = channel(Int, 1)
|
||||
pending = run(produce(values))
|
||||
first = receive(values)
|
||||
second = receive(values)
|
||||
end = receive(values)
|
||||
wait(pending)
|
||||
nullable_values = channel(Message, 2)
|
||||
send(nullable_values, Message.Value(null))
|
||||
send(nullable_values, Message.Value(30))
|
||||
nullable_item = receive(nullable_values)
|
||||
present = receive(nullable_values)
|
||||
alias = nullable_values
|
||||
close(alias)
|
||||
nullable_end = receive(nullable_values)
|
||||
lists = channel(List<Int>, 1)
|
||||
original = [1, 2]
|
||||
send(lists, original)
|
||||
original = append(original, 3)
|
||||
copied = receive(lists)
|
||||
nested_values = channel(Channel<Int>, 1)
|
||||
child = channel(Int, 1)
|
||||
send(child, 7)
|
||||
send(nested_values, child)
|
||||
nested_ok = nested(receive(nested_values))
|
||||
failures = channel(Result, 1)
|
||||
send(failures, Result.Failed("reason"))
|
||||
error_ok = handled(receive(failures))
|
||||
answer = first == 10 and second == 20 and end == null and nullable_item == Message.Value(null) and present == Message.Value(30) and nullable_end == null and copied == [1, 2] and original == [1, 2, 3] and nested_ok and error_ok
|
||||
}
|
||||
|
||||
creation() (value: Channel<Int>) { value = channel(Int, 1) }
|
||||
|
||||
nullable() (value: Channel<Int>?) { value = null }
|
||||
|
||||
invalid() () { values = channel(Int, 0) }
|
||||
|
||||
closed() () { values = channel(Int, 1)
|
||||
close(values)
|
||||
send(values, 42) }
|
||||
@@ -0,0 +1,204 @@
|
||||
type State {
|
||||
Idle
|
||||
Working(value: Int)
|
||||
Label(text: Text)
|
||||
}
|
||||
|
||||
type Result {
|
||||
Ready(value: Int)
|
||||
error Failed(message: Text)
|
||||
}
|
||||
|
||||
type Problem {
|
||||
error Denied(code: Int)
|
||||
error Missing(message: Text)
|
||||
}
|
||||
|
||||
type Captured {
|
||||
Done(value: Int)
|
||||
error Rejected(problem: Problem)
|
||||
}
|
||||
|
||||
readState(value: State.Idle) (answer: Int) pure {
|
||||
answer = 0
|
||||
}
|
||||
readState(value: State.Working(number)) (answer: Int) pure {
|
||||
answer = number
|
||||
}
|
||||
readState(value: State.Label(text)) (answer: Int) pure {
|
||||
answer = length(text)
|
||||
}
|
||||
|
||||
nullableState(value: null) (answer: Int) pure {
|
||||
answer = -1
|
||||
}
|
||||
nullableState(value: State.Working(number)) (answer: Int) pure {
|
||||
answer = number
|
||||
}
|
||||
nullableState(value: other) (answer: Int) pure {
|
||||
answer = readState(value)
|
||||
}
|
||||
|
||||
nullableText(value: other) (answer: Int) pure {
|
||||
answer = length(value)
|
||||
}
|
||||
nullableText(value: null) (answer: Int) pure {
|
||||
answer = -1
|
||||
}
|
||||
nullableText(value: "zero") (answer: Int) pure {
|
||||
answer = 0
|
||||
}
|
||||
|
||||
change(value: State.Label(text)) (answer: State) {
|
||||
text = text + "新"
|
||||
answer = State.Label(text)
|
||||
}
|
||||
change(value: other) (answer: State) {
|
||||
answer = value
|
||||
}
|
||||
|
||||
operation(value: Int >= 0) (answer: Int) pure {
|
||||
answer = value
|
||||
}
|
||||
operation(value: other) (answer: Int) pure {
|
||||
fail(Result.Failed("negative"))
|
||||
}
|
||||
|
||||
propagate(value: Int) (answer: Int) pure {
|
||||
answer = operation(value)
|
||||
}
|
||||
|
||||
capture(value: Int) (answer: Result) pure {
|
||||
answer = result(propagate(value))
|
||||
}
|
||||
|
||||
recoverValue(value: Result.Ready(number)) (answer: Int) pure recover("use message length for this diagnostic probe") {
|
||||
answer = number
|
||||
}
|
||||
recoverValue(value: Result.Failed(message)) (answer: Int) recover("use message length for this diagnostic probe") pure {
|
||||
answer = length(message)
|
||||
}
|
||||
|
||||
requireValue(value: Result.Ready(number)) (answer: Int) pure {
|
||||
answer = number
|
||||
}
|
||||
requireValue(value: Result.Failed(message)) (answer: Int) pure {
|
||||
fail(Result.Failed(message))
|
||||
}
|
||||
|
||||
deny(value: Int) (answer: Int) {
|
||||
fail(Problem.Denied(value))
|
||||
}
|
||||
|
||||
captureDomain(value: Int) (answer: Captured) {
|
||||
answer = result(deny(value))
|
||||
}
|
||||
|
||||
problemValue(value: Problem.Denied(code)) (answer: Int) recover("inspect domain payload in the probe") {
|
||||
answer = code
|
||||
}
|
||||
problemValue(value: Problem.Missing(message)) (answer: Int) recover("inspect domain payload in the probe") {
|
||||
answer = length(message)
|
||||
}
|
||||
|
||||
capturedValue(value: Captured.Done(number)) (answer: Int) recover("inspect wrapped result in the probe") {
|
||||
answer = number
|
||||
}
|
||||
capturedValue(value: Captured.Rejected(problem)) (answer: Int) recover("inspect wrapped result in the probe") {
|
||||
answer = problemValue(problem)
|
||||
}
|
||||
|
||||
optionalEqual(value: Int?) (answer: Bool) pure {
|
||||
answer = value == null or null == value or value == 1 or 1 == value
|
||||
}
|
||||
|
||||
increment(value: Int) (answer: Int) pure {
|
||||
answer = value + 1
|
||||
}
|
||||
|
||||
faultEntry() (answer: Int) recover("only recover business errors in the probe") {
|
||||
answer = recoverValue(result(increment(9223372036854775807)))
|
||||
}
|
||||
|
||||
failureEntry() (answer: Int) {
|
||||
answer = propagate(-1)
|
||||
}
|
||||
|
||||
type Ack {
|
||||
Done
|
||||
error Stopped
|
||||
}
|
||||
|
||||
argumentEntry() (answer: Result) {
|
||||
answer = result(operation(deny(5)))
|
||||
}
|
||||
|
||||
optionalFloat() (answer: Float?) {
|
||||
answer = 1.5 + 2.5
|
||||
}
|
||||
|
||||
optionalList() (answer: List<Int>?) {
|
||||
answer = []
|
||||
}
|
||||
|
||||
optionalLength(value: null) (answer: Int) { answer = -1 }
|
||||
optionalLength(value: List<Int>) (answer: Int) { answer = length(value) }
|
||||
|
||||
act(ready: true) () {}
|
||||
act(ready: false) () {
|
||||
fail(Ack.Stopped)
|
||||
}
|
||||
|
||||
ack(ready: Bool) (answer: Ack) {
|
||||
answer = result(act(ready))
|
||||
}
|
||||
|
||||
ackValue(value: Ack.Done) (answer: Bool) recover("represent optional probe failure as false") {
|
||||
answer = true
|
||||
}
|
||||
ackValue(value: Ack.Stopped) (answer: Bool) recover("represent optional probe failure as false") {
|
||||
answer = false
|
||||
}
|
||||
|
||||
type PairResult {
|
||||
Ready(text: Text, number: Int)
|
||||
error Stopped
|
||||
}
|
||||
|
||||
pair(ready: true) (text: Text, number: Int) {
|
||||
text = "雪"
|
||||
number = 42
|
||||
}
|
||||
pair(ready: false) (text: Text, number: Int) {
|
||||
fail(PairResult.Stopped)
|
||||
}
|
||||
|
||||
capturePair(ready: Bool) (answer: PairResult) {
|
||||
answer = result(pair(ready))
|
||||
}
|
||||
|
||||
pairValid(value: PairResult.Ready(text, number)) (answer: Bool) recover("failed optional pair probe is false") {
|
||||
answer = text == "雪" and number == 42
|
||||
}
|
||||
pairValid(value: PairResult.Stopped) (answer: Bool) recover("failed optional pair probe is false") {
|
||||
answer = false
|
||||
}
|
||||
|
||||
optionalCapture(value: Int) (answer: Result?) {
|
||||
answer = result(operation(value))
|
||||
}
|
||||
|
||||
optionalResult(value: null) (answer: Int) {
|
||||
answer = 0
|
||||
}
|
||||
optionalResult(value: Result) (answer: Int) {
|
||||
answer = recoverValue(value)
|
||||
}
|
||||
|
||||
verify() (answer: Bool) {
|
||||
original = State.Label("雪")
|
||||
changed = change(original)
|
||||
base = readState(State.Idle) == 0 and readState(State.Working(42)) == 42 and readState(original) == 1 and readState(changed) == 2 and nullableState(null) == -1 and nullableState(State.Working(7)) == 7 and nullableState(State.Idle) == 0 and nullableText(null) == -1 and nullableText("zero") == 0 and nullableText("雪山") == 2
|
||||
failures = recoverValue(capture(42)) == 42 and recoverValue(capture(-1)) == 8 and requireValue(capture(9)) == 9 and capturedValue(captureDomain(23)) == 23 and optionalEqual(null) and optionalEqual(1) and not optionalEqual(2)
|
||||
answer = base and failures and ackValue(ack(true)) and not ackValue(ack(false)) and pairValid(capturePair(true)) and not pairValid(capturePair(false)) and optionalResult(optionalCapture(-1)) == 8 and optionalResult(null) == 0 and optionalFloat() == 4e0 and optionalLength(optionalList()) == 0
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
type Row {
|
||||
name: Text
|
||||
count: Int
|
||||
}
|
||||
|
||||
type Result {
|
||||
Ready(value: Int)
|
||||
error Failed(value: Int)
|
||||
}
|
||||
|
||||
checked(value: Int < 0) (answer: Int) {
|
||||
fail(Result.Failed(value))
|
||||
}
|
||||
checked(value: other) (answer: Int) {
|
||||
answer = value
|
||||
}
|
||||
|
||||
double(value: Int) (answer: Int) {
|
||||
answer = value * 2
|
||||
}
|
||||
|
||||
positive(value: Int) (answer: Bool) {
|
||||
answer = value > 0
|
||||
}
|
||||
|
||||
above(value: Int, threshold: Int) (answer: Bool) {
|
||||
answer = value > threshold
|
||||
}
|
||||
|
||||
add(value: Int, state: Int) (next: Int) {
|
||||
next = state + value
|
||||
}
|
||||
|
||||
stop(value: Int, state: Int) (next: Int, stop: Bool) {
|
||||
next = state + value
|
||||
stop = next >= 3
|
||||
}
|
||||
|
||||
ignore(value: Int) () {
|
||||
|
||||
}
|
||||
|
||||
append(value: Text, suffix: Text) (answer: Text) {
|
||||
answer = value + suffix
|
||||
}
|
||||
|
||||
change(value: Row) (answer: Row) {
|
||||
answer = value
|
||||
answer.name = value.name + "新"
|
||||
answer.count = value.count + 1
|
||||
}
|
||||
|
||||
keepRow(value: Row, threshold: Int) (answer: Bool) {
|
||||
answer = value.count > threshold
|
||||
}
|
||||
|
||||
extend(value: Text, state: Text) (next: Text) {
|
||||
next = state + value
|
||||
}
|
||||
|
||||
mapInts(values: List<Int>) (answer: List<Int>) {
|
||||
answer = each(double, values)
|
||||
}
|
||||
|
||||
filterInts(values: List<Int>) (answer: List<Int>) {
|
||||
answer = filter(positive, values)
|
||||
}
|
||||
|
||||
findInt(values: List<Int>, threshold: Int) (answer: Int?) {
|
||||
answer = find(above, values, threshold)
|
||||
}
|
||||
|
||||
projectSum(values: List<Int>) (answer: Int) {
|
||||
answer = sum(double, values)
|
||||
}
|
||||
|
||||
foldSum(values: List<Int>) (answer: Int) {
|
||||
answer = reduce(add, values, 0)
|
||||
}
|
||||
|
||||
stopSum(values: List<Int>) (answer: Int) {
|
||||
answer = reduceUntil(stop, values, 0)
|
||||
}
|
||||
|
||||
mapRows(values: List<Row>) (answer: List<Row>) {
|
||||
answer = each(change, values)
|
||||
}
|
||||
|
||||
projectFloat(value: Float) (answer: Float) {
|
||||
answer = value * 2.0
|
||||
}
|
||||
|
||||
projectDecimal(value: Decimal) (answer: Decimal) {
|
||||
answer = value * 2
|
||||
}
|
||||
|
||||
floatSum(values: List<Float>) (answer: Float) {
|
||||
answer = sum(projectFloat, values)
|
||||
}
|
||||
|
||||
decimalSum(values: List<Decimal>) (answer: Decimal) {
|
||||
answer = sum(projectDecimal, values)
|
||||
}
|
||||
|
||||
captureList(values: List<Int>) (answer: Result) {
|
||||
answer = result(checkedSum(values))
|
||||
}
|
||||
|
||||
checkedSum(values: List<Int>) (answer: Int) {
|
||||
answer = sum(checked, values)
|
||||
}
|
||||
|
||||
insertNext(item: Int, state: Map<Int, Int>) (next: Map<Int, Int>) {
|
||||
snapshot = state
|
||||
key = length(snapshot)
|
||||
key = key + 1
|
||||
next = put(state, key, item + length(snapshot))
|
||||
}
|
||||
|
||||
verify() (answer: Bool) recover("compare captured test failures with their expected values") {
|
||||
values = [1, -2, 3]
|
||||
original = Row { name = "雪"
|
||||
count = 2 }
|
||||
rows = [original]
|
||||
changed = mapRows(rows)
|
||||
selected = filter(keepRow, rows, 1)
|
||||
found = find(keepRow, rows, 1)
|
||||
texts = each(append, ["雪", "山"], "新")
|
||||
each(ignore, values)
|
||||
empty = each(double, [])
|
||||
answer = mapInts(values) == [2, -4, 6]
|
||||
answer = answer and filterInts(values) == [1, 3] and findInt(values, 1) == 3 and findInt(values, 3) == null
|
||||
answer = answer and projectSum(values) == 4 and foldSum(values) == 2 and stopSum([1, 2, 9223372036854775807]) == 3
|
||||
answer = answer and length(empty) == 0 and sum(double, []) == 0 and reduce(add, [], 9) == 9 and reduceUntil(stop, [], 9) == 9
|
||||
answer = answer and reduce(checkedAdd, [], 7) == 7 and reduce(checkedAdd, [1, 2, 3], 7) == 13
|
||||
answer = answer and texts == ["雪新", "山新"] and reduce(extend, ["雪", "山"], "前") == "前雪山"
|
||||
answer = answer and changed == [Row { name = "雪新"
|
||||
count = 3 }] and selected == rows and found == original and original.name == "雪" and original.count == 2
|
||||
answer = answer and floatSum([1.5, 2.5]) == floatEight() and decimalSum([1.5, 2.5]) == 8
|
||||
answer = answer and floatSum([]) == floatZero() and decimalSum([]) == 0
|
||||
answer = answer and captureList([1, -2, 3]) == Result.Failed(-2) and captureList([1, 2, 3]) == Result.Ready(6)
|
||||
partial = changePartial(original)
|
||||
dependent = changeDependent(original)
|
||||
answer = answer and partial.name == "雪局" and partial.count == 2 and dependent.name == "雪顺" and dependent.count == 2 and original.name == "雪"
|
||||
table = {0 = 7}
|
||||
inserted = reduce(insertNext, [2, 3], table)
|
||||
answer = answer and inserted == {0 = 7, 2 = 3, 3 = 5} and table == {0 = 7}
|
||||
answer = answer and reduce(insertNext, [], table) == table
|
||||
}
|
||||
|
||||
failureEntry() (answer: Int) {
|
||||
answer = checkedSum([1, -2, 3])
|
||||
}
|
||||
|
||||
overflowEntry() (answer: Int) {
|
||||
answer = sum(addContext, [9223372036854775807, 1], 0)
|
||||
}
|
||||
|
||||
addContext(value: Int, context: Int) (answer: Int) {
|
||||
answer = value + context
|
||||
}
|
||||
|
||||
handlerFaultEntry() (answer: Int) {
|
||||
answer = projectSum([9223372036854775807])
|
||||
}
|
||||
|
||||
extraEntry() (answer: Int) {
|
||||
answer = sum(addContext, [], checked(-9))
|
||||
}
|
||||
|
||||
floatEight() (answer: Float) {
|
||||
answer = 8.0
|
||||
}
|
||||
|
||||
floatZero() (answer: Float) {
|
||||
answer = 0.0
|
||||
}
|
||||
|
||||
changePartial(value: Row) (answer: Row) {
|
||||
answer = value
|
||||
answer.name = value.name + "局"
|
||||
}
|
||||
|
||||
changeDependent(value: Row) (answer: Row) {
|
||||
answer = value
|
||||
answer.name = value.name + "顺"
|
||||
answer.count = length(answer.name)
|
||||
}
|
||||
|
||||
failedRow(value: Row) (answer: Row) {
|
||||
amount = checked(value.count)
|
||||
answer = Row { name = value.name
|
||||
count = amount }
|
||||
}
|
||||
|
||||
changeFailed(value: Row) (answer: Row) {
|
||||
answer = failedRow(value)
|
||||
answer.name = "替"
|
||||
answer.count = 0
|
||||
}
|
||||
|
||||
initializeEntry() (answer: Row) {
|
||||
answer = changeFailed(Row { name = "原"
|
||||
count = -1 })
|
||||
}
|
||||
|
||||
checkedAdd(value: Int, state: Int) (next: Int) {
|
||||
next = state + checked(value)
|
||||
}
|
||||
|
||||
reductionFailureEntry() (answer: Int) {
|
||||
answer = reduce(checkedAdd, [1, -2, 9223372036854775807], 0)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
type Row {
|
||||
name: Text
|
||||
count: Int
|
||||
}
|
||||
|
||||
sign(value: Int < 0) (answer: Int) pure {
|
||||
answer = -1
|
||||
}
|
||||
sign(value: 0) (answer: Int) pure {
|
||||
answer = 0
|
||||
}
|
||||
sign(value: Int > 0) (answer: Int) pure {
|
||||
answer = 1
|
||||
}
|
||||
|
||||
main() (answer: Int) {
|
||||
answer = sum([10, 20, 11]) + sign(3)
|
||||
}
|
||||
|
||||
copyCheck() (answer: Bool) {
|
||||
original = Row { name = "初始"
|
||||
count = 1 }
|
||||
changed = original
|
||||
changed.name = "修改"
|
||||
answer = original.name == "初始" and changed.name == "修改"
|
||||
}
|
||||
|
||||
join(first: Text, second: Text) (answer: Text) pure {
|
||||
answer = first + second
|
||||
}
|
||||
|
||||
textCheck() (answer: Text) {
|
||||
value = "雪"
|
||||
joined = join(value, value)
|
||||
answer = joined + value
|
||||
}
|
||||
|
||||
increment(value: Int) (answer: Int) pure {
|
||||
answer = value + 1
|
||||
}
|
||||
|
||||
overflowCheck() (answer: Int) {
|
||||
answer = increment(9223372036854775807)
|
||||
}
|
||||
|
||||
change(value: Row) (answer: Row) {
|
||||
value.name = value.name + "新"
|
||||
answer = value
|
||||
}
|
||||
|
||||
pair(value: Int) (count: Int, name: Text) pure {
|
||||
count = value
|
||||
name = "雪"
|
||||
}
|
||||
|
||||
total(values: List<Int>) (answer: Int) pure {
|
||||
answer = sum(values)
|
||||
}
|
||||
|
||||
verify() (answer: Bool) {
|
||||
original = Row { name = "雪"
|
||||
count = 1 }
|
||||
changed = change(original)
|
||||
values = [1, 2, 3]
|
||||
result = pair(42)
|
||||
decimal = 1 / 8
|
||||
floating = 1.5e0 + 2.0e0
|
||||
skipped = false and overflowCheck() > 0
|
||||
answer = main() == 42 and copyCheck() and textCheck() == "雪雪雪" and original.name == "雪" and changed.name == "雪新" and total(values) == 6 and sum(values) == 6 and result.count == 42 and result.name == "雪" and decimal == 0.125 and floating == 3.5e0 and not skipped and length("雪山") == 2 and sign(-3) == -1 and sign(0) == 0
|
||||
}
|
||||
|
||||
arithmetic(value: Int) (answer: Int) pure {
|
||||
answer = (value * 3 + 7) // 2
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
verify() (answer: Bool) {
|
||||
password = dever.crypto.token(16)
|
||||
wrong = dever.crypto.token(32)
|
||||
hash = dever.crypto.passwordHash(password)
|
||||
accepted = dever.crypto.passwordVerify(password, hash)
|
||||
rejected = dever.crypto.passwordVerify(wrong, hash)
|
||||
data = dever.bytes.fromText("message")
|
||||
first = dever.crypto.hmacSha256(password, data)
|
||||
second = dever.crypto.hmacSha256(password, data)
|
||||
digest = dever.crypto.sha256(dever.bytes.fromText("abc"))
|
||||
expected = dever.bytes.fromInts([186, 120, 22, 191, 143, 1, 207, 234, 65, 65, 64, 222, 93, 174, 34, 35, 176, 3, 97, 163, 150, 23, 122, 156, 180, 16, 255, 97, 242, 0, 21, 173])
|
||||
answer = accepted and not rejected and dever.crypto.constantTimeEq(first, second) and dever.bytes.length(first) == 32 and dever.crypto.constantTimeEq(digest, expected)
|
||||
}
|
||||
|
||||
asyncVerify() (answer: Bool) {
|
||||
password = dever.crypto.token(16)
|
||||
hash = blocking(dever.crypto.passwordHash(password))
|
||||
answer = blocking(dever.crypto.passwordVerify(password, hash))
|
||||
}
|
||||
|
||||
rejected(value: dever.crypto.VerifyResult.Ready(valid)) (answer: Bool) recover("观察不支持的PHC") { answer = false }
|
||||
rejected(value: dever.crypto.VerifyResult.Failed(message)) (answer: Bool) recover("观察不支持的PHC") { answer = length(message) > 0 }
|
||||
|
||||
phcVerify() (answer: Bool) {
|
||||
password = dever.crypto.token(16)
|
||||
answer = rejected(result(dever.crypto.passwordVerify(password, "$argon2id$v=19$m=19456,t=2,p=536870912$c29tZXNhbHQ$AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")))
|
||||
}
|
||||
|
||||
invalid() () { password = dever.crypto.token(0) }
|
||||
|
||||
leak() (value: Secret?) { value = null }
|
||||
|
||||
leakList() (values: List<Secret>) { values = [] }
|
||||
@@ -0,0 +1,11 @@
|
||||
# account
|
||||
|
||||
本领域的职责与行为见主题说明;函数签名和调用关系从源码读取。
|
||||
|
||||
## 说明
|
||||
|
||||
- [读取验收入口](doc/api.md)
|
||||
- [读取验收业务](doc/app.md)
|
||||
- [读取验收存储](doc/model.md)
|
||||
|
||||
[调用流程](link.md)由 `dever inspect . --links` 生成。
|
||||
@@ -0,0 +1,13 @@
|
||||
cmd lookup = app.lookup
|
||||
|
||||
cmd names = app.names
|
||||
|
||||
cmd count = app.count
|
||||
|
||||
cmd early = app.early
|
||||
|
||||
cmd closed = app.closed
|
||||
|
||||
cmd broken = app.broken
|
||||
|
||||
cmd probe = app.probe
|
||||
@@ -0,0 +1,50 @@
|
||||
type View { name: Text }
|
||||
|
||||
type ReadResult {
|
||||
Ready(value: Text)
|
||||
error Failed(error: dever.database.Error)
|
||||
}
|
||||
|
||||
name(row: null) (value: Text) { value = "missing" }
|
||||
name(row: View) (value: Text) { value = row.name }
|
||||
|
||||
lookup(value: Text) (answer: Text) { answer = name(model.lookup(value)) }
|
||||
|
||||
names(limit: Int) (rows: List<View>) { rows = model.names(limit) }
|
||||
|
||||
add(row: model.Account, state: Int) (next: Int) pure { next = state + 1 }
|
||||
|
||||
count() (total: Int) { rows = model.stream({ order = [id.asc] })
|
||||
total = reduce(add, rows, 0)
|
||||
}
|
||||
|
||||
one(row: model.Account, state: Int) (next: Int, stop: Bool) pure {
|
||||
next = state + 1
|
||||
stop = true
|
||||
}
|
||||
|
||||
early() (total: Int) {
|
||||
rows = model.stream()
|
||||
found = reduceUntil(one, rows, 0)
|
||||
total = model.count() + found
|
||||
}
|
||||
|
||||
closed() (total: Int) {
|
||||
rows = model.stream()
|
||||
close(rows)
|
||||
total = model.count()
|
||||
}
|
||||
|
||||
broken() (value: Text) { row = model.broken()
|
||||
value = row.name
|
||||
}
|
||||
|
||||
probe() (name: Text) {
|
||||
name = inspect(result(broken()))
|
||||
}
|
||||
|
||||
inspect(value: ReadResult.Ready(text)) (name: Text) recover("inspect expected database test errors") { name = "unexpected success" }
|
||||
inspect(value: ReadResult.Failed(error)) (name: Text) recover("inspect expected database test errors") { name = category(error) }
|
||||
|
||||
category(error: dever.database.Error.InvalidData(message)) (name: Text) recover("inspect expected database test errors") { name = "InvalidData" }
|
||||
category(error: other) (name: Text) recover("inspect expected database test errors") { name = "unexpected error" }
|
||||
@@ -0,0 +1,31 @@
|
||||
# 读取验收入口
|
||||
|
||||
提供独立只读命令。
|
||||
|
||||
### lookup
|
||||
|
||||
精确 SQL 查询。
|
||||
|
||||
### names
|
||||
|
||||
有界 SQL 查询。
|
||||
|
||||
### count
|
||||
|
||||
完整流式查询。
|
||||
|
||||
### early
|
||||
|
||||
提前停止流。
|
||||
|
||||
### closed
|
||||
|
||||
关闭未消费的流。
|
||||
|
||||
### broken
|
||||
|
||||
观察真实解码错误。
|
||||
|
||||
### probe
|
||||
|
||||
核对源语言错误类别。
|
||||
@@ -0,0 +1,107 @@
|
||||
# 读取验收业务
|
||||
|
||||
把 SQL 和流结果转换为业务输出。
|
||||
|
||||
## demo.account.View
|
||||
|
||||
仅公开需要的投影。
|
||||
|
||||
字段:`name`:姓名
|
||||
|
||||
## demo.account.ReadResult
|
||||
|
||||
保留原始数据库错误类型。
|
||||
|
||||
分支:
|
||||
- `Ready(value: Text)`:读取成功
|
||||
- `error Failed(error: dever.database.Error)`:读取失败
|
||||
|
||||
## demo.account.name
|
||||
|
||||
处理 SQL 的可空结果。
|
||||
|
||||
输入:`row`:查询结果
|
||||
输出:`value`:姓名或空值标记
|
||||
|
||||
## demo.account.lookup
|
||||
|
||||
读取指定姓名。
|
||||
|
||||
输入:`value`:姓名
|
||||
输出:`answer`:结果
|
||||
|
||||
## demo.account.names
|
||||
|
||||
读取有界列表。
|
||||
|
||||
输入:`limit`:行数
|
||||
输出:`rows`:投影
|
||||
|
||||
## demo.account.add
|
||||
|
||||
逐行归约。
|
||||
|
||||
输入:
|
||||
- `row`:行
|
||||
- `state`:累计值
|
||||
输出:`next`:累计值
|
||||
|
||||
## demo.account.count
|
||||
|
||||
流式读取不受分页上限截断。
|
||||
|
||||
输入:无
|
||||
输出:`total`:总行数
|
||||
|
||||
## demo.account.one
|
||||
|
||||
第一行后停止。
|
||||
|
||||
输入:
|
||||
- `row`:行
|
||||
- `state`:累计值
|
||||
输出:
|
||||
- `next`:累计值
|
||||
- `stop`:停止标记
|
||||
|
||||
## demo.account.early
|
||||
|
||||
提前关闭后继续使用同一连接池。
|
||||
|
||||
输入:无
|
||||
输出:`total`:可继续读取的行数
|
||||
|
||||
## demo.account.closed
|
||||
|
||||
未拉取就关闭所有别名。
|
||||
|
||||
输入:无
|
||||
输出:`total`:后续行数
|
||||
|
||||
## demo.account.broken
|
||||
|
||||
保留 SQL 解码错误。
|
||||
|
||||
输入:无
|
||||
输出:`value`:不可达结果
|
||||
|
||||
## demo.account.probe
|
||||
|
||||
确认真实解码错误保持源语言类型。
|
||||
|
||||
输入:无
|
||||
输出:`name`:错误类别
|
||||
|
||||
## demo.account.inspect
|
||||
|
||||
验收时把捕获错误转成可观察类别。
|
||||
|
||||
输入:`value`:捕获结果
|
||||
输出:`name`:类别
|
||||
|
||||
## demo.account.category
|
||||
|
||||
分类预期的错误,不丢弃生产错误。
|
||||
|
||||
输入:`error`:原始错误
|
||||
输出:`name`:类别
|
||||
@@ -0,0 +1,31 @@
|
||||
# 读取验收存储
|
||||
|
||||
验证原生 SQL 和单连接流式查询。
|
||||
|
||||
## demo.account.model.Account
|
||||
|
||||
保存有序姓名。
|
||||
|
||||
字段:`name`:姓名
|
||||
|
||||
## demo.account.model.Summary
|
||||
|
||||
原生 SQL 的具体投影。
|
||||
|
||||
字段:`name`:姓名
|
||||
|
||||
### seed
|
||||
|
||||
准备三条独立记录。
|
||||
|
||||
### lookup
|
||||
|
||||
使用参数绑定精确匹配姓名。
|
||||
|
||||
### names
|
||||
|
||||
返回有界具体记录列表。
|
||||
|
||||
### broken
|
||||
|
||||
验证行类型错误的正常传播。
|
||||
@@ -0,0 +1,24 @@
|
||||
type Account { name: Text unique }
|
||||
|
||||
type Summary { name: Text }
|
||||
|
||||
seed {
|
||||
{ name = "alpha" }
|
||||
{ name = "beta" }
|
||||
{ name = "雪' OR 1=1 --" }
|
||||
}
|
||||
|
||||
sql lookup(name: Text) (row: app.View?) {
|
||||
sqlite = "SELECT name FROM demo_account_model WHERE name = ?1"
|
||||
postgres = "SELECT name FROM demo_account_model WHERE name = $1"
|
||||
}
|
||||
|
||||
sql names(limit: Int) (rows: List<app.View>) {
|
||||
sqlite = "SELECT name FROM demo_account_model ORDER BY id LIMIT ?1"
|
||||
postgres = "SELECT name FROM demo_account_model ORDER BY id LIMIT $1"
|
||||
}
|
||||
|
||||
sql broken() (row: Summary) {
|
||||
sqlite = "SELECT 3 AS name"
|
||||
postgres = "SELECT 3::BIGINT AS name"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
# people
|
||||
|
||||
本领域的职责与行为见主题说明;函数签名和调用关系从源码读取。
|
||||
|
||||
## 说明
|
||||
|
||||
- [关联验收](doc/api.md)
|
||||
- [关联验收](doc/app.md)
|
||||
- [关联验收](doc/model-organization.md)
|
||||
- [关联验收](doc/model-post.md)
|
||||
- [关联验收](doc/model-user.md)
|
||||
|
||||
[调用流程](link.md)由 `dever inspect . --links` 生成。
|
||||
@@ -0,0 +1,7 @@
|
||||
cmd users = app.users
|
||||
|
||||
cmd plain = app.plain
|
||||
|
||||
cmd first = app.first
|
||||
|
||||
cmd cursor = app.cursor
|
||||
@@ -0,0 +1,33 @@
|
||||
type Window {
|
||||
names: List<Text>
|
||||
next: Text
|
||||
has_more: Bool
|
||||
}
|
||||
|
||||
title(value: null) (name: Text) { name = "none" }
|
||||
title(value: model.organization.Organization) (name: Text) { name = value.name }
|
||||
|
||||
organization(value: Related.Unloaded<model.organization.Organization?>()) (name: Text) { name = "unloaded" }
|
||||
organization(value: Related.Loaded<model.organization.Organization?>(organization)) (name: Text) { name = title(organization) }
|
||||
|
||||
posts(value: Related.Unloaded<List<model.post.Post>>()) (count: Int) { count = -1 }
|
||||
posts(value: Related.Loaded<List<model.post.Post>>(rows)) (count: Int) { count = length(rows) }
|
||||
|
||||
show(value: model.user.User) (name: Text) { name = value.name + ":" + organization(value.organization) + ":" + int.toText(posts(value.posts)) }
|
||||
|
||||
users() (names: List<Text>) { page = model.user.list({ order = [id.asc], size = 2, with = [organization, manager, posts] })
|
||||
names = each(show, page.items)
|
||||
}
|
||||
|
||||
plain() (names: List<Text>) { page = model.user.list({ order = [id.asc], size = 2 })
|
||||
names = each(show, page.items)
|
||||
}
|
||||
|
||||
first() (name: Text) { name = optional(model.user.first({ order = [id.asc], with = [organization, posts] })) }
|
||||
|
||||
cursor() (result: Window) { page = model.user.cursor({ order = [id.asc], size = 1, with = [organization, manager, posts] })
|
||||
result = Window { names = each(show, page.items), next = optional(page.next), has_more = page.has_more }
|
||||
}
|
||||
|
||||
optional(value: null) (name: Text) { name = "missing" }
|
||||
optional(value: model.user.User) (name: Text) { name = show(value) }
|
||||
@@ -0,0 +1,19 @@
|
||||
# 关联验收
|
||||
|
||||
验证静态关系加载与两态值。
|
||||
|
||||
### users
|
||||
|
||||
执行关联验收。
|
||||
|
||||
### plain
|
||||
|
||||
执行关联验收。
|
||||
|
||||
### first
|
||||
|
||||
执行关联验收。
|
||||
|
||||
### cursor
|
||||
|
||||
执行关联验收。
|
||||
@@ -0,0 +1,75 @@
|
||||
# 关联验收
|
||||
|
||||
验证静态关系加载与两态值。
|
||||
|
||||
## demo.people.Window
|
||||
|
||||
显式投影游标,不导出关系状态。
|
||||
|
||||
字段:
|
||||
- `names`:当前页
|
||||
- `next`:下一页锚点
|
||||
- `has_more`:是否还有后续页
|
||||
|
||||
## demo.people.title
|
||||
|
||||
执行关联验收。
|
||||
|
||||
输入:`value`:组织
|
||||
输出:`name`:名称
|
||||
|
||||
## demo.people.organization
|
||||
|
||||
执行关联验收。
|
||||
|
||||
输入:`value`:加载状态
|
||||
输出:`name`:名称
|
||||
|
||||
## demo.people.posts
|
||||
|
||||
执行关联验收。
|
||||
|
||||
输入:`value`:加载状态
|
||||
输出:`count`:数量
|
||||
|
||||
## demo.people.show
|
||||
|
||||
执行关联验收。
|
||||
|
||||
输入:`value`:用户
|
||||
输出:`name`:展示
|
||||
|
||||
## demo.people.users
|
||||
|
||||
执行关联验收。
|
||||
|
||||
输入:无
|
||||
输出:`names`:结果
|
||||
|
||||
## demo.people.plain
|
||||
|
||||
执行关联验收。
|
||||
|
||||
输入:无
|
||||
输出:`names`:结果
|
||||
|
||||
## demo.people.first
|
||||
|
||||
执行关联验收。
|
||||
|
||||
输入:无
|
||||
输出:`name`:结果
|
||||
|
||||
## demo.people.cursor
|
||||
|
||||
执行关联验收。
|
||||
|
||||
输入:无
|
||||
输出:`result`:结果
|
||||
|
||||
## demo.people.optional
|
||||
|
||||
将可空 Model 投影到文本。
|
||||
|
||||
输入:`value`:结果
|
||||
输出:`name`:摘要
|
||||
Loaded 100 of 1479 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user