Initial Dever 0.0.1

This commit is contained in:
Codex committed 2026-11-03 03:32:07 +00:00
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`:输入或输出的业务值
+52
View File
@@ -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?
}
+31
View File
@@ -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
}
+196
View File
@@ -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]))
}
+22
View File
@@ -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") }
+56
View File
@@ -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) }
+204
View File
@@ -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
}
+213
View File
@@ -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)
}
+74
View File
@@ -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
}
+33
View File
@@ -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