commit 81233d098cef21d3a3f63445c854c9941551f140 Author: Codex Date: Tue Nov 3 03:32:07 2026 +0000 Initial Dever 0.0.1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e39fd13 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/target/ +/rust/target/ +core.[0-9]* +/build/documentation-*/ +/config/setting.json +/.release-key.*/ +/.agents/ +/.codex/ +**/__pycache__/ +**/*.pyc diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b703a18 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,36 @@ +# Dever 核心开发 + +适用于整个仓库。这里维护编译器、运行库和标准库;Dever 应用开发使用 [随包 Skill](skills/SKILL.md)。 + +## 工作规则 + +- 首条回复和最终回复称呼用户为“主人”。先读适用规则、相关源码和测试,再修改;已读且未变的文件不重复读取。 +- 落实用户已授权的工作,不用方案或记录代替实施。讨论方案时只读;安装、部署、重启、提交、推送和重写 Git 历史须在授权范围内。 +- 在 `/dm` 工作时遵守根 AGENTS。构建和程序运行使用已有开发容器,不往宿主机安装环境;保留用户改动、数据和凭据。 +- 开发阶段统一使用 `0.0.1`,没有生产数据,不做旧语法、接口、配置或数据的兼容层。公开行为改动须说明,并同步调用方、测试和文档。 + +## 实现规则 + +- 保留 Dever 语法与约束。固定路线:Dever → 具体类型 Rust → 官方 Cargo/rustc → 原生程序。不开新后端,不 fork rustc。 +- 优先复用 Rust 标准库和已有成熟库。只实现 Dever 自身规则与必要适配,不重复造解析器、调度器、网络栈或数值算法。 +- 不预建扩展点、占位层或没有调用方的抽象。函数用于消除真实重复或表达清楚的职责,不包装每次库调用。 +- 不添加无依据的判空、类型猜测、备用实现或静默兜底。外部输入在所属入口验证,内部不重复检查已保证的条件;鉴权、数据隔离、协议和资源释放必须正确。 +- 错误沿现有路径传播,保留原因,不用空值或默认成功掩盖失败。重试、锁、缓存和补偿只用于当前真实需求。 +- 每次复制、扫描、分配、加锁或调度交接都要有用途。只读优先借用,能移动就移动,不用 clone 代替所有权分析。 +- 普通调用生成直接函数调用,只有需要挂起的函数才生成异步调用。具体类型直接传递,集合直接遍历,不套动态值或中间容器。 +- 环境地址、端口、路径和凭据走既有配置;身份及数据归属来自可信上下文。不按账号、租户、测试名称或固定答案写特例。协议常量和局部算法常量可留在代码中。 +- Rust 代码写必要的中文注释,说明原因和语义差异。Dever 源码为 `.dever` 纯代码;函数用 lowerCamelCase,字段、局部值和目录用 snake_case。 +- 沿用目录结构,只保留工具包和运行库两份 Cargo 清单。每目录最多八个手写 `.rs` 文件,通常每文件 100–300 行,接近 400 行审查,超过 500 行拆分;小模块可直接放在 mod.rs。 + +## 验证与交付 + +- 先比较生成代码、运行库调用和独立 Rust 实现,找到多余操作再改。保留已有优化,不为某个性能样例添加特殊路径。 +- 性能对照使用相同语义、输入、精度、依赖和编译选项。共享 Dever 运行库的对照只检验生成层,不能证明运行库追平。 +- 提前固定轮次,保留全部原始结果及源码、二进制对应关系。报告绝对耗时、修改前后变化和对 Rust 的差距,不挑选样本或隐瞒退化。 +- 每个独立改动先做相关功能和性能验证,再继续下一项;每批结束检查相关矩阵。稳定慢 2% 及以上继续定位,约 1% 不为凑数字增加代码。 +- 新增复杂度没有稳定收益就撤回。删除等价冗余且无退化可以保留,但不冒充提速。 +- 复用现有检查和测试,不造自证逻辑、不削弱断言。已通过的检查仅在相关内容变化或证据不足时补验。 +- 说明只写当前用法和必要规则,不写对话、开发日志或占位描述。修改代码同步相关说明;调用图由工具生成,见 [说明写法](DOCUMENTATION.md)。 +- 交付前检查差异、错误处理、冗余、引用和临时文件。未执行、失败或未测项目如实说明,不以构建成功代替业务通过。 + +源码布局、构建和测试命令见 [rust/README.md](rust/README.md)。语言行为见 [LANGUAGE.md](LANGUAGE.md)。 diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md new file mode 100644 index 0000000..f09df5b --- /dev/null +++ b/DOCUMENTATION.md @@ -0,0 +1,64 @@ +# 源码与说明 + +`.dever` 放代码,每个领域使用以下阅读入口: + +```text +module/manage/account/ +├── README.md +├── doc/ +│ ├── session.md +│ └── members.md +├── app/ +│ ├── session.dever +│ └── members.dever +├── domain.dever +├── model.dever +└── link.md +``` + +README 用一段话介绍职责,并链接主题说明。doc 按功能组织;业务角色只创建实际需要的文件,小领域可以使用 `app.dever`。 + +## 函数说明 + +二级标题写完整符号名,正文说明代码不容易表达的业务含义: + +```markdown +# 登录 + +## manage.account.login + +校验登录名和密码,成功后创建会话并设置 Cookie。凭据错误返回未授权。 +``` + +- 简单函数通常一两句即可;复杂功能再补规则、失败条件和副作用。 +- 参数含义不清楚时解释参数,不重复列出已展示的类型和签名。 +- 不写“操作参数”“业务值”等占位文字,不把同一段介绍复制给每个函数。 +- 一个逻辑函数只写一处说明,多分句共用。类型也可用完整符号名绑定说明。 +- 调用方、行号和调用关系由工具读取,不手工维护。 + +不确定符号名时执行 `dever inspect . --json`。说明标题失效或重复时,检查器会报错;文字是否符合实现仍须对照源码。 + +## 调用流程 + +```sh +dever inspect . --links +dever inspect . --links --check +``` + +第一条命令生成各领域的 `link.md`,第二条检查是否过期。流程按源码求值顺序展示入口、分支、调用和任务调度,点击节点可打开源码。 + +图展示代码可能执行的路径,不是一次请求的运行记录。Port 的实际 Adapter 由配置选择。完整调用关系保留在 link 中,README 和 doc 不再复制。 + +## 修改与阅读 + +先读领域 README、相关主题和调用流程,再读源码。修改实现时同步受影响的说明与测试,重新生成调用图。 + +```sh +dever fmt . --check +dever check . +dever test . --filter manage.account +dever inspect . --links +dever inspect . --links --check +``` + +VS Code 插件可在悬浮中显示签名、说明和调用摘要,并跳转源码及上下游调用,见 [插件说明](editor/vscode/README.md)。源码和说明保存后刷新索引,不需要在 Dever 源码里加说明注释。 diff --git a/LANGUAGE.md b/LANGUAGE.md new file mode 100644 index 0000000..8b93ba7 --- /dev/null +++ b/LANGUAGE.md @@ -0,0 +1,448 @@ +# Dever 语言规范 + +Dever 使用静态类型、命名输出和互斥分句。应用由 API、CMD、Job 声明入口,编译为原生程序。 + +- 开始项目:[开发约定](skills/references/development.md) +- 源码格式:[源码与说明](DOCUMENTATION.md) +- AI 开发:[随包 Skill](skills/SKILL.md) +- 安装:[安装指引](skills/references/install.md) + +本文代码块是语法片段,放入对应角色的 `.dever` 文件。完整项目见 [示例](skills/references/examples.md)。 + +## 项目与命名 + +```text +config/setting.json +module/<组件>/<领域>/<角色>.dever +test/<组件>/<领域>/<主题>.dever +data/ +``` + +配置来自 `config/setting.json`。run 读取项目配置,独立程序读取可执行文件旁的 `config/setting.json`。 + +| 名称 | 格式 | +| --- | --- | +| 组件、领域、主题、字段、局部值 | ASCII lower_snake_case | +| 函数、SQL 方法、标准库方法 | ASCII lowerCamelCase | +| 类型、choice 分支 | UpperCamelCase | + +源码使用 UTF-8 和 `.dever` 后缀,不写分号。说明放领域 README 和 doc,Markdown 不参与编译。词法支持 `#` 行注释,项目源码不写说明注释;`//` 表示整数除法,不支持块注释。 + +| 角色 | 职责与可见性 | +| --- | --- | +| app | 公开领域能力;可调用本领域 Domain、Model、Port 和其他领域 App | +| domain | 本领域业务规则;不能调用其他领域或执行外部 I/O | +| model | 字段、查询和迁移,只由本领域 App 操作 | +| api | 绑定本领域 App,或声明自动 REST | +| port | 外部操作的输入、输出和错误 | +| adapter | 实现 Port,可调用本文件函数和标准能力 | +| job | 绑定本领域 App 的持久任务 | + +应用不声明 package、import、exposes、internal 或 main。App 函数按 `组件.领域.函数` 调用;同组件可省略组件,同领域使用 `app.函数`。跨领域只访问 App,公开参数和输出不能暴露私有类型。领域之间不得循环依赖。 + +角色可使用 `app/publish.dever` 等主题文件,主题名不进入调用名,目录允许一个文件。App/Domain/Port/Adapter/Job 的主文件与目录二选一;Model/API 主文件可与目录共存。只有 API 目录可递归,其路径参与 URL。目录不能命名为 `common/shared/utils/helper/base/service`。 + +## 类型与值 + +| 类型 | 语义 | +| --- | --- | +| Bool | true / false | +| Int | 有符号 64 位整数,检查溢出 | +| Decimal | decimal128,最多 34 位有效数字,银行家舍入 | +| Float | IEEE 754 binary64 | +| Text / Bytes | UTF-8 文本 / 字节序列 | +| Id / Uuid | 不透明标识 / UUID | +| DateTime / Date / Time / Duration | UTC 时间点、日期、日内时间、时长;毫秒精度 | +| Json | 不透明 JSON 值,保留数字文本 | +| Secret | 仅在规定的输入、密码和摘要操作中使用的敏感值 | +| T? | T 或 null | +| List / Map | 同类型列表 / 保留插入顺序的映射 | + +整数字面量默认 Int,小数字面量默认 Decimal;明确 Float 上下文可接收小数。科学计数形式为 Float。Decimal 与 Float 不隐式混合。Text 支持反斜杠、引号、换行、回车、Tab 和 `\u{HEX}` 转义。 + +字段型 type: + +```dever +type User { + name: Text + phone: Text? +} + +rename(user: User, name: Text) (result: User) { + result = user + result.name = name +} +``` + +构造使用 `User { name = "Lin" phone = null }`,必须一次给全字段。字段型、choice、List、Map 使用值语义,修改局部副本不改变原值。 + +选择型 type: + +```dever +type State { + Draft + Published + Rejected(reason: Text) +} +``` + +构造为 `State.Published` 或 `State.Rejected("原因")`,载荷按位置传入。 + +List 写为 `[1, 2]`,Map 写为 `{"name" = "Lin"}`。空集合需由上下文确定类型。Map 键允许 Text、Int、Bool、Id 和无载荷 choice。Map 相等比较包含插入顺序;List 按位置比较。Float 的 NaN 与自身不相等。 + +## 函数与分句 + +```dever +greet(name: Text) (message: Text) { + message = "Hello, " + name +} +``` + +函数格式为 `名称(输入) (命名输出)`,空输入或输出仍写 `()`。调用按位置传参;单输出直接得到值,多输出按名称访问。输出必须在正常结束前赋值。 + +`=` 可初始化或修改当前函数的参数、局部值和输出;变量类型保持不变,读取前须初始化。字段只能在完整值上读取或修改。 + +局部值首次声明可写 `input: CreateInput = dever.json.decode(payload)`,为解码、空集合或 null 提供具体类型;类型省略时仍由表达式推断。后续赋值只写 `input = ...`,不重复声明类型,也不改变原类型。 + +同名、同参数数量的分句须相邻,输出类型和错误声明一致。分句互不重叠并覆盖全部输入,没有先后优先级: + +```dever +label(state: State.Published) (text: Text) { + text = "published" +} + +label(state: State.Rejected(reason)) (text: Text) { + text = reason +} + +label(state: other) (text: Text) { + text = "draft" +} +``` + +模式支持类型、choice 分支、null、常量、Int/Decimal 范围和 other。other 是该参数位置未被明确模式覆盖的补集;多个参数按全部组合检查。choice 载荷按位置绑定,不使用的载荷可写 `_`。 + +可空值用 `value: Text` 与 `value: null` 两个分句处理。范围可写 `score: Int >= 60 and < 90`。没有 if/else、switch/match、return、条件表达式、用户递归、lambda、运行时闭包或用户泛型。 + +## 运算与范围 + +| 运算 | 规则 | +| --- | --- | +| + - * | 同类数值;Int 可提升为 Decimal;Text 支持 + | +| / | Int/Int 和 Decimal 运算返回 Decimal;Float/Float 返回 Float | +| // % | 仅 Int,整数除法向零截断 | +| == != | 同类型且可比较的值 | +| < <= > >= | Int、Decimal、Float | +| and or not | Bool;and/or 短路 | + +优先级依次为调用/字段、一元运算、乘除取余、加减、比较、and、or。同级从左到右。 + +字段、choice 载荷、函数及 handler 参数都可声明 Int、Decimal 范围: + +```dever +type Port { + value: Int >= 1 and <= 65535 +} +``` + +构造、赋值和返回都须满足范围。未知 Int 先经范围分句确认;不支持任意用户谓词。静态可知的数值故障在检查时拒绝,运行时故障报告源码位置并终止当前执行。 + +## 集合与 handler + +使用具名函数处理集合,不提供 for/while、break/continue。 + +| 操作 | 行为 | +| --- | --- | +| each(handler, values[, context]) | 零输出时遍历;单输出时生成 List | +| filter(handler, values[, context]) | Bool handler 筛选 List | +| find(handler, values[, context]) | 返回 List 的首个匹配项或 null | +| sum(values) / sum(handler, values[, context]) | 数值 List 求和 | +| reduce(handler, values, initial) | handler 输入为元素、当前状态,返回下一状态 | +| reduceUntil(handler, values, initial) | 另输出 stop: Bool,处理当前项后停止 | +| parallelEach(handler, values, limit[, context]) | 并行执行零输出 handler,返回前收尾全部工作 | +| contains(values, target) | 判断 List 中是否存在相等项,直接返回 Bool | + +List/Bytes 支持 each、reduce、reduceUntil;Bytes 元素是 0–255 的 Int。Stream/AsyncStream 的 each 只接受零输出 handler。filter/find/sum 只处理 List。除 parallelEach 外按顺序处理,空集合求和为零,reduce 返回初始状态。 + +context 最多一个,需要多项数据时用 record;reduce 的共享数据放 State。parallelEach 的非挂起并发数为 1–256,挂起任务容量为 1–65536,并受运行时容量限制。同步 Stream 不接受挂起 handler。 + +handler 是签名匹配的具名函数或 handler 参数,不是可存储、比较或返回的运行时值: + +```dever +transformAll( + transform: handler(value: Int) (result: Text), + values: List +) (results: List) { + results = each(transform, values) +} +``` + +`append`、`put`、`remove` 返回新集合;`get`、`first`、`find` 返回可空值。entries 返回有序 MapEntry 列表。length 接受 List、Map、Text、Bytes,Text 按 Unicode 码点计数。 + +## 失败、纯函数与敏感值 + +choice 的错误分支显式标记 error: + +```dever +type DeliveryError { + error Unavailable(message: Text) +} +``` + +普通调用返回成功值,失败自动向调用方传播。`fail(DeliveryError.Unavailable("原因"))` 主动失败;`result(call)` 捕获后交给覆盖所有结果的分句处理。 + +捕获按错误类型及变体身份匹配,不按消息或载荷形状猜测。结果 Choice 的 error 分支可包装原错误 Choice,实际构造的仍是原失败变体;原错误 Choice 含有成功分支不妨碍捕获。参数求值的失败继续向外传播。 + +错误必须传播、返回或明确恢复。读取消息、记录日志、覆盖变量不算处理;恢复函数写 `recover("具体原因")`,全部分句理由一致。失败义务跟随 record、choice 和集合,局部查找不能代表处理了其余错误。程序故障不属于可捕获业务失败。 + +`pure` 写在输出声明后,禁止 I/O、时钟、随机源、并发等效果;检查沿调用链和 handler 传播。pure 函数仍可能有数值故障。 + +private 字段只能由所属源码边界访问,含私有字段的值不能直接用于公开 wire 输出。Model 私有字段仅供同领域 App 操作,跨边界返回显式 View。 + +Secret 不可比较、打印、拼接、转为普通文本、作为 Model 列或输出。密码使用 dever.crypto;哈希和验证属于阻塞操作。测试源可用 `secret(Text)` 构造固定输入,生产源码没有此能力。 + +## 并发与资源 + +挂起由调用图推断,不写 async/await。普通调用等待完成,显式 run 才启动子任务。 + +| 操作 | 合同 | +| --- | --- | +| run(call) | 启动 Task | +| wait(task) | 等待并消费 Task | +| stop(task) | 请求取消并等待退出;仅接受零输出 Task | +| timeout(task, ms, fallback) | 到期先清理任务,再调用输出签名相同的零输入 fallback | +| race(task, task, ...) | 取首个完成结果,清理其余任务;输出签名须相同 | +| group(limit) / run(group, call) | 有界零输出任务组,满时等待;最终 wait/stop | +| parallel(pure_call) | 在线程池执行非挂起纯计算 | +| blocking(call) | 在线程池执行明确的非挂起阻塞调用 | +| channel(T, capacity) | 有界通道;send/receive 挂起,close 唤醒等待者 | + +Task/Group 不能复制、嵌套、传参或逃逸,必须消费一次。异常路径也会收尾子任务;取消不撤销已经发生的外部副作用。程序故障不会变成 timeout 的正常 fallback。 + +Group、Channel 容量为 1–65536。Channel 关闭后仍可读完缓存,之后 receive 返回 null。Channel 元素不能为可空类型;需要传递空值时,用含可空字段的消息 Choice 区分消息与关闭。 + +跨线程或挂起点存活的普通值须可传递。挂起调用链中的同步文件、睡眠、密码计算等操作用 blocking;网络等待和 dever.task.sleep 自动挂起。 + +File、Socket、Listener、Stream、AsyncStream 等资源的副本共享状态。普通数据的值语义不适用于这些句柄。 + +- Stream 按需单次消费,别名共享读取位置;reduceUntil 停止后可继续读取同步 Stream。 +- AsyncStream 的消费结束、提前停止、故障或取消会关闭流;别名不能继续消费。 +- `stream(channel/list/bytes)` 创建 AsyncStream;`dever.task.ticks(ms)` 创建周期流。 +- 关闭流释放其持有的资源别名,不关闭调用者另外持有的 File、Socket 或 Channel。 + +## API、身份与权限 + +API 文件只声明绑定: + +```dever +get profile = app.profile +public post login = app.login +authenticated get current = app.current +long post generate = app.generate +cmd export = app.export +rest model +``` + +HTTP 支持 get/post/put/delete。URL 为组件、领域、API 主题路径和 action;CMD 为 `组件.领域.命令`。API 绑定本领域 App 的一个可序列化输出。 + +GET/DELETE 从 query 读取标量;POST/PUT 从 JSON 对象读取;CMD 接受单个 JSON 对象。未知、重复和缺少必填字段失败。成功包裹为 `{"code":0,"message":"ok","data":...}`,失败的 data 为 null。dever.api.Error 映射 400/401/403/404/409/429,其他内部失败返回安全的 500。 + +| 声明 | 访问要求 | +| --- | --- | +| 默认 HTTP | 可信身份与精确角色权限 | +| public HTTP | 匿名入口 | +| authenticated HTTP | 可信身份;对象所有权与业务权限由 App 检查 | + +public/authenticated 不用于 CMD 或 REST。权限键为 `component.domain.site.action`,REST 的 action 为 read/create/replace/delete。HTTP 方法不进入权限键。 + +`sites..path` 匹配 API 父目录,每条接口须恰好匹配一个站点。例如 admin 匹配 api/admin/**,不匹配 api/admin.dever,也不改写 URL。 + +认证 Provider 绑定项目的 verify App。它接收 dever.auth.Claims,返回仅含 `id: Text`、`user_id: UserModelId?`、`tenant_id: TenantModelId?` 的身份 record。verify 校验账号、会话、租户和成员关系,只读 global Model,不返回权限列表。 + +身份读取使用 dever.auth 和 dever.site,ID 类型须匹配 verify。客户端参数不能建立身份。public、CMD、Job 和普通测试不能直接读取 HTTP 身份。 + +角色与权限由核心管理。授权读取允许受保护或 authenticated 入口;修改只允许普通受保护写入口。Owner 为受保护角色,不按特定用户 ID 判断。业务 capability 在可信租户和入口命名空间内注册,撤销同时清理 grants,重新注册不恢复授权。 + +HTTP 元数据用 dever.api 读取和设置。敏感头不能读作普通 Text;Cookie 登录/退出用 issueCookie/clearCookie。Cookie 写入及 Cookie 认证的写请求要求 Origin、Host 精确匹配站点 origin。响应元数据仅在处理与编码成功后发布。 + +Upload 仅用于 POST 的顶层 multipart 参数,不能复制、嵌套、输出或进入 CMD。显式关闭,或一次性传给本领域 Port,由 Adapter 使用 storage;失败和取消清理临时文件。 + +完整配置与调用边界见 [应用指引](skills/references/application.md)和 [签名索引](skills/references/signatures.md)。 + +## Model 与查询 + +主文件的记录名对应领域,主题文件对应主题名: + +```dever +type Article { + title: Text(1, 200) + body: Text + published: Bool default false index +} +``` + +自动生成只读 id 和 created_at。每个 Model 的 id 是不同的名义类型,不能用 Int 或其他 Model 的 ID 替代。 + +字段支持存储标量、无载荷 choice、Model ID 及可空形式。Text/Bytes 可指定长度,Decimal(P,S) 指定精度;generated 用于非空 Uuid。default 为静态值,index/unique 可用于单字段或顶层复合索引。外键自动建立索引,不自动加载关联。结构化数据显式使用 Json。 + +Model 的 `Text` 字段写入时默认去除首尾 Unicode 空白,再检查长度;不处理内部空白。`raw` 保留原文,`lower`/`upper` 在去除首尾空白后转换大小写,三者互斥。可空字段的 null 保持不变,空串不转为 null。普通函数中的 Text、查询条件、读取结果和原生 SQL 不自动转换。 + +```dever +type Category { + name: Text(1, 80) + code: Text(1, 40) lower unique + description: Text? + content: Text raw + active: Bool default true +} +``` + +create、update、条件更新、createMany、upsert 和 REST 使用同一写入规则。default、Seed 在编译期按该规则处理。富文本、代码、签名原文等需要逐字保留的字段使用 raw;不会依据字段名称猜测策略。去除空白不负责 HTML 安全过滤。 + +同领域 App 使用 model.create/createMany/get/first/list/cursor/count/exists/stream/update/delete/upsert。多 Model 使用 model.topic.operation。查询块只出现在调用位置,不是可保存的动态 Map: + +```dever +page = model.list({ + where = published == true + order = [created_at.desc, id.desc] + page = 1 + size = 20 +}) +``` + +where 支持布尔组合、比较、null、in、between、contains、startsWith、endsWith;结构静态确定,值参数绑定。list 返回 items/page/size/total/pages;cursor 返回 items/next/has_more,不做 count。max_page_size 限制结果规模。 + +update(id, values) 返回记录,不存在时失败;条件 update/delete 返回影响行数,必须明确 where。delete(id) 返回影响行数。关系用 with 显式加载,返回 Related 的 Unloaded/Loaded 状态;仅 first/list/cursor 支持一层关系。 + +### REST 字段 + +create/replace/search 表达式写在 Model 字段上:引用 input 时转换客户端值,否则由服务端提供并拒绝客户端同名输入。表达式只允许纯计算及指定的只读请求能力。search 最多一个,不能叫 page/size。 + +`private owner user_id: ... = dever.auth.userId()` 创建时绑定可信用户,全部读写 SQL 均包含 owner 条件。private 字段不出现在 REST 输出;没有 replace 来源时 PUT 保留其原值。 + +普通 CRUD 直接使用 rest model;关联编排、状态转换或不同输出合同由 App 实现。 + +### 事务 + +```dever +transaction publish(id: model.id) (article: model.Article) { + article = model.update(id, { published = true }) +} +``` + +普通写 HTTP、CMD、Job 建立整体事务,成功编码后提交。嵌套 transaction 复用同一连接的事务;失败或取消回滚后传播原错误。GET 不写库或入队。 + +long 入口不建立整体事务,写库和入队必须进入显式 transaction。外部 I/O、Worker、等待、并发和密码计算放在事务外,写入依赖的状态在事务内重新确认。long 已提交阶段不会因后续失败撤销。 + +事务不能跨物理数据库或混用 global/tenant 存储,不暴露 begin/commit/rollback。数据库失败保留 dever.database.Error。 + +### 数据库、迁移与租户 + +Model 优先使用组件同名连接,否则 database.default;显式 `database report` 绑定固定连接,缺失时报错。SQLite/PostgreSQL 配置在 setting.json;PostgreSQL 明确选择 TLS system/disabled。 + +当前 Model 是目标 schema。空库建最终表;安全变更自动应用,收紧约束先检查数据。字段重命名用 `from old_name`;破坏性变化用命名 migrate。已执行迁移的内容与顺序不可修改。 + +migrate 可含 before/after SQL,每块提供 sqlite、postgres 和参数列表,只允许单条参数化 INSERT/UPDATE/DELETE。全新数据库不执行历史数据变换。seed 按唯一键补缺失记录,不覆盖既有数据;迁移原子性限于所属 Model。 + +复杂查询在 Model 声明 `sql name(inputs) (output)`,两种方言均为静态字符串;结果是具体 Model、私有 SQL record 或本领域 App View。只有可证明只读的 SQL 能用于 GET。 + +启用 tenant 后,global type 使用控制库,其余 Model/Job 使用租户库。只支持 database 隔离。租户库须先显式迁移;缺失、未就绪或 schema 过期时报错,不回退控制库。 + +运维入口为 `dever tenant migrate`、`dever tenant owner`、`dever tenant component enable|disable`。数据库权限和真实数据操作由部署侧管理。 + +## Job + +```dever +database default + +job store(input: app.Request) () retry(3) timeout(30000) { + app.store(input) +} +``` + +Job 输入为零个或一个本领域 App record,无输出,只绑定本领域 App。retry 为 1–100 次,包含首次执行;timeout 为 1–3600000 毫秒。定时声明为 `schedule name = "0 * * * *"`,使用 UTC。 + +App 使用 dever.job.enqueue/enqueueAt,传入任务、可选 payload 和幂等 key。key 为 1–1024 字节;同租户、任务身份和 key 的活跃任务复用 Id。入队与业务写入可共享事务。 + +任务至少执行一次,外部副作用须业务幂等。受保护 HTTP 入队保存可信身份与精确入口权限键,执行时重验;public/authenticated 不能入队角色权限绑定的任务。CMD、schedule、测试使用 System 身份。身份失效、坏 payload 或不兼容 schema 进入 blocked。 + +runtime.mode 为 api/worker/all。任务配置包括 workers、poll_ms、lease_ms 和重试间隔;SQLite 的 lease 必须长于 Job timeout,worker 连接池需留出监管连接。停止时先停止接收和 claim,再按 shutdown_ms 排空。 + +测试使用临时数据库和用例独立时钟,可用 dever.test.advanceClock、drainJobs 验证队列。 + +## Port、Adapter 与依赖 + +Port 声明输入、命名输出和明确失败集合: + +```dever +send(message: Text) () fails app.DeliveryError +``` + +Adapter 用 `port.send(...) (...) { ... }` 实现。每个文件完整实现一个 Port;单实现自动绑定,多实现由 setting.json 的 adapter 配置选择。实现只能抛出 Port 声明的错误。Port 不接受 handler 或数值范围参数。 + +Adapter 可声明 setting,选中的实现从配置读取同类型字段。外部程序用 external command/exec/pip/npm/go;代码可信性、网络、文件和设备权限由运行环境负责,allow 声明不是 OS 沙箱。 + +| 方式 | 用途 | +| --- | --- | +| external command | 普通 ELF;argv/stdin 输入,code/stdout/stderr 输出,无 shell | +| external exec | 遵循 Port Worker 协议的程序 | +| external pip/npm/go | 由相应生态实现 Port,使用锁定资源 | + +外部调用和 fake 用法见 [依赖](skills/references/libraries.md)、[外部库示例](skills/references/external-lib.md)。协议与运行资源见 [SDK](sdk/README.md)。 + +Dever Package 是 module 下的可复用组件,使用 `dever package add/update/remove/list/doctor/export` 管理。dever.lock 固定版本、来源和摘要,不能覆盖本地同名组件。已有锁通过 `dever lib install` 恢复资源;run/build 消费锁定资产。当前 CLI 不提供 Lib add/update 或 target add。 + +## 标准库与数据格式 + +标准库按包名直接调用,无需 import。功能目录见 [标准库](lib/README.md),参数和输出见 [签名索引](skills/references/signatures.md)。 + +### JSON 数据 + +API/CMD 根据参数和输出类型编解码。Decimal/Id/Uuid 使用字符串,ModelId 为整数,日期时间使用规范字符串,Duration 为整数毫秒。支持 List、可空、可公开传输的 record 和无载荷、无 error 分支的 Choice。此类 Choice 按声明的分支名编码为 JSON 字符串,也可作 query 或 multipart 标量;未知分支报错。带载荷或 error 分支的 Choice、Map、资源不用于普通 wire。缺少可空字段得到 null,未知、重复和缺少必填字段失败。 + +已有 Json 需要转换成具体类型时,使用 `dever.json.decode(value)`;目标类型由局部声明、命名输出、字段或参数类型确定。直接从已有原文解码,不转为 Text 或 Document。解码失败传播 `dever.json.JsonResult.Failed`。Text 不在此处清洗。 + +需要处理解码失败时,直接写 `decoded: DecodeResult = result(dever.json.decode(payload))`。DecodeResult 必须有一个含单个目标类型字段的成功分支,以及包装 dever.json.JsonResult 的 error 分支,例如 `Parsed(value: CreateInput)` 和 `error Invalid(cause: dever.json.JsonResult)`。不需要另写解码函数;无类型上下文时不会猜测目标类型。 + +单个布尔条件失败使用 `dever.api.require(condition, dever.api.Error.Invalid)`;false 传播指定错误,true 继续。该函数不代替业务状态分支和执行时的权限校验。 + +日期时间使用 RFC3339,输出 UTC,最多毫秒精度;Date 为 YYYY-MM-DD,Time 为 HH:MM:SS[.sss]。无效日期、非有限 Float、越界整数失败。 + +Json 提供 fromText/toText、object/array、propertyValue/setProperty 等操作;encode 按实参具体类型输出 Json。原始数字不经过 Float。Json 限制为 16 MiB、64 层、65536 个值。 + +需要遍历节点时使用 parse/stringify 和 Document:root 为根 ID,nodes 为连续节点列表,子 ID 小于父 ID。Number 保留原文,Object 保留键顺序。node/property/element 缺失返回 null;JSON null 是实际节点。Document 的解析深度上限为 256,stringify 检查手工节点引用。 + +### 文件、HTTP 与流 + +文件 create 只建新文件,read 可能短读。I/O 错误通过声明失败传播;流的终止错误最多产生一次,之后结束。TCP 支持同时读写,每个方向串行;部分写入后失败关闭连接,不重发整条消息。 + +HTTP 支持 HTTP/1、HTTP/2、TLS、连接池、流式响应、SSE 与 HTTP/1 WebSocket。普通应用使用 API 声明,底层服务和客户端放 Adapter。 + +- Limits 控制头、正文、期限和连接数;HTTP/2 另限制流数与接收窗口。 +- serve/send 缓冲完整正文;serveLive/open/openStream 按块处理。 +- 池和会话归创建它们的任务作用域,响应须消费或关闭后释放名额。 +- TLS 校验证书和名称,不自动降级;HTTP/2 使用 ALPN 或明文 prior knowledge。 +- HTTP 协议头和正文定界由引擎管理;应用不写 Content-Length/Transfer-Encoding/Upgrade。 +- 关闭 Listener 停止接收并排空;stop 取消服务并等待清理。 +- 不支持 CONNECT、非空 trailer、服务器推送、WebSocket-over-HTTP/2、WebSocket 子协议或压缩扩展。 + +## 检查、测试与构建 + +```sh +dever new my_app +dever fmt my_app --check +dever check my_app +dever test my_app --filter hello.greeting +dever run my_app -- hello.greeting.greet '{"name":"Dever"}' +dever build my_app --output my_app/program +``` + +check 检查源码、配置、类型规则和生产入口可达性,不启动应用。重复实现和业务合理性仍需代码审查。 + +测试入口把文件主题转成 lowerCamelCase,例如 account_login.dever 对应 accountLogin() (),可调用本领域 App/Domain。外部 Port 使用用例独立 fake;Model 使用临时 SQLite。筛选不跳过源码检查,每例独立进程,默认 60 秒;任一失败返回非零。 + +`dever api --output <新文件>` 导出接口快照。已有 dever.api 时,check/run/build 检查基线;变更需审查新快照,工具不自动覆盖。 + +构建输出须是新文件。生成的程序可脱离开发包运行,仍需配套配置和系统运行库。 + +安装环境与交叉编译见 [安装说明](build/README.md)和 [编译器开发](rust/README.md)。 diff --git a/README.md b/README.md new file mode 100644 index 0000000..2f734db --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# Dever + +Dever 用于开发 HTTP 服务和命令行程序。源码使用 `.dever`,编译成原生二进制,运行时不需要 Dever 或 Rust 开发工具。 + +当前版本:`0.0.1`。 + +## 安装 + +```sh +curl -fsSL https://git.dever.cc/dever/core/releases/download/v0.0.1/install | bash +``` + +支持 Linux x86_64、glibc 2.41 及以上,已在 Debian 13 验证。安装器会后台准备编译环境;准备期间可以创建项目、编辑和检查代码,首次测试、运行或构建会等待准备完成。 + +CI 安装可加 `--wait`。离线包和 VS Code 插件见 [发布页](https://git.dever.cc/dever/core/releases/tag/v0.0.1),详细步骤见 [安装说明](build/README.md)。 + +## 开始使用 + +```sh +dever new hello +dever check hello +dever test hello +dever run hello -- hello.greeting.greet '{"name":"Dever"}' +dever build hello --output ./hello-app +./hello-app hello.greeting.greet '{"name":"World"}' +``` + +`dever new` 生成源码、测试和说明。开发服务、数据库和后台任务可参考 [完整示例](examples/README.md)。 + +## 主要功能 + +- 静态类型、命名输出、函数分句和有序集合。 +- HTTP/1、HTTP/2、TLS、SSE、WebSocket 和文件上传。 +- Model、REST、SQLite/PostgreSQL、事务和租户数据库。 +- 认证、角色权限、持久化 Job 和并发任务。 +- Python、JavaScript、Go 外部组件。 +- VS Code 悬浮说明、源码跳转和自动调用图。 + +## 文档 + +| 内容 | 入口 | +| --- | --- | +| 语法与语言规则 | [语言规范](LANGUAGE.md) | +| 项目结构与开发方法 | [开发指引](skills/references/development.md) | +| 源码说明与调用流程 | [说明写法](DOCUMENTATION.md) | +| 标准库 | [功能目录](lib/README.md) | +| 示例项目 | [CMS](examples/README.md)、[入门示例](skills/references/examples.md) | +| VS Code | [插件说明](editor/vscode/README.md) | +| AI 开发 | [Dever Skill](skills/SKILL.md) | +| 编译器开发 | [Rust 源码与测试](rust/README.md) | +| 性能测试 | [测试方法](test/performance/README.md) | + +## 发布环境 + +安装包附 SHA-256,当前未签名。ARM64 和 ESP32 尚未完成设备验证。 + +生产容器只需放入业务二进制、配置和所需资源,不需要开发包。首次构建会编译依赖,后续复用缓存。 diff --git a/build/README.md b/build/README.md new file mode 100644 index 0000000..c8d36a3 --- /dev/null +++ b/build/README.md @@ -0,0 +1,64 @@ +# 安装 Dever + +支持 Linux x86_64、glibc 2.41 及以上,已在 Debian 13 验证。应用开发只需安装 Dever,编译环境由安装包管理。 + +## 在线安装 + +```sh +curl -fsSL https://git.dever.cc/dever/core/releases/download/v0.0.1/install | bash +``` + +默认安装到 `~/.dever`。重新打开终端,或执行安装器打印的命令加载 PATH。 + +安装器先提供 Dever 命令,再后台下载编译环境。准备期间可使用 `new/fmt/check`;首次 `run/test/build` 会等待准备完成。下载中断后再次安装或构建即可继续。首次构建还会编译依赖,后续使用缓存。 + +CI 需要等待完整环境就绪: + +```sh +curl -fsSL https://git.dever.cc/dever/core/releases/download/v0.0.1/install | bash -s -- --wait +``` + +| 选项 | 用途 | +| --- | --- | +| `--prefix <目录>` | 指定安装目录 | +| `--no-modify-path` | 不修改终端启动文件 | +| `--wait` | 等待编译环境就绪 | +| `--release-url <地址>` | 使用自托管发行资源 | + +需要 Bash、支持并行下载的 curl、tar、xz、sha256sum、flock、getconf 和 nohup。`RUSTUP_DIST_SERVER` 可指定受信任的工具链镜像。 + +同版本安装会复用已有文件。验证重新打包的 `0.0.1` 时使用新的 `--prefix` 目录。安装不覆盖项目、系统编译器或用户 Cargo 配置。 + +## 离线安装 + +从 [发布页](https://git.dever.cc/dever/core/releases/tag/v0.0.1) 下载完整开发包及校验文件,在可写目录解压: + +```sh +sha256sum -c dever-0.0.1-linux-x86_64-development.tar.xz.sha256 +tar -xf dever-0.0.1-linux-x86_64-development.tar.xz +export PATH="$PWD/dever/bin:$PATH" +dever new hello +dever test hello +dever run hello -- hello.greeting.greet '{"name":"Dever"}' +``` + +完整包包含编译工具和离线依赖,无需另外安装 Rust、Cargo 或 GCC。外部 Python/JavaScript/Go 组件仍需准备它们各自的运行资源。 + +## 缓存与排错 + +在线安装的共享工具在 `toolchains/`,依赖缓存在 `cache/cargo/`;离线包使用包内 `toolchain/`。项目编译缓存位于安装包的 `rust/target/`。 + +环境准备失败时,命令会给出 `prepare.log` 路径;构建错误给出 `build.log` 路径。失败返回非零状态,修复后重试原命令。 + +安装包附 SHA-256,当前未签名。包中保留第三方许可证和工具版本记录。 + +## 交付程序 + +```sh +dever build hello --output ./hello-app +./hello-app hello.greeting.greet '{"name":"Dever"}' +``` + +部署只携带业务二进制、配置和必要资源;目标系统仍须具备匹配的系统运行库。安装包可以搬到新目录,移动后可能需要重新编译缓存。 + +编译器作者组装安装包的方法见 [源码开发](../rust/README.md)。 diff --git a/build/development.py b/build/development.py new file mode 100644 index 0000000..d7bd2b0 --- /dev/null +++ b/build/development.py @@ -0,0 +1,280 @@ +#!/usr/bin/env python3 +"""在 Debian 13 开发容器内组装 Linux x86_64 完整离线开发包。""" + +import argparse +import hashlib +import json +import os +from pathlib import Path +import shutil +import subprocess +import tomllib + + +SOURCE = Path(__file__).resolve().parents[1] +TRIPLE = "x86_64-unknown-linux-gnu" +PACKAGES = ( + "gcc-14-x86-64-linux-gnu", "cpp-14-x86-64-linux-gnu", "libgcc-14-dev", + "libc6-dev", "linux-libc-dev", "binutils-x86-64-linux-gnu", "libc6", "libgcc-s1", +) + + +def native_input(path): + """只保留原生 C 构建所需工具;Rust LTO 使用包内 LLVM,不需要 GCC LTO。""" + name = str(path) + if name.startswith("/usr/include/"): + return True + if path.parent == Path("/usr/bin"): + return path.name in { + "x86_64-linux-gnu-gcc-14", "x86_64-linux-gnu-as", + "x86_64-linux-gnu-ld", "x86_64-linux-gnu-ar", "x86_64-linux-gnu-ranlib", + } + if name.startswith("/usr/libexec/gcc/"): + return path.name in {"cc1", "collect2", "liblto_plugin.so", "lto-wrapper"} + if name.startswith("/usr/lib/gcc/"): + return "/include/" in name or path.name in { + "libgcc.a", "libgcc_eh.a", "libgcc_s.so", "crtbegin.o", "crtbeginS.o", + "crtbeginT.o", "crtend.o", "crtendS.o", + "liblto_plugin.so", + } + return path.parent in {Path("/usr/lib/x86_64-linux-gnu"), Path("/usr/lib64")} + + +def output(*arguments, **options): + return subprocess.check_output(arguments, text=True, **options).strip() + + +def digest(path): + with path.open("rb") as stream: + return hashlib.file_digest(stream, "sha256").hexdigest() + + +def copy(source, destination): + """只解引用作者明确选定的工具文件,不复制 Cargo home 或用户配置。""" + if source.is_dir(): + shutil.copytree(source, destination, dirs_exist_ok=True) + else: + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(source, destination) + destination.chmod(0o755 if os.access(source, os.X_OK) else 0o644) + + +def script(path, body): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text('#!/bin/sh\nset -eu\ntools=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)\n' + body) + path.chmod(0o755) + + +def prepare(compiler, work): + """两种交付方式共用现有本地打包入口,不另做编译器发行格式。""" + work.mkdir() + version = tomllib.loads((SOURCE / "rust/Cargo.toml").read_text())["workspace"]["package"]["version"] + rust_version = tomllib.loads((SOURCE / "rust/rust-toolchain.toml").read_text())["toolchain"]["channel"] + author = work / "author" + for path in ( + "rust/Cargo.toml", "rust/Cargo.lock", "rust/rust-toolchain.toml", "rust/src", + "lib", "skills", "LANGUAGE.md", "DOCUMENTATION.md", + ): + copy(SOURCE / path, author / "source" / path) + copy(compiler, author / "bin/dever") + (author / "config").mkdir() + (author / "config/setting.json").write_text(json.dumps({ + "format": "dever-local-bootstrap-input-v1", "version": version, + "target": "linux-x86_64", "source": "source", + "core": {"source": "bin/dever", "sha256": digest(compiler)}, + })) + bundle = work / "dever" + subprocess.run([str(compiler), "release", str(author), "--output", str(bundle)], check=True) + return bundle, version, rust_version + + +def record(bundle, paths): + """把交付附带文件纳入原有清单校验。""" + manifest = json.loads((bundle / "bootstrap.json").read_text()) + for path in sorted(paths): + if path.is_symlink(): + raise RuntimeError(f"staged artifact must be a regular file: {path}") + if path.is_file(): + manifest["artifacts"].append({ + "path": path.relative_to(bundle).as_posix(), "size": path.stat().st_size, + "sha256": digest(path), "executable": os.access(path, os.X_OK), + }) + manifest["artifacts"].sort(key=lambda artifact: artifact["path"]) + (bundle / "bootstrap.json").write_text(json.dumps(manifest, indent=2) + "\n") + return manifest + + +def native_tools(tools): + """保留 Debian 工具的相对安装布局;glibc 只进链接 sysroot,不覆盖宿主加载器。""" + base = {str(Path(path).resolve()) for path in output("dpkg-query", "-L", "libc6").splitlines()} + packages = set(PACKAGES) + executables = [] + for package in PACKAGES: + for name in output("dpkg-query", "-L", package).splitlines(): + path = Path(name) + if not path.is_file() or not native_input(path): + continue + copy(path, tools / "native" / path.relative_to("/")) + # Debian 的链接脚本使用 /lib;发行包展开 usrmerge 别名,不改写官方库文件。 + if package == "libc6": + copy(path, tools / "native" / path.relative_to("/usr")) + if os.access(path, os.X_OK): + with path.open("rb") as stream: + if stream.read(4) == b"\x7fELF": + executables.append(path) + for name in ("as", "ld", "ar"): + copy(Path(f"/usr/bin/x86_64-linux-gnu-{name}"), tools / "native/usr/bin" / name) + libraries = set() + for executable in executables: + listing = subprocess.run(["ldd", str(executable)], text=True, capture_output=True) + if listing.returncode and "not a dynamic executable" not in listing.stderr + listing.stdout: + raise RuntimeError(f"ldd {executable}: {listing.stderr}{listing.stdout}") + for line in listing.stdout.splitlines(): + if "=> not found" in line: + raise RuntimeError(line) + fields = line.split() + if "=>" in fields and fields[2].startswith("/"): + library = Path(fields[2]) + if str(library.resolve()) not in base: + libraries.add(library) + for library in sorted(libraries): + copy(library, tools / "lib" / library.name) + owner = output("dpkg-query", "-S", str(library.resolve())).splitlines()[0].split(": ", 1)[0] + packages.add(owner) + copy(Path("/usr/lib/x86_64-linux-gnu/libgcc_s.so.1"), tools / "lib/libgcc_s.so.1") + packages.add("libgcc-s1") + for package in sorted(packages): + name = package.split(":", 1)[0] + copy(Path("/usr/share/doc") / name / "copyright", tools / "licenses" / name / "copyright") + copy(Path("/usr/share/common-licenses"), tools / "licenses/common") + (tools / "native-packages.txt").write_text(output("dpkg-query", "-W", *sorted(packages)) + "\n") + script(tools / "bin/cc", '''export LD_LIBRARY_PATH="$tools/lib" +exec "$tools/native/usr/bin/x86_64-linux-gnu-gcc-14" \\ + --sysroot="$tools/native" \\ + -B"$tools/native/usr/lib/gcc/x86_64-linux-gnu/14/" \\ + -B"$tools/native/usr/libexec/gcc/x86_64-linux-gnu/14/" \\ + -B"$tools/native/usr/bin/" \\ + -B"$tools/native/usr/lib/x86_64-linux-gnu/" "$@" +''') + + +def trim_vendor(vendor, version): + """开发包编译业务程序;按 Cargo 完整依赖图保留运行库的全部特性和平台依赖。""" + metadata = json.loads(output("cargo", f"+{version}", "metadata", "--locked", "--offline", + "--all-features", "--format-version", "1", cwd=SOURCE / "rust")) + dependencies = {node["id"]: node["dependencies"] for node in metadata["resolve"]["nodes"]} + pending = [package["id"] for package in metadata["packages"] if package["name"] == "dever-runtime"] + required = set() + while pending: + package = pending.pop() + if package not in required: + required.add(package) + pending.extend(dependencies[package]) + for package in metadata["packages"]: + if package["source"] and package["id"] not in required: + directory = vendor / f'{package["name"]}-{package["version"]}' + # 编译器二进制仍使用这些依赖;源码可移除,第三方许可必须随包保留。 + licenses = {path for path in directory.rglob("*") if path.is_file() + and path.name.upper().startswith(("LICENSE", "COPYING", "COPYRIGHT", "NOTICE", "AUTHORS"))} + if package["license_file"]: + licenses.add(directory / package["license_file"]) + for path in licenses: + copy(path, vendor.parent / "licenses/cargo" / path.relative_to(vendor)) + shutil.rmtree(directory) + + +def rust_tools(tools, version, vendor): + rust = Path(output("rustc", f"+{version}", "--print", "sysroot")) + if f"host: {TRIPLE}" not in output("rustc", f"+{version}", "-vV"): + raise RuntimeError(f"this package requires the official {TRIPLE} toolchain") + for name in ("cargo", "rustc"): + copy(rust / "bin" / name, tools / "rust/bin" / name) + for library in (rust / "lib").glob("*.so*"): + if not library.is_symlink(): + copy(library, tools / "rust/lib" / library.name) + for name in ("lib", "bin"): + copy(rust / "lib/rustlib" / TRIPLE / name, tools / "rust/lib/rustlib" / TRIPLE / name) + # 只裁剪已链接的工具;标准库 rlib/rmeta 中的符号、类型元数据与位码仍用于编译。 + binaries = [*(tools / "rust/bin").iterdir(), *(tools / "rust/lib").glob("*.so*")] + binaries.extend(path for path in (tools / "rust/lib/rustlib" / TRIPLE / "bin").rglob("*") + if path.is_file()) + for binary in binaries: + # 官方 .so 目录也含链接脚本,按原样保留,只处理 ELF 文件。 + with binary.open("rb") as stream: + if stream.read(4) == b"\x7fELF": + subprocess.run(["strip", "--strip-unneeded", str(binary)], check=True) + copy(rust / "share/doc/rust", tools / "licenses/rust") + if vendor: + copy(vendor, tools / "vendor") + else: + subprocess.run([ + "cargo", f"+{version}", "vendor", "--locked", "--offline", "--versioned-dirs", + str(tools / "vendor"), + ], cwd=SOURCE / "rust", check=True) + trim_vendor(tools / "vendor", version) + config = tools / ".cargo/config.toml" + config.parent.mkdir() + config.write_text('''[source.crates-io] +replace-with = "vendored-sources" + +[source.vendored-sources] +directory = "vendor" +''') + script(tools / "bin/cargo", '''export CARGO_HOME="$tools/.cargo" +export RUSTC="$tools/rust/bin/rustc" +export CC="$tools/bin/cc" +export AR="$tools/native/usr/bin/x86_64-linux-gnu-ar" +export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER="$CC" +export LD_LIBRARY_PATH="$tools/lib" +export PATH="$tools/native/usr/bin:/usr/bin:/bin" +exec "$tools/rust/bin/cargo" --config "$tools/.cargo/config.toml" "$@" +''') + script(tools / "bin/rustc", '''export LD_LIBRARY_PATH="$tools/lib" +exec "$tools/rust/bin/rustc" "$@" +''') + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--compiler", required=True, type=Path) + parser.add_argument("--work", required=True, type=Path, help="new staging directory") + parser.add_argument("--output", required=True, type=Path, help="new .tar.xz or .tar.gz archive") + parser.add_argument("--vendor", type=Path, help="optional cargo vendor --locked --versioned-dirs result") + args = parser.parse_args() + system = Path("/etc/os-release").read_text() + if 'ID=debian\n' not in system or 'VERSION_ID="13"' not in system: + parser.error("prepare this package in the Debian 13 development container") + args.compiler = args.compiler.resolve(strict=True) + args.work = args.work.absolute() + args.output = args.output.absolute() + if not args.output.name.endswith((".tar.xz", ".tar.gz")): + parser.error("output must end in .tar.xz or .tar.gz") + if args.output.exists() or Path(str(args.output) + ".sha256").exists(): + parser.error("output archive and checksum must not exist") + bundle, _, rust_version = prepare(args.compiler, args.work) + author = args.work / "author" + tools = bundle / "toolchain" + tools.mkdir() + native_tools(tools) + rust_tools(tools, rust_version, args.vendor) + # 先实际链接,避免把缺少 sysroot 文件的包花时间压缩后才发现。 + smoke = author / "link.rs" + smoke.write_text('fn main() { println!("private Rust/C linker ready"); }\n') + subprocess.run([str(tools / "bin/rustc"), str(smoke), "-C", + "linker=" + str(tools / "bin/cc"), "-o", str(author / "link")], check=True) + subprocess.run([str(author / "link")], check=True) + copy(SOURCE / "build/README.md", bundle / "DEVELOPMENT.md") + manifest = record(bundle, [bundle / "DEVELOPMENT.md", *tools.rglob("*")]) + args.output.parent.mkdir(parents=True, exist_ok=True) + # 归档必须先于第一次构建,不能把 Cargo 编译缓存放进发行包。 + with args.output.open("xb") as stream: + compression = "xz -T4 -6" if args.output.name.endswith(".xz") else "gzip -9" + subprocess.run(["tar", "-C", str(args.work), "-I", compression, "-cf", "-", "dever"], + stdout=stream, check=True) + Path(str(args.output) + ".sha256").write_text(f"{digest(args.output)} {args.output.name}\n") + print(json.dumps({"archive": str(args.output), "bytes": args.output.stat().st_size, + "files": len(manifest["artifacts"]), "compiler_sha256": digest(args.compiler)})) + + +if __name__ == "__main__": + main() diff --git a/build/online.py b/build/online.py new file mode 100644 index 0000000..59e8750 --- /dev/null +++ b/build/online.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +"""在开发容器组装在线安装资源;不下载工具链、不发布或激活版本。""" + +import argparse +import json +from pathlib import Path +import subprocess +import tomllib + +from development import SOURCE, TRIPLE, copy, digest, native_tools, output, prepare, record, script + + +def archive(directory, destination): + with destination.open("xb") as stream: + subprocess.run([ + "tar", "--sort=name", "--mtime=@0", "--owner=0", "--group=0", "--numeric-owner", + "-C", str(directory.parent), "-I", "xz -T4 -6", "-cf", "-", directory.name, + ], stdout=stream, check=True) + return digest(destination) + + +def checksum(path): + Path(str(path) + ".sha256").write_text(f"{digest(path)} {path.name}\n") + + +def dependencies(bundle, rust_version): + """Cargo 自己裁剪锁文件;安装时只下载本机运行库依赖,不带作者编译依赖。""" + project = bundle / "toolchain/project" + (project / "src").mkdir(parents=True) + (project / "src/main.rs").write_text("fn main() {}\n") + features = sorted(tomllib.loads((bundle / "rust/src/runtime/Cargo.toml").read_text())["features"]) + (project / "Cargo.toml").write_text( + '[package]\nname = "dever-development-dependencies"\nversion = "0.0.0"\nedition = "2024"\n' + '[workspace]\n[dependencies]\n' + f'dever-runtime = {{ path = "../../rust/src/runtime", features = {json.dumps(features)} }}\n' + ) + copy(bundle / "rust/Cargo.lock", project / "Cargo.lock") + subprocess.run(["cargo", f"+{rust_version}", "metadata", "--offline", "--format-version", "1", + "--manifest-path", str(project / "Cargo.toml")], stdout=subprocess.DEVNULL, check=True) + + +def compiler_licenses(tools, rust_version): + metadata = json.loads(output("cargo", f"+{rust_version}", "metadata", "--locked", "--offline", + "--all-features", "--format-version", "1", cwd=SOURCE / "rust")) + for package in metadata["packages"]: + if not package["source"]: + continue + source = Path(package["manifest_path"]).parent + licenses = {path for path in source.rglob("*") if path.is_file() + and path.name.upper().startswith(("LICENSE", "COPYING", "COPYRIGHT", "NOTICE", "AUTHORS"))} + if package["license_file"]: + licenses.add(source / package["license_file"]) + for path in licenses: + copy(path, tools / "licenses/cargo" / source.name / path.relative_to(source)) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--compiler", required=True, type=Path) + parser.add_argument("--work", required=True, type=Path) + parser.add_argument("--output", required=True, type=Path, help="new release assets directory") + args = parser.parse_args() + system = Path("/etc/os-release").read_text() + if 'ID=debian\n' not in system or 'VERSION_ID="13"' not in system: + parser.error("prepare native tools in the Debian 13 development container") + compiler = args.compiler.resolve(strict=True) + work, destination = args.work.absolute(), args.output.absolute() + if work.exists() or destination.exists(): + parser.error("work and output must be new directories") + destination.mkdir(parents=True) + bundle, version, rust_version = prepare(compiler, work) + tools = bundle / "toolchain" + tools.mkdir() + native = work / "native-tools" + native.mkdir() + native_tools(native) + native_archive = destination / "native.tar.xz" + native_id = archive(native, native_archive) + native_archive = native_archive.rename(destination / f"dever-native-linux-x86_64-{native_id}.tar.xz") + checksum(native_archive) + copy(native / "bin/cc", tools / "bin/cc") + script(tools / "bin/cargo", '''export CARGO_HOME="$tools/cache" +export RUSTC="$tools/rust/bin/rustc" +export CC="$tools/bin/cc" +export AR="$tools/native/usr/bin/x86_64-linux-gnu-ar" +export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER="$CC" +export LD_LIBRARY_PATH="$tools/lib" +export PATH="$tools/native/usr/bin:/usr/bin:/bin" +exec "$tools/rust/bin/cargo" "$@" +''') + script(tools / "bin/rustc", '''export LD_LIBRARY_PATH="$tools/lib" +exec "$tools/rust/bin/rustc" "$@" +''') + (tools / "online").write_text(f"{rust_version} {native_id}\n") + copy(SOURCE / "build/prepare.sh", tools / "prepare") + dependencies(bundle, rust_version) + compiler_licenses(tools, rust_version) + copy(SOURCE / "build/README.md", bundle / "DEVELOPMENT.md") + record(bundle, [bundle / "DEVELOPMENT.md", *tools.rglob("*")]) + core_archive = destination / f"dever-{version}-linux-x86_64.tar.xz" + archive(bundle, core_archive) + checksum(core_archive) + installer = (SOURCE / "install").read_text().replace('@DEVER_VERSION@', version) + (destination / "install").write_text(installer) + (destination / "install").chmod(0o755) + checksum(destination / "install") + print(json.dumps({"version": version, "compiler_sha256": digest(compiler), + "core_bytes": core_archive.stat().st_size, + "native_bytes": native_archive.stat().st_size, + "installer_bytes": (destination / "install").stat().st_size, + "rust": rust_version, "target": TRIPLE})) + + +if __name__ == "__main__": + main() diff --git a/build/prepare.sh b/build/prepare.sh new file mode 100644 index 0000000..647222e --- /dev/null +++ b/build/prepare.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# 在线包的唯一环境准备入口;安装器后台启动,编译入口等待同一把锁。 +set -euo pipefail +package=$(cd -- "$(dirname -- "$0")/.." && pwd -P) +tools="$package/toolchain" +root=$(cd "$package/../.." && pwd -P) +[[ ! -f $tools/ready ]] || exit 0 +exec 9>"$root/.prepare.lock" +previous= +until flock -w 1 9; do + status='Waiting for Dever environment preparation...' + if [[ -f $tools/status ]]; then read -r status < "$tools/status" || true; fi + if [[ $status != "$previous" ]]; then printf '%s\n' "$status" >&2; previous=$status; fi +done +[[ ! -f $tools/ready ]] || exit 0 +exec 3>&2 +exec >"$tools/prepare.log" 2>&1 +stage= +rust_stage= +phase='Preparing Dever environment' +finish() { + local code=$? + if (( code )); then + printf 'Failed: %s (exit %s). Details: %s\n' "$phase" "$code" "$tools/prepare.log" > "$tools/status" + cat "$tools/status" >&3 + fi + if [[ -n $stage ]]; then rm -rf -- "$stage"; fi + if [[ -n $rust_stage ]]; then rm -rf -- "$rust_stage"; fi +} +trap finish EXIT +trap 'exit 130' INT +trap 'exit 143' TERM +progress() { + phase=$1 + printf '%s\n' "$phase" > "$tools/status" + printf '%s\n' "$phase" >&3 +} +read -r rust_version native_id < "$tools/online" +mapfile -t sources < "$tools/sources" +release_url=${sources[0]} +rust_dist=${sources[1]} +triple=x86_64-unknown-linux-gnu +rust_root="$root/toolchains/rust-$rust_version-$triple" +native_root="$root/toolchains/native-$native_id" +downloads="$root/cache/downloads" +mkdir -p "$downloads" +stage=$(mktemp -d "$root/.prepare.XXXXXX") +transfers=() +archives=() +queue() { + local url=$1 name=$2 + archives+=("$name") + if (( ${#transfers[@]} )); then transfers+=(--next); fi + transfers+=(--fail --location --silent --show-error --retry 2 --connect-timeout 15 + --speed-limit 1024 --speed-time 60 --continue-at - --output "$downloads/$name.part" "$url") + transfers+=(--next --fail --location --silent --show-error --retry 2 --connect-timeout 15 + --output "$downloads/$name.sha256" "$url.sha256") +} +native_archive="dever-native-linux-x86_64-$native_id.tar.xz" +if [[ ! -d $native_root ]]; then queue "$release_url/$native_archive" "$native_archive"; fi +if [[ ! -d $rust_root ]]; then + for component in rustc rust-std cargo; do + name="$component-$rust_version-$triple.tar.xz" + queue "$rust_dist/dist/$name" "$name" + done +fi +if (( ${#archives[@]} )); then + progress 'Downloading Dever build environment...' + curl --parallel --parallel-immediate --parallel-max 3 --fail-early "${transfers[@]}" + progress 'Verifying Dever build environment...' + for name in "${archives[@]}"; do + read -r expected remainder < "$downloads/$name.sha256" + if [[ $name == "$native_archive" && $expected != "$native_id" ]] || + ! printf '%s %s\n' "$expected" "$downloads/$name.part" | sha256sum --check --status; then + rm -f -- "$downloads/$name.part" "$downloads/$name.sha256" + echo "Checksum mismatch: $name" >&2 + exit 1 + fi + done +fi +progress 'Installing Dever build environment...' +if [[ ! -d $rust_root ]]; then + # 官方脚本不支持含空格的 prefix;先在独占临时目录安装,再原子发布。 + rust_stage=$(mktemp -d /tmp/dever-toolchain.XXXXXX) + for component in rustc rust-std cargo; do + name="$component-$rust_version-$triple" + tar -xJf "$downloads/$name.tar.xz.part" -C "$rust_stage" + bash "$rust_stage/$name/install.sh" --prefix="$rust_stage/toolchain" --disable-ldconfig + rm -rf -- "$rust_stage/$name" + done + mv "$rust_stage/toolchain" "$stage/rust" + mv "$stage/rust" "$rust_root" +fi +if [[ ! -d $native_root ]]; then + tar -xJf "$downloads/$native_archive.part" -C "$stage" + mv "$stage/native-tools" "$native_root" +fi +"$tools/bin/rustc" --version +progress 'Preparing Dever project dependencies...' +"$tools/bin/cargo" fetch --locked --target "$triple" --manifest-path "$tools/project/Cargo.toml" +for name in "$native_archive" "rustc-$rust_version-$triple.tar.xz" \ + "rust-std-$rust_version-$triple.tar.xz" "cargo-$rust_version-$triple.tar.xz"; do + rm -f -- "$downloads/$name.part" "$downloads/$name.sha256" +done +touch "$tools/ready" +progress 'Dever build environment is ready.' diff --git a/config/setting.example.json b/config/setting.example.json new file mode 100644 index 0000000..245b447 --- /dev/null +++ b/config/setting.example.json @@ -0,0 +1,24 @@ +{ + "database": { + "default": { + "type": "postgres", + "url": "postgresql://dever@127.0.0.1:5432/dever_default?sslmode=disable", + "tls": "disabled", + "min_connections": 0, + "max_connections": 2, + "max_page_size": 100, + "wait_timeout_ms": 1000, + "io_timeout_ms": 5000 + }, + "postgres_test": { + "type": "postgres", + "url": "postgresql://dever@127.0.0.1:5432/dever_{case}?sslmode=disable", + "tls": "disabled", + "min_connections": 0, + "max_connections": 2, + "max_page_size": 100, + "wait_timeout_ms": 1000, + "io_timeout_ms": 5000 + } + } +} diff --git a/editor/vscode/.gitignore b/editor/vscode/.gitignore new file mode 100644 index 0000000..9ac2c1c --- /dev/null +++ b/editor/vscode/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +*.vsix +dist/ +/core.[0-9]* diff --git a/editor/vscode/.vscodeignore b/editor/vscode/.vscodeignore new file mode 100644 index 0000000..5aafb2a --- /dev/null +++ b/editor/vscode/.vscodeignore @@ -0,0 +1,12 @@ +**/* +!package.json +!README.md +!language-configuration.json +!syntaxes/** +!extension.cjs +!graph.cjs +!inspection.cjs +!navigation.cjs +!reading.cjs +!hover.cjs +!dist/** diff --git a/editor/vscode/README.md b/editor/vscode/README.md new file mode 100644 index 0000000..8371f58 --- /dev/null +++ b/editor/vscode/README.md @@ -0,0 +1,50 @@ +# Dever for VS Code + +为 `.dever` 提供高亮、悬浮说明、源码跳转和调用流程。 + +## 安装 + +从 [发布页](https://git.dever.cc/dever/core/releases/tag/v0.0.1) 下载 `dever-language-0.0.1.vsix`,在 VS Code 执行 **Extensions: Install from VSIX**。 + +插件默认调用 PATH 中的 `dever`,需要带 `inspect` 的编译器。可用 `dever.command` 设置命令及固定参数,值为数组;远程执行时,编译器须能读取相同的源码路径。未信任的工作区不会执行编译器。 + +## 使用 + +| 操作 | 功能 | +| --- | --- | +| 悬浮函数 | 显示签名、主题说明和调用摘要 | +| F12 | 跳转定义 | +| Shift+F12 | 查看引用 | +| 显示调用层次结构 | 查看调用方和被调用方 | +| Dever: Preview Documentation | 打开主题说明 | +| Dever: Show Calls | 查看每处调用及源码位置 | +| Dever: Open Domain Links | 阅读领域调用图 | + +源码与说明保存后刷新索引。未保存修改或源码检查错误会暂停相关导航并提示原因。查询不运行应用,也不调用 AI。 + +调用图内置 Mermaid,可离线阅读和点击节点跳转。修改代码后生成流程: + +```sh +dever inspect . --links +dever inspect . --links --check +``` + +图保留分句、参数求值顺序、短路、集合处理、任务、事务和 Port/Adapter 选择。它展示可能执行的路径;部署配置决定实际 Adapter。 + +当前支持函数、API/CMD、REST、认证、Job、回调、SQL 和 Port/Adapter 导航,尚不提供通用类型和字段引用导航。源码与说明的组织见 [说明写法](https://git.dever.cc/dever/core/src/branch/main/DOCUMENTATION.md)。 + +## 开发插件 + +在本目录执行: + +```sh +npm ci +npm run build +DEVER_TEST_BIN=/path/to/dever npm run test:navigation +npm test -- --markdown-grammar /path/to/markdown.tmLanguage.json +npm run package +``` + +流程图浏览器测试使用 `npm run test:flows`,需设置 `DEVER_TEST_BIN` 和 `DEVER_BROWSER_TOOLS`,后者指向已有 playwright-core、Chromium 的 node_modules;系统库路径按测试环境设置。 + +构建会把 Markdown 解析器和 Mermaid 放入 VSIX,用户无需另装 npm 或渲染库。开发阶段版本统一为 `0.0.1`。 diff --git a/editor/vscode/build.cjs b/editor/vscode/build.cjs new file mode 100644 index 0000000..c4e5dc2 --- /dev/null +++ b/editor/vscode/build.cjs @@ -0,0 +1,12 @@ +const esbuild = require('esbuild'); +const fs = require('node:fs/promises'); + +async function build() { + await fs.mkdir('dist', { recursive: true }); + await Promise.all([ + fs.copyFile(require.resolve('@mermaid-js/tiny'), 'dist/mermaid.js'), + fs.copyFile('flow-browser.js', 'dist/flow-browser.js'), + esbuild.build({ entryPoints: ['flow-preview.cjs'], bundle: true, outfile: 'dist/flow-preview.cjs', minify: true, legalComments: 'eof', platform: 'node', external: ['vscode'] }) + ]); +} +build().catch(error => { console.error(error); process.exitCode = 1; }); diff --git a/editor/vscode/extension.cjs b/editor/vscode/extension.cjs new file mode 100644 index 0000000..ba4665d --- /dev/null +++ b/editor/vscode/extension.cjs @@ -0,0 +1,7 @@ +const { registerNavigation } = require('./navigation.cjs'); + +function activate(context) { + registerNavigation(context); +} + +module.exports = { activate }; diff --git a/editor/vscode/flow-browser.js b/editor/vscode/flow-browser.js new file mode 100644 index 0000000..f22a94b --- /dev/null +++ b/editor/vscode/flow-browser.js @@ -0,0 +1,39 @@ +(() => { +const mermaid = globalThis.mermaid; +const vscode = acquireVsCodeApi(); +mermaid.initialize({ startOnLoad: false, securityLevel: 'loose', theme: document.body.classList.contains('vscode-dark') ? 'dark' : 'default', flowchart: { htmlLabels: false } }); + +// 渲染失败保留原始 Mermaid 并显示原因,不把空白图当成成功。 +async function render() { + let sequence = 0; + for (const code of document.querySelectorAll('pre > code.language-mermaid')) { + try { + const { svg } = await mermaid.render(`dever-flow-${sequence++}`, code.textContent); + const container = document.createElement('div'); + container.className = 'dever-flow'; + container.innerHTML = svg; + code.parentElement.replaceWith(container); + } catch (error) { + const message = document.createElement('p'); + message.className = 'dever-flow-error'; + message.textContent = `流程图渲染失败:${error.message}`; + code.parentElement.before(message); + } + // Mermaid 的连续 Promise 会占满微任务队列;每张图后让页面处理加载、滚动和点击。 + await new Promise(resolve => setTimeout(resolve, 0)); + } + document.getElementById(document.body.dataset.anchor)?.scrollIntoView(); + document.body.dataset.rendered = 'true'; +} + +document.addEventListener('click', event => { + const link = event.target.closest('a'); + if (!link) return; + const href = link.getAttribute('href') || link.getAttribute('xlink:href'); + if (!href) return; + event.preventDefault(); + if (href.startsWith('#')) document.getElementById(decodeURIComponent(href.slice(1)))?.scrollIntoView(); + else vscode.postMessage({ type: 'open', href }); +}); +render(); +})(); diff --git a/editor/vscode/flow-preview.cjs b/editor/vscode/flow-preview.cjs new file mode 100644 index 0000000..e0da3f1 --- /dev/null +++ b/editor/vscode/flow-preview.cjs @@ -0,0 +1,84 @@ +const vscode = require('vscode'); +const fs = require('node:fs/promises'); +const path = require('node:path'); +const { randomBytes } = require('node:crypto'); +const { pathToFileURL, fileURLToPath } = require('node:url'); +const markdown = require('markdown-it')({ html: true }); +const validateLink = markdown.validateLink; +markdown.validateLink = href => href.startsWith('file:') || validateLink(href); + +// 流程图与调用表共用阅读面板;链接打开真实源码或当前索引。 +async function openFlowPreview(context, target, root, calls) { + const panel = vscode.window.createWebviewPanel('dever.links', 'Dever 调用流程', vscode.ViewColumn.Beside, { + enableScripts: true, + localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, 'dist')] + }); + let current = target.fsPath; + async function render(filename, anchor = '') { + const text = calls ? await calls.read(filename) : await fs.readFile(filename, 'utf8'); + if (!calls && !text.startsWith('')) { + throw new Error('此文件不是生成的领域索引,请运行 dever inspect . --links。'); + } + current = filename; + panel.title = calls ? `Dever · ${filename}` : `Dever · ${path.basename(path.dirname(filename))}`; + const script = panel.webview.asWebviewUri(vscode.Uri.joinPath(context.extensionUri, 'dist', 'flow-browser.js')); + const mermaid = panel.webview.asWebviewUri(vscode.Uri.joinPath(context.extensionUri, 'dist', 'mermaid.js')); + const nonce = randomBytes(16).toString('hex'); + panel.webview.html = ` + + + + ${markdown.render(text)} + `; + } + const messages = panel.webview.onDidReceiveMessage(async message => { + if (message.type !== 'open' || typeof message.href !== 'string') return; + try { + if (calls && message.href.startsWith('command:dever.showCalls?')) { + const [requestedRoot, name] = JSON.parse(decodeURIComponent(message.href.split('?')[1])); + if (requestedRoot !== root) throw new Error('链接超出当前项目。'); + await render(name); + return; + } + if (calls && message.href.startsWith('dever-source:')) { + const target = vscode.Uri.parse(message.href); + if (JSON.parse(target.query).root !== root) throw new Error('链接超出当前项目。'); + const document = await vscode.workspace.openTextDocument(target); + const location = /^#L(\d+)(?:,(\d+))?$/.exec(new URL(message.href).hash); + const line = location ? Number(location[1]) - 1 : 0; + const column = location?.[2] ? Number(location[2]) - 1 : 0; + await vscode.window.showTextDocument(document, { viewColumn: vscode.ViewColumn.One, selection: new vscode.Range(line, column, line, column) }); + return; + } + const destination = new URL(message.href, pathToFileURL(calls ? root + path.sep : current)); + if (destination.protocol !== 'file:') throw new Error('流程阅读页只打开项目源码和领域索引。'); + const filename = fileURLToPath(destination); + const relative = path.relative(root, filename); + if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) { + throw new Error('链接超出当前项目。'); + } + if (!calls && path.basename(filename) === 'link.md') { + await render(filename, decodeURIComponent(destination.hash.slice(1))); + } else { + const location = /^#L(\d+)(?:,(\d+))?$/.exec(destination.hash); + const line = location ? Number(location[1]) - 1 : 0; + const column = location?.[2] ? Number(location[2]) - 1 : 0; + const document = await vscode.workspace.openTextDocument(vscode.Uri.file(filename)); + await vscode.window.showTextDocument(document, { viewColumn: vscode.ViewColumn.One, selection: new vscode.Range(line, column, line, column) }); + } + } catch (error) { vscode.window.showErrorMessage(error.message); } + }); + panel.onDidDispose(() => messages.dispose()); + if (calls) { + const changed = calls.changed.event(() => render(current).catch(error => { + panel.dispose(); + vscode.window.showErrorMessage(error.message); + })); + panel.onDidDispose(() => changed.dispose()); + } + context.subscriptions.push(panel); + try { await render(calls ? calls.name : current); } catch (error) { panel.dispose(); throw error; } + return panel; +} + +module.exports = { openFlowPreview }; diff --git a/editor/vscode/graph.cjs b/editor/vscode/graph.cjs new file mode 100644 index 0000000..0d92efb --- /dev/null +++ b/editor/vscode/graph.cjs @@ -0,0 +1,28 @@ +// 所有关系来自编译器;这里只按源码位置选择符号和分组显示。 +function contains(range, position) { + const compare = (left, right) => left.line - right.line || left.character - right.character; + return compare(range.start, position) <= 0 && compare(position, range.end) < 0; +} + +// 自动分派用于调用层级;不能抢走显式绑定或 Port 声明的“转到定义”。 +const generated = new Set(['verify', 'rest-field', 'rest-model', 'adapter']); +function at(index, file, position) { + const calls = index.calls.filter(call => !generated.has(call.kind) && call.site.file === file && contains(call.site, position)); + const targets = new Set(calls.map(call => call.callee)); + if (targets.size) return index.symbols.filter(symbol => targets.has(symbol.id)); + return index.symbols.filter(symbol => symbol.declarations.some(location => + location.file === file && contains(location, position))); +} + +function relations(index, id, incoming) { + const grouped = new Map(); + for (const call of index.calls) { + if ((incoming ? call.callee : call.caller) !== id) continue; + const target = incoming ? call.caller : call.callee; + if (!grouped.has(target)) grouped.set(target, []); + grouped.get(target).push(call); + } + return [...grouped].map(([id, calls]) => ({ symbol: index.symbols.find(symbol => symbol.id === id), calls })); +} + +module.exports = { at, relations, contains }; diff --git a/editor/vscode/hover.cjs b/editor/vscode/hover.cjs new file mode 100644 index 0000000..e8b2f1f --- /dev/null +++ b/editor/vscode/hover.cjs @@ -0,0 +1,60 @@ +const vscode = require('vscode'); +const { relations } = require('./graph.cjs'); +const { uri } = require('./inspection.cjs'); + +function command(name, args) { + return `command:${name}?${encodeURIComponent(JSON.stringify(args))}`; +} + +async function description(inspection, index, symbol, root) { + const value = index.documentation.find(item => item.symbol === symbol.name); + if (!value) return undefined; + const documentIndex = { files: [index.documentation_files[value.file]] }; + await inspection.verify(documentIndex, 0, root); + return { ...value, uri: uri(documentIndex, 0, root) }; +} + +function registerHover(context, selector, inspection, lookup) { + context.subscriptions.push(vscode.languages.registerHoverProvider(selector, { + async provideHover(document, position) { + const state = await lookup(document, position); + if (!state?.symbols.length) return undefined; + const contents = []; + for (const symbol of state.symbols) { + const page = new vscode.MarkdownString(); + page.isTrusted = { enabledCommands: ['dever.showCalls', 'dever.openDomainLinks', 'dever.previewDocumentation'] }; + page.appendMarkdown(`**${symbol.name}**\n\n`); + if (state.index.documentation_errors.length) { + page.appendMarkdown('**说明校验未通过。** 失效或重复的标题详见“Dever”输出;签名和调用关系仍来自源码。\n\n'); + } + for (const signature of symbol.signatures) page.appendCodeblock(signature, 'dever'); + const detail = await description(inspection, state.index, symbol, state.root); + if (detail) { + page.baseUri = detail.uri; + page.appendMarkdown(`${detail.markdown}\n\n`); + } else { + page.appendMarkdown('尚未提供领域说明;签名和关系来自已保存源码。\n\n'); + } + for (const incoming of [true, false]) { + const related = relations(state.index, symbol.id, incoming); + const label = incoming ? '调用方' : '调用目标'; + const names = related.slice(0, 4).map(item => `\`${item.symbol.name}\``).join('、'); + page.appendMarkdown(`${label}:${names || '当前静态索引中无记录'}${related.length > 4 ? `,共 ${related.length} 个` : ''}。\n\n`); + } + const links = []; + for (const site of symbol.declarations.slice(0, 1)) { + const target = uri(state.index, site.file, state.root).with({ fragment: `L${site.start.line + 1},${site.start.character + 1}` }); + links.push(`[源码](${target.toString()})`); + } + if (detail) links.push(`[说明](${command('dever.previewDocumentation', [state.root, symbol.name])})`); + links.push(`[调用详情](${command('dever.showCalls', [state.root, symbol.name])})`); + links.push(`[领域流程](${command('dever.openDomainLinks', [state.root, symbol.name])})`); + page.appendMarkdown(links.join(' · ')); + contents.push(page); + } + return new vscode.Hover(contents); + } + })); +} + +module.exports = { registerHover, description, command }; diff --git a/editor/vscode/inspection.cjs b/editor/vscode/inspection.cjs new file mode 100644 index 0000000..9ca7114 --- /dev/null +++ b/editor/vscode/inspection.cjs @@ -0,0 +1,132 @@ +const vscode = require('vscode'); +const path = require('node:path'); +const fs = require('node:fs/promises'); +const { createHash } = require('node:crypto'); +const { execFile } = require('node:child_process'); +const { promisify } = require('node:util'); +const execute = promisify(execFile); + +function isSource(document) { + return document.languageId === 'dever'; +} + +async function projectRoot(document) { + if (document.uri.scheme === 'dever-source') return JSON.parse(document.uri.query).root; + const folder = vscode.workspace.getWorkspaceFolder(document.uri)?.uri.fsPath; + for (let directory = path.dirname(document.uri.fsPath); ; directory = path.dirname(directory)) { + try { + await fs.stat(path.join(directory, 'config', 'setting.json')); + return directory; + } catch (error) { + if (error.code !== 'ENOENT') throw error; + } + if (directory === folder || directory === path.dirname(directory)) break; + } + return folder || document.uri.fsPath; +} + +function uri(index, file, root) { + const source = index.files[file]; + return source.text === undefined ? vscode.Uri.file(source.path) : vscode.Uri.from({ + scheme: 'dever-source', path: `/${source.path}`, query: JSON.stringify({ root, path: source.path }) + }); +} + +function range(location) { + return new vscode.Range(location.start.line, location.start.character, location.end.line, location.end.character); +} + +function createInspection(context) { + const cache = new Map(); + const changed = new vscode.EventEmitter(); + const output = vscode.window.createOutputChannel('Dever'); + context.subscriptions.push(changed, output); + const invalidate = () => { cache.clear(); changed.fire(); }; + const watcher = vscode.workspace.createFileSystemWatcher('**/{*.dever,README.md,doc/*.md,dever.lock,dever.api,setting.json}'); + context.subscriptions.push(watcher, watcher.onDidChange(invalidate), watcher.onDidCreate(invalidate), watcher.onDidDelete(invalidate)); + context.subscriptions.push(vscode.workspace.onDidSaveTextDocument(document => { + if (isSource(document) || /[/\\]doc[/\\][^/\\]+\.md$/.test(document.uri.fsPath)) invalidate(); + })); + context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(event => { + if (event.affectsConfiguration('dever.command')) invalidate(); + })); + + async function load(root) { + if (!vscode.workspace.isTrusted) throw new Error('请信任工作区后再运行 Dever 源码检查。'); + if (!cache.has(root)) { + const command = vscode.workspace.getConfiguration('dever', vscode.Uri.file(root)).get('command'); + const promise = execute(command[0], [...command.slice(1), 'inspect', root, '--json'], { + maxBuffer: 32 * 1024 * 1024, windowsHide: true + }).then(({ stdout }) => { + const index = JSON.parse(stdout); + for (const error of index.documentation_errors) output.appendLine(error); + return index; + }).catch(error => { + if (cache.get(root) === promise) cache.delete(root); + const message = error.stderr?.trim() || error.message; + output.appendLine(message); + throw new Error(message); + }); + cache.set(root, promise); + } + const pending = cache.get(root); + const index = await pending; + if (cache.get(root) !== pending) throw new Error('检查期间源码已变化,请重新查询调用关系。'); + return index; + } + + async function verify(index, file, root) { + const document = await vscode.workspace.openTextDocument(uri(index, file, root)); + if (document.isDirty) throw new Error('调用关系基于已保存源码,请先保存文件。'); + const digest = createHash('sha256').update(document.getText()).digest('hex'); + if (digest !== index.files[file].sha256) { + invalidate(); + throw new Error('源码已变化,请刷新调用关系。'); + } + return document; + } + + async function current(document) { + if (document.isDirty) return undefined; + const root = await projectRoot(document); + if (vscode.workspace.textDocuments.some(value => value.isDirty && isSource(value) + && (value.uri.fsPath === root || value.uri.fsPath.startsWith(`${root}${path.sep}`)))) return undefined; + const index = await load(root); + const sourcePath = document.uri.scheme === 'dever-source' ? JSON.parse(document.uri.query).path : document.uri.fsPath; + const file = index.files.findIndex(source => source.path === sourcePath); + if (file < 0) return undefined; + await verify(index, file, root); + return { root, index, file }; + } + + async function location(index, site, root) { + await verify(index, site.file, root); + return new vscode.Location(uri(index, site.file, root), range(site)); + } + + async function checkLinks(root) { + if (!vscode.workspace.isTrusted) throw new Error('请信任工作区后再检查领域流程。'); + if (vscode.workspace.textDocuments.some(document => document.isDirty && isSource(document) + && document.uri.fsPath.startsWith(`${root}${path.sep}`))) throw new Error('请先保存源码,再查看领域流程。'); + const command = vscode.workspace.getConfiguration('dever', vscode.Uri.file(root)).get('command'); + try { + await execute(command[0], [...command.slice(1), 'inspect', root, '--links', '--check'], { windowsHide: true }); + } catch (error) { + throw new Error(error.stderr?.trim() || error.stdout?.trim() || error.message); + } + } + + context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider('dever-source', { + async provideTextDocumentContent(uri) { + const { root, path } = JSON.parse(uri.query); + const index = await load(root); + const source = index.files.find(source => source.path === path) + || index.documentation_files.find(description => description.path === path); + if (source?.text === undefined) throw new Error(`未找到内置源码:${path}`); + return source.text; + } + })); + return { load, current, verify, location, checkLinks, invalidate, changed }; +} + +module.exports = { createInspection, range, uri, isSource, projectRoot }; diff --git a/editor/vscode/language-configuration.json b/editor/vscode/language-configuration.json new file mode 100644 index 0000000..16d730b --- /dev/null +++ b/editor/vscode/language-configuration.json @@ -0,0 +1,18 @@ +{ + "comments": { + "lineComment": "#" + }, + "brackets": [["{", "}"], ["[", "]"], ["(", ")"]], + "autoClosingPairs": [ + { "open": "{", "close": "}", "notIn": ["string", "comment"] }, + { "open": "[", "close": "]", "notIn": ["string", "comment"] }, + { "open": "(", "close": ")", "notIn": ["string", "comment"] }, + { "open": "\"", "close": "\"", "notIn": ["string", "comment"] } + ], + "surroundingPairs": [["{", "}"], ["[", "]"], ["(", ")"], ["\"", "\""]], + "wordPattern": "[A-Za-z_][A-Za-z0-9_]*", + "indentationRules": { + "increaseIndentPattern": "^\\s*(?!#)(?:[^\"#]|\"(?:\\\\.|[^\"\\\\])*\")*[({\\[]\\s*(?:#.*)?$", + "decreaseIndentPattern": "^\\s*[})\\]]" + } +} diff --git a/editor/vscode/navigation.cjs b/editor/vscode/navigation.cjs new file mode 100644 index 0000000..c661105 --- /dev/null +++ b/editor/vscode/navigation.cjs @@ -0,0 +1,103 @@ +const vscode = require('vscode'); +const { createInspection, range, uri } = require('./inspection.cjs'); +const { at, relations } = require('./graph.cjs'); +const { registerReading } = require('./reading.cjs'); +const { registerHover } = require('./hover.cjs'); + +function registerNavigation(context) { + const inspection = createInspection(context); + const selector = [ + { language: 'dever', scheme: 'file' }, + { language: 'dever', scheme: 'dever-source' } + ]; + + async function item(index, symbol, root) { + const first = symbol.declarations[0]; + if (!first) return undefined; + await inspection.verify(index, first.file, root); + const end = symbol.declarations.at(-1).end; + const span = range({ start: first.start, end }); + const selected = new vscode.Range(span.start, span.start); + const value = new vscode.CallHierarchyItem(vscode.SymbolKind.Function, symbol.name, symbol.kind, + uri(index, first.file, root), span, selected); + return value; + } + + async function lookup(document, position) { + const state = await inspection.current(document); + return state ? { ...state, symbols: at(state.index, state.file, position) } : undefined; + } + + context.subscriptions.push(vscode.languages.registerDefinitionProvider(selector, { + async provideDefinition(document, position) { + const state = await lookup(document, position); + if (!state) return []; + return Promise.all(state.symbols.flatMap(symbol => symbol.declarations) + .map(site => inspection.location(state.index, site, state.root))); + } + })); + context.subscriptions.push(vscode.languages.registerReferenceProvider(selector, { + async provideReferences(document, position, options) { + const state = await lookup(document, position); + if (!state) return []; + const ids = new Set(state.symbols.map(symbol => symbol.id)); + const sites = state.index.calls.filter(call => ids.has(call.callee)).map(call => call.site); + if (options.includeDeclaration) sites.push(...state.symbols.flatMap(symbol => symbol.declarations)); + const unique = [...new Map(sites.map(site => [JSON.stringify(site), site])).values()]; + return Promise.all(unique.map(site => inspection.location(state.index, site, state.root))); + } + })); + + async function hierarchy(value, incoming) { + const document = await vscode.workspace.openTextDocument(value.uri); + const state = await inspection.current(document); + if (!state) throw new Error('请保存源码后重新打开调用层级。'); + const { root, index, file } = state; + const start = value.selectionRange.start; + const symbol = index.symbols.find(symbol => symbol.name === value.name && symbol.declarations.some(site => + site.file === file && site.start.line === start.line && site.start.character === start.character)); + if (!symbol) throw new Error('函数位置已变化,请重新打开调用层级。'); + await inspection.verify(index, symbol.declarations[0].file, root); + const result = []; + for (const relation of relations(index, symbol.id, incoming)) { + const target = await item(index, relation.symbol, root); + if (!target) continue; + for (const call of relation.calls) await inspection.verify(index, call.site.file, root); + const ranges = relation.calls.map(call => range(call.site)); + result.push(incoming ? new vscode.CallHierarchyIncomingCall(target, ranges) + : new vscode.CallHierarchyOutgoingCall(target, ranges)); + } + return result; + } + + context.subscriptions.push(vscode.languages.registerCallHierarchyProvider(selector, { + async prepareCallHierarchy(document, position) { + const state = await lookup(document, position); + if (!state) return []; + return (await Promise.all(state.symbols.map(symbol => item(state.index, symbol, state.root)))).filter(Boolean); + }, + provideCallHierarchyIncomingCalls(value) { return hierarchy(value, true); }, + provideCallHierarchyOutgoingCalls(value) { return hierarchy(value, false); } + })); + context.subscriptions.push(vscode.languages.registerCodeLensProvider(selector, { + onDidChangeCodeLenses: inspection.changed.event, + async provideCodeLenses(document) { + const state = await inspection.current(document); + if (!state) return []; + return state.index.symbols.filter(symbol => symbol.kind !== 'generated' + && symbol.declarations[0]?.file === state.file).map(symbol => { + const incoming = relations(state.index, symbol.id, true); + const count = incoming.reduce((total, relation) => total + relation.calls.length, 0); + const start = range(symbol.declarations[0]).start; + return new vscode.CodeLens(new vscode.Range(start, start), { + title: `${incoming.length} 个调用方 · ${count} 处调用 · 查看调用关系`, + command: 'dever.showCalls', arguments: [state.root, symbol.name] + }); + }); + } + })); + registerReading(context, inspection, lookup); + registerHover(context, selector, inspection, lookup); +} + +module.exports = { registerNavigation }; diff --git a/editor/vscode/package-lock.json b/editor/vscode/package-lock.json new file mode 100644 index 0000000..6255a25 --- /dev/null +++ b/editor/vscode/package-lock.json @@ -0,0 +1,2458 @@ +{ + "name": "dever-language", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dever-language", + "version": "0.0.1", + "license": "UNLICENSED", + "devDependencies": { + "@mermaid-js/tiny": "12.2.0", + "@vscode/vsce": "4.0.0", + "esbuild": "0.28.2", + "markdown-it": "15.0.3", + "vscode-oniguruma": "2.0.1", + "vscode-textmate": "9.2.0" + }, + "engines": { + "vscode": "^1.85.0" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.2.0.tgz", + "integrity": "sha512-fNAjWnA/nZ2jz31kxR/AqRaUT8ewHBw/WuBIosK0moMy1C9e5ValbDfFdIxJzVOOYaYkV/b2F1S4H/aHiqfVQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.11.0.tgz", + "integrity": "sha512-IUZydyTUkDnYdstOW9pFOOUQlBjAepK5teihDE3x6yxsPJs/hsAaaYpeGxdxrgtOiJbBKSjKW7MDk7AEhb4LRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.11.1.tgz", + "integrity": "sha512-2QygG2F76ZpMP2eMztiJvAiFMu71M9rDeU7vO/QKg5Css7MgM4frUOslFjhVjRhbGaCNPtz/S8M6y46/fFKVuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-process": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@azure/core-process/-/core-process-1.0.0.tgz", + "integrity": "sha512-/shnJ+ooO8WPxDhPEeI/2oRQuubn16gZ6CvlbpWbEswZfzwI9tI/sMAHmF3x1LuQ9yZYXfLW3TjzGMLEC5blKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.25.0.tgz", + "integrity": "sha512-bMs8ekJLjX8wPV+9IPBges1SLPyuDtE9g5gLDWOpxzKcoOFQnpLGkbcT1tdw3FaAmDS1gnPmMmJ6y/T5B96kIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.4.0.tgz", + "integrity": "sha512-eGwxD0AtncrxeBM4tG8R55Pc3rdX1hNW2WibJAgYpCVA6E93mvvVH+LcssoVjOBrSKWS55yEIHsk0X8ctHmfOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.14.0.tgz", + "integrity": "sha512-9n2pWK61veAuN0V20t9lOuoV4CFMdyAZ1ygZzvBGk/pBBJRib/PjL9PLXa/aI2CcPpyHfqVsxxqLCYl6uZlfDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.13.3", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.3.tgz", + "integrity": "sha512-zGQPtqvXPgSA8yfV2CkIQ1qirqk0p9AIVpC5uEkdXQYcKl07QHvyaGYRnZOk0AsQUmxNb4wfkcwY5di8Z5xa9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-process": "^1.0.0", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^5.5.0", + "@azure/msal-node": "^6.0.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.4.0.tgz", + "integrity": "sha512-rbAE25KUfjU/s3XHUdJgceoCP5dEOpMx85J04kF+QMdta73XkuG9JGHHinch+XIoKpBdqljin+KqURpJriSzLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.24.0.tgz", + "integrity": "sha512-jS1stt81tslcVZi7fqVD+g6vr8w/IWqFl/vAB2dx/6EOroQDuCR4+z0o3C+d6a/GpLATvdoO+kwnDcYzsBXKQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.14.2" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "16.14.2", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.14.2.tgz", + "integrity": "sha512-HOBR0Gfo39TMrkxNG8k6cfjRkrRjXZc3JQs3ebMWQc9S4cihuknMYra3sgmzbZB752oQBqBN9+WF7c826p2vIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-6.0.1.tgz", + "integrity": "sha512-ixSO1Y/kCVRthRs+hSx/5qkwaunX1/RAePhlMN0wIpIQ4WEZ6AREGGnGd1AP0qspHVsAwzWQKGubpjh50JyJIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.14.1", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@azure/msal-node/node_modules/@azure/msal-common": { + "version": "16.14.1", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.14.1.tgz", + "integrity": "sha512-Or6xhPNyi4zHW25158yxBoyxuCqNSPa5YBVqfF1J5Ks4MJWBo/USXdp05DQIPu1Zli00YZu6t0+h6KvHJealxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@mermaid-js/tiny": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/tiny/-/tiny-12.2.0.tgz", + "integrity": "sha512-9bEE+R974NTh1YJlfc6ztgwhy6+9sE+wAFiFrqvSdtlJHdxGfLytQmHYHNpFKmj94+Z+uMicYrQeGXnTTjILAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@napi-rs/keyring": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring/-/keyring-1.3.0.tgz", + "integrity": "sha512-WrOw/bcXm0f9qHkumlT1QlArXSTWqaY9sunsDpOk+yCCorCKMxvWT/a3xko4EYHVdeZoh00yI2TydXn6eyICDA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/keyring-darwin-arm64": "1.3.0", + "@napi-rs/keyring-darwin-x64": "1.3.0", + "@napi-rs/keyring-freebsd-x64": "1.3.0", + "@napi-rs/keyring-linux-arm-gnueabihf": "1.3.0", + "@napi-rs/keyring-linux-arm64-gnu": "1.3.0", + "@napi-rs/keyring-linux-arm64-musl": "1.3.0", + "@napi-rs/keyring-linux-riscv64-gnu": "1.3.0", + "@napi-rs/keyring-linux-x64-gnu": "1.3.0", + "@napi-rs/keyring-linux-x64-musl": "1.3.0", + "@napi-rs/keyring-win32-arm64-msvc": "1.3.0", + "@napi-rs/keyring-win32-ia32-msvc": "1.3.0", + "@napi-rs/keyring-win32-x64-msvc": "1.3.0" + } + }, + "node_modules/@napi-rs/keyring-darwin-arm64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-darwin-arm64/-/keyring-darwin-arm64-1.3.0.tgz", + "integrity": "sha512-pl76hJvdYUBn6I24bXiOBMA9nbDapo3I5B+f3OorjDU4dUMSypXeKbOVehJe8fhgTiH24flMyTS3aAIy43xegQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-darwin-x64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-darwin-x64/-/keyring-darwin-x64-1.3.0.tgz", + "integrity": "sha512-YcJtEV5LA3cvA4z3BurgxH5IhTsW1JfIvcAAcqcecwk06Si9F9NqkxbZVIfDwQ8oRHgaBmT3zZJnLAotCrVahw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-freebsd-x64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-freebsd-x64/-/keyring-freebsd-x64-1.3.0.tgz", + "integrity": "sha512-vlLf31TGhfRAaxLDBhg8b89ss0HHD/lyNmL5F3UjSaz5CUXElsJmKYq9fqA/B+cZKUEUcLHHGhF0I/CqcFdaVw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-arm-gnueabihf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm-gnueabihf/-/keyring-linux-arm-gnueabihf-1.3.0.tgz", + "integrity": "sha512-KiWdMMu/Inz/bHHIAGrnF7r54FZDYXuHO6UFF/rhIrshUsxbMG1Rl9lEymNtqqsVo927G0VYcb02FzWQ3iBQRQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-arm64-gnu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm64-gnu/-/keyring-linux-arm64-gnu-1.3.0.tgz", + "integrity": "sha512-eyKGpY40lm9Jvs1aD294XRH4y7+TlJM0YVAryZeXA6TX0mb4gMkxVXwSQv7MCwgah7raeUd0dKUb4BPAYIgcMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-arm64-musl": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm64-musl/-/keyring-linux-arm64-musl-1.3.0.tgz", + "integrity": "sha512-iIK6JWHXAJqDrEyLY3TmswwloVyt2vj+04TZnew+uSJ9gnDO8EwRbp3/iw3LpWaXiDO7VomGO6y8I0Id8uBZSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-riscv64-gnu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-riscv64-gnu/-/keyring-linux-riscv64-gnu-1.3.0.tgz", + "integrity": "sha512-/PGqrwn6EwgtK6vccASSXJRfOSP4vN1F4ASsIQ+7MdrK6hNvAJ1FZPrIuD5gGGdxezo3F++To2Wq7DbuGIeuNQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-x64-gnu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-x64-gnu/-/keyring-linux-x64-gnu-1.3.0.tgz", + "integrity": "sha512-2PDK1WKWTu9lBGq9VvNEkSlQD3O7YwVpmnyN2M3cy4v7NJ/8gDMd9GXv3G+FVXN13uhp4gnnPBS+ScefmEeD2A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-x64-musl": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-x64-musl/-/keyring-linux-x64-musl-1.3.0.tgz", + "integrity": "sha512-oJ2HkX8YUo46QBkn0pG+HuIKQNqr523q6vBobCn+P95s4C4K6/kLBqHY/1bg5J4ap31DzsznhnFKcfBNBsjCnw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-win32-arm64-msvc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-arm64-msvc/-/keyring-win32-arm64-msvc-1.3.0.tgz", + "integrity": "sha512-tOd3c/uAaeoE4ycVlmAdSvygz0Zt3zdca6Y7gokBeIbaRDWpjDIUOpU3MvML59XAaqyuKGsVVu0F/DZb1lHPmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-win32-ia32-msvc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-ia32-msvc/-/keyring-win32-ia32-msvc-1.3.0.tgz", + "integrity": "sha512-sPSqeAFZMGqP1R++M2JTza7GQJJ/TpCo6JU6Vcd4jnebvOaEDs9b7eipakU1PJdSvhpC2yXMCNRk9gXfrhuwHQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-win32-x64-msvc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-x64-msvc/-/keyring-win32-x64-msvc-1.3.0.tgz", + "integrity": "sha512-4DnCWXwDc0HRKwyRlG5y0VhKZW2tNRQfKKfyj6IX/KWfDNyq9hn4n+GL1auyDcOO/v8PwnhmYo2+rOOqCkvvOg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@secretlint/core": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/core/-/core-10.2.2.tgz", + "integrity": "sha512-6rdwBwLP9+TO3rRjMVW1tX+lQeo5gBbxl1I5F8nh8bgGtKwdlCMhMKsBWzWg1ostxx/tIG7OjZI0/BxsP8bUgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/profiler": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "structured-source": "^4.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/profiler": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/profiler/-/profiler-10.2.2.tgz", + "integrity": "sha512-qm9rWfkh/o8OvzMIfY8a5bCmgIniSpltbVlUVl983zDG1bUuQNd1/5lUEeWx5o/WJ99bXxS7yNI4/KIXfHexig==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/secretlint-rule-no-dotenv": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-no-dotenv/-/secretlint-rule-no-dotenv-10.2.2.tgz", + "integrity": "sha512-KJRbIShA9DVc5Va3yArtJ6QDzGjg3PRa1uYp9As4RsyKtKSSZjI64jVca57FZ8gbuk4em0/0Jq+uy6485wxIdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/secretlint-rule-preset-recommend": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-10.2.2.tgz", + "integrity": "sha512-K3jPqjva8bQndDKJqctnGfwuAxU2n9XNCPtbXVI5JvC7FnQiNg/yWlQPbMUlBXtBoBGFYp08A94m6fvtc9v+zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/source-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/source-creator/-/source-creator-10.2.2.tgz", + "integrity": "sha512-h6I87xJfwfUTgQ7irWq7UTdq/Bm1RuQ/fYhA3dtTIAop5BwSFmZyrchph4WcoEvbN460BWKmk4RYSvPElIIvxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2", + "istextorbinary": "^9.5.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/types": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-10.2.2.tgz", + "integrity": "sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.9.tgz", + "integrity": "sha512-edSdeAqkdxBVzA1yL1LrLCml1YjyCVvPMtMqJpbF+6K609tHe8V6sQUzFQSGcYNhcuhOceZtjvN32+mpIth30A==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@vscode/vsce": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-4.0.0.tgz", + "integrity": "sha512-NImwuLaenMmb5D5Jer9/lzi/F9ZQUBOp8Azhj/BVYcTFgixv8KehFXqEUDjQlD2tAiw2E6dDGyjTuAB//di60A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/identity": "^4.13.2", + "@napi-rs/keyring": "^1.3.0", + "@secretlint/core": "^10.2.2", + "@secretlint/secretlint-rule-no-dotenv": "^10.2.2", + "@secretlint/secretlint-rule-preset-recommend": "^10.2.2", + "@secretlint/source-creator": "^10.2.2", + "@secretlint/types": "^10.2.2", + "@vscode/vsce-sign": "^2.1.0", + "azure-devops-node-api": "^12.5.0", + "cockatiel": "^3.2.1", + "commander": "^12.1.0", + "hosted-git-info": "^4.1.0", + "jsonc-parser": "^3.3.1", + "marked": "^18.0.11", + "mime": "^1.6.0", + "minimatch": "^10.2.6", + "parse5": "^8.0.1", + "proper-lockfile": "^4.1.2", + "read": "^1.0.7", + "semver": "^7.8.5", + "tinyglobby": "^0.2.17", + "typed-rest-client": "^1.8.11", + "url-join": "^4.0.1", + "xml2js": "^0.5.0", + "yauzl": "^3.4.0", + "yazl": "^2.5.1" + }, + "bin": { + "vsce": "vsce" + }, + "engines": { + "node": ">= 22" + } + }, + "node_modules/@vscode/vsce-sign": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.1.0.tgz", + "integrity": "sha512-9AQrqazrBgTgRSuwleLVXUrIUphY02/SFCh2TKYoLV/xifJAdblhdmEmw5gUrYSPQ3sRwNs9iyCMD14sATEE6g==", + "dev": true, + "hasInstallScript": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optionalDependencies": { + "@vscode/vsce-sign-alpine-arm64": "2.0.6", + "@vscode/vsce-sign-alpine-x64": "2.0.6", + "@vscode/vsce-sign-darwin-arm64": "2.0.6", + "@vscode/vsce-sign-darwin-x64": "2.0.6", + "@vscode/vsce-sign-linux-arm": "2.0.6", + "@vscode/vsce-sign-linux-arm64": "2.0.6", + "@vscode/vsce-sign-linux-x64": "2.0.6", + "@vscode/vsce-sign-win32-arm64": "2.0.6", + "@vscode/vsce-sign-win32-x64": "2.0.6" + } + }, + "node_modules/@vscode/vsce-sign-alpine-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.6.tgz", + "integrity": "sha512-wKkJBsvKF+f0GfsUuGT0tSW0kZL87QggEiqNqK6/8hvqsXvpx8OsTEc3mnE1kejkh5r+qUyQ7PtF8jZYN0mo8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-alpine-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.6.tgz", + "integrity": "sha512-YoAGlmdK39vKi9jA18i4ufBbd95OqGJxRvF3n6ZbCyziwy3O+JgOpIUPxv5tjeO6gQfx29qBivQ8ZZTUF2Ba0w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.6.tgz", + "integrity": "sha512-5HMHaJRIQuozm/XQIiJiA0W9uhdblwwl2ZNDSSAeXGO9YhB9MH5C4KIHOmvyjUnKy4UCuiP43VKpIxW1VWP4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.6.tgz", + "integrity": "sha512-25GsUbTAiNfHSuRItoQafXOIpxlYj+IXb4/qarrXu7kmbH94jlm5sdWSCKrrREs8+GsXF1b+l3OB7VJy5jsykw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.6.tgz", + "integrity": "sha512-UndEc2Xlq4HsuMPnwu7420uqceXjs4yb5W8E2/UkaHBB9OWCwMd3/bRe/1eLe3D8kPpxzcaeTyXiK3RdzS/1CA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.6.tgz", + "integrity": "sha512-cfb1qK7lygtMa4NUl2582nP7aliLYuDEVpAbXJMkDq1qE+olIw/es+C8j1LJwvcRq1I2yWGtSn3EkDp9Dq5FdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.6.tgz", + "integrity": "sha512-/olerl1A4sOqdP+hjvJ1sbQjKN07Y3DVnxO4gnbn/ahtQvFrdhUi0G1VsZXDNjfqmXw57DmPi5ASnj/8PGZhAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-win32-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.6.tgz", + "integrity": "sha512-ivM/MiGIY0PJNZBoGtlRBM/xDpwbdlCWomUWuLmIxbi1Cxe/1nooYrEQoaHD8ojVRgzdQEUzMsRbyF5cJJgYOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce-sign-win32-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.6.tgz", + "integrity": "sha512-mgth9Kvze+u8CruYMmhHw6Zgy3GRX2S+Ed5oSokDEK5vPEwGGKnmuXua9tmFhomeAnhgJnL4DCna3TiNuGrBTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/argparse": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.4.tgz", + "integrity": "sha512-fwWK3Zq22l4XJJjx5mcBHGikIk+hJqO+32dPm0Jur7+rtG5x1scOjSfdT9ZKeWHPpsya/l0HHK4tDImBkTrtvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "PSF-2.0" + }, + "node_modules/azure-devops-node-api": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", + "dev": true, + "license": "MIT", + "dependencies": { + "tunnel": "0.0.6", + "typed-rest-client": "^1.8.4" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/binaryextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-6.11.0.tgz", + "integrity": "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/boundary": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/boundary/-/boundary-2.0.0.tgz", + "integrity": "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/bundle-name": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.1.tgz", + "integrity": "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/cockatiel": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", + "integrity": "sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/default-browser": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz", + "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/editions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/editions/-/editions-6.22.0.tgz", + "integrity": "sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "version-range": "^4.15.0" + }, + "engines": { + "ecmascript": ">= es5", + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/entities": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istextorbinary": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-9.5.0.tgz", + "integrity": "sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "binaryextensions": "^6.11.0", + "editions": "^6.21.0", + "textextensions": "^6.11.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/linkify-it": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-6.1.0.tgz", + "integrity": "sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^3.0.0" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/markdown-it": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-15.0.3.tgz", + "integrity": "sha512-+Az7Ojz3iRGu8KYMvJ6xVhvrxP3eE+vEtJX75Ow88XBpcQNV/0lGX5ZynIIBftREo4DC2jsveAMDaeltmm8QvQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^3.0.3", + "entities": "^8.1.0", + "linkify-it": "^6.1.0", + "mdurl": "^2.1.0", + "punycode.js": "^2.3.1", + "uc.micro": "^3.0.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/marked": { + "version": "18.0.14", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.14.tgz", + "integrity": "sha512-mBHK6FBHuBAlhgRe88w9F0O1AbwwXJUcQibUbC/QcdTbVGAD7aWza+xt3N6oT/jCZx3/OMeS+8rnuiHZcQ9s7A==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdurl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/structured-source": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/structured-source/-/structured-source-4.0.0.tgz", + "integrity": "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boundary": "^2.0.0" + } + }, + "node_modules/textextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-6.11.0.tgz", + "integrity": "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/typed-rest-client": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "qs": "^6.9.1", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + } + }, + "node_modules/uc.micro": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-3.0.0.tgz", + "integrity": "sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/version-range": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/version-range/-/version-range-4.15.0.tgz", + "integrity": "sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg==", + "dev": true, + "license": "Artistic-2.0", + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/vscode-oniguruma": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-2.0.1.tgz", + "integrity": "sha512-poJU8iHIWnC3vgphJnrLZyI3YdqRlR27xzqDmpPXYzA93R4Gk8z7T6oqDzDoHjoikA2aS82crdXFkjELCdJsjQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-textmate": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-9.2.0.tgz", + "integrity": "sha512-rkvG4SraZQaPSN/5XjwKswdU0OP9MF28QjrYzUBbhb8QyG3ljB1Ky996m++jiI7KdiAP2CkBiQZd9pqEDTClqA==", + "dev": true, + "license": "MIT" + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yauzl": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", + "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3" + } + } + } +} diff --git a/editor/vscode/package.json b/editor/vscode/package.json new file mode 100644 index 0000000..0ada3ae --- /dev/null +++ b/editor/vscode/package.json @@ -0,0 +1,110 @@ +{ + "name": "dever-language", + "displayName": "Dever Language", + "description": "Dever source navigation, domain documentation, hover and call flows.", + "version": "0.0.1", + "main": "./extension.cjs", + "activationEvents": [ + "onLanguage:dever" + ], + "publisher": "shemic", + "private": true, + "license": "UNLICENSED", + "engines": { + "vscode": "^1.85.0" + }, + "categories": [ + "Programming Languages" + ], + "keywords": [ + "dever", + "markdown", + "syntax highlighting" + ], + "repository": { + "type": "git", + "url": "ssh://git@git.dever.cc:2222/dever/core.git", + "directory": "editor/vscode" + }, + "contributes": { + "commands": [ + { + "command": "dever.previewDocumentation", + "title": "Dever: Preview Documentation" + }, + { + "command": "dever.showCalls", + "title": "Dever: Show Calls" + }, + { + "command": "dever.openDomainLinks", + "title": "Dever: Open Domain Links" + } + ], + "configuration": { + "title": "Dever", + "properties": { + "dever.command": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "default": [ + "dever" + ], + "description": "Compiler executable followed by fixed arguments; inspect is appended without a shell." + } + } + }, + "languages": [ + { + "id": "dever", + "aliases": [ + "Dever", + "dever" + ], + "extensions": [ + ".dever" + ], + "configuration": "./language-configuration.json" + } + ], + "grammars": [ + { + "language": "dever", + "scopeName": "source.dever", + "path": "./syntaxes/dever.tmLanguage.json" + }, + { + "scopeName": "markdown.dever.codeblock", + "path": "./syntaxes/dever-markdown.tmLanguage.json", + "injectTo": [ + "text.html.markdown" + ], + "embeddedLanguages": { + "meta.embedded.block.dever": "dever" + }, + "tokenTypes": { + "meta.embedded.block.dever": "other" + } + } + ] + }, + "scripts": { + "build": "node build.cjs", + "vscode:prepublish": "npm run build", + "test": "node ../../test/editor-vscode/highlight.test.cjs", + "test:navigation": "node --test ../../test/editor-vscode/navigation.test.cjs", + "test:flows": "node --test ../../test/editor-vscode/flows.test.cjs", + "package": "vsce package --no-dependencies --skip-license" + }, + "devDependencies": { + "@mermaid-js/tiny": "12.2.0", + "@vscode/vsce": "4.0.0", + "esbuild": "0.28.2", + "markdown-it": "15.0.3", + "vscode-oniguruma": "2.0.1", + "vscode-textmate": "9.2.0" + } +} diff --git a/editor/vscode/reading.cjs b/editor/vscode/reading.cjs new file mode 100644 index 0000000..82a9f6c --- /dev/null +++ b/editor/vscode/reading.cjs @@ -0,0 +1,119 @@ +const vscode = require('vscode'); +const path = require('node:path'); +const { relations } = require('./graph.cjs'); +const { uri, projectRoot } = require('./inspection.cjs'); +const { openFlowPreview } = require('./dist/flow-preview.cjs'); +const { description, command } = require('./hover.cjs'); + +function registerReading(context, inspection, lookup) { + const changed = new vscode.EventEmitter(); + const previews = new Map(); + context.subscriptions.push(changed, inspection.changed.event(() => { + for (const uri of previews.values()) changed.fire(uri); + })); + context.subscriptions.push(vscode.workspace.onDidCloseTextDocument(document => { + previews.delete(document.uri.toString()); + })); + const provider = { + onDidChange: changed.event, + async provideTextDocumentContent(documentUri) { + previews.set(documentUri.toString(), documentUri); + const { root, name } = JSON.parse(documentUri.query); + const index = await inspection.load(root); + const symbols = index.symbols.filter(symbol => symbol.name === name); + if (!symbols.length) throw new Error(`函数已不存在:${name}`); + const lines = [`# ${name}`, '', '静态源码关系;分支可能只执行一部分,任务派发不表示同步执行。', '', index.coverage, '']; + if (index.documentation_errors.length) lines.push('**说明校验未通过。** 详见“Dever”输出。', ''); + function link(site) { + const file = index.files[site.file]; + const target = uri(index, site.file, root).with({ fragment: `L${site.start.line + 1},${site.start.character + 1}` }); + return `[${file.path}:${site.start.line + 1}](${target.toString()})`; + } + for (const symbol of symbols) { + for (const signature of symbol.signatures) lines.push('```dever', signature, '```', ''); + const detail = await description(inspection, index, symbol, root); + if (detail) lines.push(detail.markdown, '', `[主题说明](${detail.uri.toString()})`, ''); + for (const site of symbol.declarations) { + await inspection.verify(index, site.file, root); + lines.push('## 定义', '', link(site), ''); + } + for (const incoming of [true, false]) { + lines.push(incoming ? '## 谁调用它' : '## 它调用谁', ''); + const related = relations(index, symbol.id, incoming); + if (!related.length) lines.push('当前已检查源码中没有此类关系。', ''); + else lines.push('| 函数 | 领域 | 关系 | 调用位置 | 定义 / 上下游 |', '| --- | --- | --- | --- | --- |'); + for (const relation of related) { + for (const call of relation.calls) { + await inspection.verify(index, call.site.file, root); + const definitions = relation.symbol.declarations.map(link).join('、') || '内建操作'; + const chain = command('dever.showCalls', [root, relation.symbol.name]); + lines.push(`| ${relation.symbol.name} | ${relation.symbol.name.split('.').slice(0, 2).join('.')} | ${call.kind} | ${link(call.site)} | ${definitions} · [上下游](${chain}) |`); + } + } + lines.push(''); + } + } + return lines.join('\n'); + } + }; + context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider('dever-calls', provider)); + context.subscriptions.push(vscode.commands.registerCommand('dever.showCalls', async (root, name) => { + try { + if (!root || !name) { + const editor = vscode.window.activeTextEditor; + if (!editor) return; + const state = await lookup(editor.document, editor.selection.active); + if (!state) throw new Error('请先保存源码,再查看调用关系。'); + if (!state.symbols.length) throw new Error('请把光标放在函数或调用位置。'); + root = state.root; + name = state.symbols[0].name; + } + const target = vscode.Uri.from({ scheme: 'dever-calls', path: '/calls.md', query: JSON.stringify({ root, name }) }); + await openFlowPreview(context, target, root, { + name, + changed: inspection.changed, + read: name => provider.provideTextDocumentContent(target.with({ query: JSON.stringify({ root, name }) })) + }); + } catch (error) { vscode.window.showErrorMessage(error.message); } + })); + context.subscriptions.push(vscode.commands.registerCommand('dever.previewDocumentation', async (root, name) => { + try { + const document = vscode.window.activeTextEditor?.document; + if (!root || !name) { + if (!document) return; + const state = await lookup(document, vscode.window.activeTextEditor.selection.active); + root = state?.root || await projectRoot(document); + name = state?.symbols[0]?.name; + } + const index = await inspection.load(root); + const symbol = index.symbols.find(value => value.name === name); + const detail = symbol && await description(inspection, index, symbol, root); + if (!detail) throw new Error('该符号尚未提供 doc/ 说明。'); + await vscode.commands.executeCommand('markdown.showPreviewToSide', detail.uri.with({ fragment: `L${detail.line + 1}` })); + } catch (error) { vscode.window.showErrorMessage(error.message); } + })); + context.subscriptions.push(vscode.commands.registerCommand('dever.openDomainLinks', async (root, name) => { + try { + const document = vscode.window.activeTextEditor?.document; + if (!root && !document) return; + root ||= await projectRoot(document); + const index = await inspection.load(root); + const symbol = index.symbols.find(value => value.name === name); + const source = symbol?.declarations[0]; + const filename = source ? index.files[source.file].path : document.uri.fsPath; + const relative = path.relative(path.join(root, 'module'), filename).split(path.sep); + if (relative.length < 3 || relative[0] === '..') { + throw new Error('请在 module/<组件>/<领域>/ 内的源码中执行此命令。'); + } + const target = vscode.Uri.file(path.join(root, 'module', ...relative.slice(0, 2), 'link.md')); + await inspection.checkLinks(root); + const panel = await openFlowPreview(context, target, root); + const changed = inspection.changed.event(() => panel.dispose()); + panel.onDidDispose(() => changed.dispose()); + } catch (error) { + vscode.window.showErrorMessage(`${error.message} 可在项目根运行 dever inspect . --links 生成索引。`); + } + })); +} + +module.exports = { registerReading }; diff --git a/editor/vscode/syntaxes/dever-markdown.tmLanguage.json b/editor/vscode/syntaxes/dever-markdown.tmLanguage.json new file mode 100644 index 0000000..a74eb03 --- /dev/null +++ b/editor/vscode/syntaxes/dever-markdown.tmLanguage.json @@ -0,0 +1,31 @@ +{ + "name": "Dever Markdown blocks", + "scopeName": "markdown.dever.codeblock", + "injectionSelector": "L:text.html.markdown -markup.fenced_code.block -markup.raw.block -markup.quote -markup.list -meta.embedded.block -comment", + "patterns": [ + { + "name": "markup.fenced_code.block.markdown", + "contentName": "meta.embedded.block.dever source.dever", + "begin": "^ {0,3}(`{3,})[ \\t]*(dever|typescript dever)[ \\t]*$", + "end": "^ {0,3}\\1`* *$", + "beginCaptures": { + "1": { "name": "punctuation.definition.markdown.dever" }, + "2": { "name": "fenced_code.block.language.markdown" } + }, + "endCaptures": { "0": { "name": "punctuation.definition.markdown.dever" } }, + "patterns": [{ "include": "source.dever" }] + }, + { + "name": "markup.fenced_code.block.markdown", + "contentName": "meta.embedded.block.dever source.dever", + "begin": "^ {0,3}(~{3,})[ \\t]*(dever|typescript dever)[ \\t]*$", + "end": "^ {0,3}\\1~* *$", + "beginCaptures": { + "1": { "name": "punctuation.definition.markdown.dever" }, + "2": { "name": "fenced_code.block.language.markdown" } + }, + "endCaptures": { "0": { "name": "punctuation.definition.markdown.dever" } }, + "patterns": [{ "include": "source.dever" }] + } + ] +} diff --git a/editor/vscode/syntaxes/dever.tmLanguage.json b/editor/vscode/syntaxes/dever.tmLanguage.json new file mode 100644 index 0000000..0e9b043 --- /dev/null +++ b/editor/vscode/syntaxes/dever.tmLanguage.json @@ -0,0 +1,95 @@ +{ + "name": "Dever", + "scopeName": "source.dever", + "patterns": [ + { "include": "#comments" }, + { "include": "#strings" }, + { "include": "#unsupported" }, + { "include": "#declarations" }, + { "include": "#constants" }, + { "include": "#numbers" }, + { "include": "#names" }, + { "include": "#keywords" }, + { "include": "#operators" } + ], + "repository": { + "comments": { + "name": "comment.line.number-sign.dever", + "begin": "#", + "end": "$" + }, + "strings": { + "name": "string.quoted.double.dever", + "begin": "\"", + "end": "\"|$", + "beginCaptures": { "0": { "name": "punctuation.definition.string.begin.dever" } }, + "endCaptures": { "0": { "name": "punctuation.definition.string.end.dever" } }, + "patterns": [ + { "name": "constant.character.escape.dever", "match": "\\\\(?:[\"\\\\nrt]|u\\{[0-9A-Fa-f]{1,6}\\})" }, + { "name": "invalid.illegal.escape.dever", "match": "\\\\." } + ] + }, + "declarations": { + "patterns": [ + { + "match": "\\b(type)\\s+([A-Z][A-Za-z0-9_]*)", + "captures": { + "1": { "name": "storage.type.dever" }, + "2": { "name": "entity.name.type.dever" } + } + }, + { + "match": "^\\s*(?:(public)\\s+)?(get|post|put|delete|cmd|rest)\\b", + "captures": { + "1": { "name": "storage.modifier.dever" }, + "2": { "name": "keyword.control.api.dever" } + } + }, + { + "match": "^\\s*(external)\\s+(command|exec|pip|npm|go)\\b", + "captures": { + "1": { "name": "storage.modifier.external.dever" }, + "2": { "name": "keyword.other.external.dever" } + } + }, + { "name": "keyword.other.declaration.dever", "match": "^\\s*(database|relation|seed|migrate|sql|index|unique|schedule)\\b" } + ] + }, + "constants": { + "name": "constant.language.dever", + "match": "\\b(true|false|null|other)\\b" + }, + "unsupported": { + "name": "invalid.illegal.keyword.dever", + "match": "\\b(package|exposes|if|else|switch|match|when|by|for|while|break|continue|iterate|recur|repeat|return|function|throw|catch|try|import|use|class|interface|unsafe|macro)\\b" + }, + "numbers": { + "name": "constant.numeric.dever", + "match": "(?=|[=+*/%<>?-]" }, + { "name": "punctuation.section.dever", "match": "[{}()\\[\\]]" }, + { "name": "punctuation.separator.dever", "match": "[,:.]" } + ] + } + } +} diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..a886dd4 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,80 @@ +# Dever 示例 + +入门项目见 [随包示例](../skills/references/examples.md)。CMS 演示多租户内容管理,包括登录、权限、文章修订、上传和定时发布。 + +CMS 只维护一份 [Dever 源码](cms/dever/module/news/article/api/admin/manage.dever),领域说明存放在 README 和 doc 中: + +```text +module/ + platform/tenant/ 平台租户 + user/account/ 凭据、成员、会话、bootstrap CMD + user/authorization/ 核心角色授权的业务入口 + content/category/ 分类 + content/revision/ 文章编辑与修订快照 + media/asset/ 上传、Storage Port/Adapter + publishing/schedule/ 定时发布与 Job + audit/operation/ 操作审计 + system/health/ 健康 CMD + news/article/ + app/editing.dever + app/publication.dever + api/admin/manage.dever + api/front/browse.dever + domain.dever + model.dever + model/publication.dever +test/ + content/category/lifecycle.dever + news/article/publish.dever + user/account/bootstrap.dever + user/account/validation.dever + user/account/lifecycle.dever +``` + +## 数据和权限 + +平台租户、账号、成员和会话使用 `global type`,保存在平台库。文章、分类、修订、媒体、排期和审计放在各租户自己的数据库。 + +账户、成员和会话同属账户领域,避免登录调用与 Model ID 引用形成循环依赖。租户文章的作者编号来自已验证身份,使用 Int 存储;物理外键只连接同一个数据库中的 Model,不能从租户库指向平台账户库。 + +verify 先核实身份数据,核心再按站点和角色检查接口权限,通过后才访问租户数据。 + +文章接口位于 `/news/article/admin/manage/`:create 创建,list/detail 查询,remove 删除草稿,publish 发布。 + +编辑使用 `/content/revision/admin/manage/create`,传入文章 ID、expected_version、标题正文和可空分类/媒体 ID。版本冲突返回 409,文章更新与修订快照一起提交。文章不用自动 REST 写接口,以免跳过这些规则;列表返回 `app.ArticleView`,不泄漏私有 Model。 + +定时发布使用 `/publishing/schedule/admin/manage/schedule`,传入文章 ID 和 publish_at。创建时检查作者和草稿状态;每个排期有独立任务,不会合并同时发布的文章。 + +执行任务前,核心再次核验身份和权限。发布、审计和排期状态一起提交或回滚,完成任务重复执行不重复写入。手动重复发布返回 409。 + +## 先检查,再运行 + +在仓库根目录执行: + +```bash +dever check examples/cms/dever +dever test examples/cms/dever --filter news.article.publish +dever fmt examples/cms/dever --check +``` + +测试使用临时 SQLite,不连接部署数据库。去掉 `--filter` 可执行全部应用测试。 + +实际部署前先替换 JWT secret,并把两个站点的 origin 和 Host 改成真实 HTTPS 入口;origin 是用户访问的外部地址,不是内部监听地址。 + +用 bootstrap CMD 建立平台租户、Owner 成员和密码: + +```sh +dever run examples/cms/dever -- user.account.bootstrap '{"tenant_key":"tenant-one","tenant_name":"Tenant One","email":"owner@example.com","display_name":"Owner","password":"change-me-now"}' +``` + +替换示例账号与密码,再用返回的真实 tenant_id 和 user_id 执行以下命令,不要猜测 ID 是 1: + +```text +dever tenant migrate examples/cms/dever +dever tenant owner examples/cms/dever admin +dever tenant owner examples/cms/dever front +``` + +这会迁移租户数据库,并建立后台、前台的核心 Owner。之后各站点通过 `user/authorization//manage` 管理普通角色。运行配置在项目的 `config/setting.json`,数据在 `data/`;`dever run examples/cms/dever` 不带 CMD 参数时启动 HTTP 服务。 + +应用测试覆盖分类、发布规则、账号和会话。真实登录、HTTP、租户隔离及发布流程使用 [CMS 验收入口](../test/performance/cms.py)。 diff --git a/examples/cms/dever/config/setting.json b/examples/cms/dever/config/setting.json new file mode 100644 index 0000000..115becb --- /dev/null +++ b/examples/cms/dever/config/setting.json @@ -0,0 +1,58 @@ +{ + "runtime": { + "mode": "all", + "shutdown_ms": 30000 + }, + "job": { + "workers": 1, + "poll_ms": 1000, + "lease_ms": 60000, + "retry_base_ms": 1000, + "retry_max_ms": 300000 + }, + "http": { + "host": "127.0.0.1", + "port": 8080 + }, + "database": { + "default": { + "type": "sqlite", + "path": "data/db/platform.db", + "tenant_directory": "data/tenants", + "max_connections": 4, + "max_page_size": 100 + } + }, + "tenant": { + "database": "default", + "max_pools": 32, + "idle_timeout_ms": 300000 + }, + "auth": { + "providers": { + "session": { + "verify": "user.account.verify", + "jwtSecret": "replace-this-example-secret-before-deployment-2026", + "ttlSeconds": 3600, + "cookie": "cms_session" + } + } + }, + "sites": { + "admin": { + "path": "admin", + "hosts": ["cms.example.com"], + "auth": "session", + "origin": "https://cms.example.com" + }, + "front": { + "path": "front", + "hosts": ["cms.example.com"], + "auth": "session", + "origin": "https://cms.example.com" + } + }, + "log": { + "level": "info" + } +} diff --git a/examples/cms/dever/data/.gitkeep b/examples/cms/dever/data/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/cms/dever/data/.gitkeep @@ -0,0 +1 @@ + diff --git a/examples/cms/dever/module/audit/operation/README.md b/examples/cms/dever/module/audit/operation/README.md new file mode 100644 index 0000000..901927f --- /dev/null +++ b/examples/cms/dever/module/audit/operation/README.md @@ -0,0 +1,7 @@ +# 操作审计 + +保存登录、退出、发布、上传和授权等操作记录。 + +- [操作审计应用](doc/app.md) +- [操作审计模型](doc/model.md) +- [调用流程](link.md) diff --git a/examples/cms/dever/module/audit/operation/app.dever b/examples/cms/dever/module/audit/operation/app.dever new file mode 100644 index 0000000..f50c645 --- /dev/null +++ b/examples/cms/dever/module/audit/operation/app.dever @@ -0,0 +1,10 @@ +record(kind: model.OperationKind, resource: Text, resource_id: Text) () { + stored = model.create( + { + kind = kind + resource = resource + resource_id = resource_id + } + ) + discarded = stored +} diff --git a/examples/cms/dever/module/audit/operation/doc/app.md b/examples/cms/dever/module/audit/operation/doc/app.md new file mode 100644 index 0000000..a9041c0 --- /dev/null +++ b/examples/cms/dever/module/audit/operation/doc/app.md @@ -0,0 +1,5 @@ +# 操作审计应用 + +## audit.operation.record + +保存一次操作审计,记录操作类型、资源名称和资源标识。 diff --git a/examples/cms/dever/module/audit/operation/doc/model.md b/examples/cms/dever/module/audit/operation/doc/model.md new file mode 100644 index 0000000..393d20a --- /dev/null +++ b/examples/cms/dever/module/audit/operation/doc/model.md @@ -0,0 +1,9 @@ +# 操作审计模型 + +## audit.operation.model.OperationKind + +审计操作类型:登录、退出、发布、上传和授权。 + +## audit.operation.model.Operation + +租户操作审计记录,包含操作者、操作类型和目标资源。 diff --git a/examples/cms/dever/module/audit/operation/link.md b/examples/cms/dever/module/audit/operation/link.md new file mode 100644 index 0000000..7d2efaa --- /dev/null +++ b/examples/cms/dever/module/audit/operation/link.md @@ -0,0 +1,60 @@ + + + +# audit.operation + +按求值顺序展示调用、分支和任务调度,包含 API 与自动 REST 入口。点击节点打开源码;双框为调用,虚线为调度或错误路径。外部库和驱动不展开。 + +图不是某次运行的日志,分句和 Adapter 按输入或配置选择。修改源码后运行 `dever inspect . --links`。 + +## 入口 + +没有已收录的入口。 + +## 外部调用方 + +| 能力 | 调用方 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [audit.operation.record](app.dever#L1) | [media.asset.record](../../media/asset/app.dever#L17) | 调用 | [../../media/asset/app.dever:26:3](../../media/asset/app.dever#L26,3) | +| [audit.operation.record](app.dever#L1) | [news.article.publishDraft](../../news/article/app/publication.dever#L48) | 调用 | [../../news/article/app/publication.dever:65:3](../../news/article/app/publication.dever#L65,3) | + +## 能力与内部调用 + +### audit.operation.model.Operation + +定义:[model.dever:9:1](model.dever#L9,1) + +没有已收录的下游调用。 + +### audit.operation.model.OperationKind + +定义:[model.dever:1:1](model.dever#L1,1) + +没有已收录的下游调用。 + +### audit.operation.record + +定义:[app.dever:1:1](app.dever#L1,1) + + + +```mermaid +flowchart TD + n0(["record"]) + click n0 href "app.dever#L1,1" "打开源码" + n1(["返回"]) + n3["audit.operation.model.create"] + click n3 href "app.dever#L2,12" "打开源码" + n4["赋值 stored"] + click n4 href "app.dever#L2,12" "打开源码" + n5["赋值 discarded"] + click n5 href "app.dever#L9,15" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n5 + n5 --> n1 +``` + +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [audit.operation.model.create](model.dever#L9) | 领域内 | 内建操作 | [app.dever:2:12](app.dever#L2,12) | diff --git a/examples/cms/dever/module/audit/operation/model.dever b/examples/cms/dever/module/audit/operation/model.dever new file mode 100644 index 0000000..c4472da --- /dev/null +++ b/examples/cms/dever/module/audit/operation/model.dever @@ -0,0 +1,14 @@ +type OperationKind { + Login = "登录" + Logout = "登出" + Publish = "发布" + Upload = "上传" + Access = "授权" +} + +type Operation { + actor_id: Int? + kind: OperationKind index + resource: Text(1, 120) + resource_id: Text(1, 120) +} diff --git a/examples/cms/dever/module/content/category/README.md b/examples/cms/dever/module/content/category/README.md new file mode 100644 index 0000000..c5d4130 --- /dev/null +++ b/examples/cms/dever/module/content/category/README.md @@ -0,0 +1,8 @@ +# 内容分类 + +创建分类并管理启用状态,供文章校验所选分类。 + +- [分类接口](doc/api-admin-manage.md) +- [内容分类应用](doc/app.md) +- [内容分类模型](doc/model.md) +- [调用流程](link.md) diff --git a/examples/cms/dever/module/content/category/api/admin/manage.dever b/examples/cms/dever/module/content/category/api/admin/manage.dever new file mode 100644 index 0000000..491e23b --- /dev/null +++ b/examples/cms/dever/module/content/category/api/admin/manage.dever @@ -0,0 +1,3 @@ +post create = app.create + +delete disable = app.disable diff --git a/examples/cms/dever/module/content/category/app.dever b/examples/cms/dever/module/content/category/app.dever new file mode 100644 index 0000000..8643b45 --- /dev/null +++ b/examples/cms/dever/module/content/category/app.dever @@ -0,0 +1,39 @@ +type CategoryView { + id: model.id + name: Text + slug: Text + status: Text +} + +create(name: Text) (category: CategoryView) { + stored = model.create( + { + name = name + slug = name + } + ) + category = CategoryView { + id = stored.id + name = stored.name + slug = stored.slug + status = "active" + } +} + +disable(id: model.id) (ok: Bool) { + changed = model.update( + id, + { + status = model.CategoryStatus.Disabled + } + ) + discarded = changed + ok = true +} + +requireActive(id: model.id) () { + stored = model.get(id) + dever.api.require(stored.status == model.CategoryStatus.Active, dever.api.Error.Conflict) +} + +requireActive(id: null) () {} diff --git a/examples/cms/dever/module/content/category/doc/api-admin-manage.md b/examples/cms/dever/module/content/category/doc/api-admin-manage.md new file mode 100644 index 0000000..36d8b4e --- /dev/null +++ b/examples/cms/dever/module/content/category/doc/api-admin-manage.md @@ -0,0 +1,9 @@ +# 分类接口 + +## content.category.api.admin.manage.create + +创建分类。 + +## content.category.api.admin.manage.disable + +禁用分类。 diff --git a/examples/cms/dever/module/content/category/doc/app.md b/examples/cms/dever/module/content/category/doc/app.md new file mode 100644 index 0000000..7a6e397 --- /dev/null +++ b/examples/cms/dever/module/content/category/doc/app.md @@ -0,0 +1,17 @@ +# 内容分类应用 + +## content.category.CategoryView + +分类接口返回的编号、名称、slug 和状态。 + +## content.category.create + +创建分类。名称去除首尾空白,slug 同时转为小写并保持唯一。 + +## content.category.disable + +把已有分类标为禁用,记录仍保留。 + +## content.category.requireActive + +分类编号为空时跳过;否则要求分类已启用,禁用时返回冲突。 diff --git a/examples/cms/dever/module/content/category/doc/model.md b/examples/cms/dever/module/content/category/doc/model.md new file mode 100644 index 0000000..c846800 --- /dev/null +++ b/examples/cms/dever/module/content/category/doc/model.md @@ -0,0 +1,9 @@ +# 内容分类模型 + +## content.category.model.CategoryStatus + +分类的启用或禁用状态。 + +## content.category.model.Category + +分类名称、唯一 slug 和状态。 diff --git a/examples/cms/dever/module/content/category/link.md b/examples/cms/dever/module/content/category/link.md new file mode 100644 index 0000000..5cb6e72 --- /dev/null +++ b/examples/cms/dever/module/content/category/link.md @@ -0,0 +1,402 @@ + + + +# content.category + +按求值顺序展示调用、分支和任务调度,包含 API 与自动 REST 入口。点击节点打开源码;双框为调用,虚线为调度或错误路径。外部库和驱动不展开。 + +图不是某次运行的日志,分句和 Adapter 按输入或配置选择。修改源码后运行 `dever inspect . --links`。 + +## 入口 + +- API [content.category.api.admin.manage.create](api/admin/manage.dever#L1) → [content.category.create](app.dever#L8) +- API [content.category.api.admin.manage.disable](api/admin/manage.dever#L3) → [content.category.disable](app.dever#L23) + +## 外部调用方 + +| 能力 | 调用方 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [content.category.requireActive](app.dever#L34) | [news.article.create](../../news/article/app/editing.dever#L16) | 调用 | [../../news/article/app/editing.dever:24:3](../../news/article/app/editing.dever#L24,3) | +| [content.category.requireActive](app.dever#L34) | [news.article.edit](../../news/article/app/editing.dever#L77) | 调用 | [../../news/article/app/editing.dever:89:3](../../news/article/app/editing.dever#L89,3) | +| [content.category.requireActive](app.dever#L34) | [news.article.publishDraft](../../news/article/app/publication.dever#L48) | 调用 | [../../news/article/app/publication.dever:51:3](../../news/article/app/publication.dever#L51,3) | + +## 能力与内部调用 + +### content.category.CategoryView + +定义:[app.dever:1:1](app.dever#L1,1) + +没有已收录的下游调用。 + +### content.category.api.admin.manage.create + +定义:[api/admin/manage.dever:1:1](api/admin/manage.dever#L1,1) + + + +```mermaid +flowchart TD + n0(["content.category.api.admin.manage.create"]) + click n0 href "api/admin/manage.dever#L1,15" "打开源码" + n1{"路由 POST /content/category/admin/manage/create,topic #34;admin/manage#34; 接受请求?"} + n2(["返回 HTTP 404"]) + n3["进入 API"] + n4["取得认证时间"] + n5(["返回 HTTP 500"]) + n6["按 topic #34;admin/manage#34; 选择站点与 Provider"] + n7(["返回 HTTP 403"]) + n8{"读取 Bearer 或站点 Cookie;两者互斥"} + n9(["返回 HTTP 401"]) + n10["认证上下文就绪"] + n11(["返回 HTTP 401"]) + n12["校验 Token 的签名、有效期及站点"] + n13{"Cookie 且非 GET / HEAD / OPTIONS?"} + n14["检查站点 Origin"] + n15(["返回 HTTP 403"]) + n16(["返回 HTTP 401"]) + n17{"按 Provider.verify 选择验证函数"} + n18[["user.account.verify"]] + click n18 href "api/admin/manage.dever#L1,15" "打开源码" + n19(["返回 HTTP 业务错误映射状态;其他故障 500"]) + n20["用验证结果建立身份"] + n21(["返回 HTTP 认证错误状态"]) + n22(["返回 HTTP 500"]) + n23["建立请求数据库上下文"] + n24{"有已验证租户?"} + n25["数据库上下文就绪"] + n26{"部署启用了租户存储?"} + n27(["返回 HTTP 403"]) + n28["按已验证租户选择数据库"] + n29(["返回 HTTP 503"]) + n30{"检查租户组件启用状态:content"} + n31(["返回 HTTP 403"]) + n32(["返回 HTTP 503"]) + n33["进入写入口事务"] + n34["失败:保留 HTTP 错误状态"] + n35["回滚入口拥有的事务"] + n36(["返回 HTTP 500"]) + n37(["返回错误响应"]) + n38{"权限检查:content.category.{site}.create"} + n39["权限通过"] + n40["读取 API 输入"] + n41["解码具体参数并检查字段合同"] + n42[["content.category.create"]] + click n42 href "api/admin/manage.dever#L1,15" "打开源码" + n43["按输出类型检查可序列化值并编码响应"] + n44["提交入口拥有的事务"] + n45(["返回 HTTP 500"]) + n46["发布暂存的认证 Header / Cookie"] + n47(["返回成功响应"]) + n0 --> n1 + n1 -->|"未匹配此入口:继续路由;均未匹配"| n2 + n1 --> n3 + n3 --> n4 + n4 -->|"失败"| n5 + n4 --> n6 + n6 -->|"无站点绑定"| n7 + n6 --> n8 + n8 -->|"凭据格式错误或冲突"| n9 + n8 -->|"无凭据"| n11 + n8 -->|"有凭据"| n13 + n13 -->|"否"| n12 + n13 -->|"是"| n14 + n14 -->|"不符合"| n15 + n14 -->|"通过"| n12 + n12 -->|"无 Provider 或 Token 无效"| n16 + n12 --> n17 + n17 -->|"user.account.verify"| n18 + n18 -->|"验证函数失败"| n19 + n18 --> n20 + n20 -->|"身份不合法"| n21 + n20 -->|"通过"| n10 + n17 -->|"无匹配验证函数"| n22 + n10 --> n23 + n23 --> n24 + n24 -->|"否"| n26 + n26 -->|"是:缺少租户身份"| n27 + n26 -->|"否"| n25 + n24 -->|"是"| n28 + n28 -->|"选择失败"| n29 + n28 --> n30 + n30 -->|"未启用"| n31 + n30 -->|"查询失败"| n32 + n30 -->|"允许"| n25 + n25 --> n33 + n34 --> n35 + n35 -->|"事务结束失败"| n36 + n35 --> n37 + n33 --> n38 + n38 -->|"拒绝:Access 状态;查询故障:500"| n34 + n38 -->|"允许"| n39 + n39 --> n40 + n40 -->|"失败:输入错误状态"| n34 + n40 --> n41 + n41 -->|"失败:400"| n34 + n41 --> n42 + n42 -->|"业务错误映射状态;其他故障:500"| n34 + n42 --> n43 + n43 -->|"编码失败:500"| n34 + n43 --> n44 + n44 -->|"提交失败"| n45 + n44 --> n46 + n46 --> n47 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.verify | [api/admin/manage.dever:1:15](api/admin/manage.dever#L1,15) | [user.account.verify](../../user/account/link.md#flow-aaaacb87d61f819c-1) | +| content.category.create | [api/admin/manage.dever:1:15](api/admin/manage.dever#L1,15) | [content.category.create](link.md#flow-dffb2069d02e287c-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [content.category.create](app.dever#L8) | 领域内 | API 绑定 | [api/admin/manage.dever:1:15](api/admin/manage.dever#L1,15) | +| [user.account.verify](../../user/account/app/account.dever#L133) | 跨领域 [user.account](../../user/account/link.md) | 身份验证 | [api/admin/manage.dever:1:15](api/admin/manage.dever#L1,15) | + +### content.category.api.admin.manage.disable + +定义:[api/admin/manage.dever:3:1](api/admin/manage.dever#L3,1) + + + +```mermaid +flowchart TD + n0(["content.category.api.admin.manage.disable"]) + click n0 href "api/admin/manage.dever#L3,18" "打开源码" + n1{"路由 DELETE /content/category/admin/manage/disable,topic #34;admin/manage#34; 接受请求?"} + n2(["返回 HTTP 404"]) + n3["进入 API"] + n4["取得认证时间"] + n5(["返回 HTTP 500"]) + n6["按 topic #34;admin/manage#34; 选择站点与 Provider"] + n7(["返回 HTTP 403"]) + n8{"读取 Bearer 或站点 Cookie;两者互斥"} + n9(["返回 HTTP 401"]) + n10["认证上下文就绪"] + n11(["返回 HTTP 401"]) + n12["校验 Token 的签名、有效期及站点"] + n13{"Cookie 且非 GET / HEAD / OPTIONS?"} + n14["检查站点 Origin"] + n15(["返回 HTTP 403"]) + n16(["返回 HTTP 401"]) + n17{"按 Provider.verify 选择验证函数"} + n18[["user.account.verify"]] + click n18 href "api/admin/manage.dever#L3,18" "打开源码" + n19(["返回 HTTP 业务错误映射状态;其他故障 500"]) + n20["用验证结果建立身份"] + n21(["返回 HTTP 认证错误状态"]) + n22(["返回 HTTP 500"]) + n23["建立请求数据库上下文"] + n24{"有已验证租户?"} + n25["数据库上下文就绪"] + n26{"部署启用了租户存储?"} + n27(["返回 HTTP 403"]) + n28["按已验证租户选择数据库"] + n29(["返回 HTTP 503"]) + n30{"检查租户组件启用状态:content"} + n31(["返回 HTTP 403"]) + n32(["返回 HTTP 503"]) + n33["进入写入口事务"] + n34["失败:保留 HTTP 错误状态"] + n35["回滚入口拥有的事务"] + n36(["返回 HTTP 500"]) + n37(["返回错误响应"]) + n38{"权限检查:content.category.{site}.disable"} + n39["权限通过"] + n40["读取 API 输入"] + n41["解码具体参数并检查字段合同"] + n42[["content.category.disable"]] + click n42 href "api/admin/manage.dever#L3,18" "打开源码" + n43["按输出类型检查可序列化值并编码响应"] + n44["提交入口拥有的事务"] + n45(["返回 HTTP 500"]) + n46["发布暂存的认证 Header / Cookie"] + n47(["返回成功响应"]) + n0 --> n1 + n1 -->|"未匹配此入口:继续路由;均未匹配"| n2 + n1 --> n3 + n3 --> n4 + n4 -->|"失败"| n5 + n4 --> n6 + n6 -->|"无站点绑定"| n7 + n6 --> n8 + n8 -->|"凭据格式错误或冲突"| n9 + n8 -->|"无凭据"| n11 + n8 -->|"有凭据"| n13 + n13 -->|"否"| n12 + n13 -->|"是"| n14 + n14 -->|"不符合"| n15 + n14 -->|"通过"| n12 + n12 -->|"无 Provider 或 Token 无效"| n16 + n12 --> n17 + n17 -->|"user.account.verify"| n18 + n18 -->|"验证函数失败"| n19 + n18 --> n20 + n20 -->|"身份不合法"| n21 + n20 -->|"通过"| n10 + n17 -->|"无匹配验证函数"| n22 + n10 --> n23 + n23 --> n24 + n24 -->|"否"| n26 + n26 -->|"是:缺少租户身份"| n27 + n26 -->|"否"| n25 + n24 -->|"是"| n28 + n28 -->|"选择失败"| n29 + n28 --> n30 + n30 -->|"未启用"| n31 + n30 -->|"查询失败"| n32 + n30 -->|"允许"| n25 + n25 --> n33 + n34 --> n35 + n35 -->|"事务结束失败"| n36 + n35 --> n37 + n33 --> n38 + n38 -->|"拒绝:Access 状态;查询故障:500"| n34 + n38 -->|"允许"| n39 + n39 --> n40 + n40 -->|"失败:输入错误状态"| n34 + n40 --> n41 + n41 -->|"失败:400"| n34 + n41 --> n42 + n42 -->|"业务错误映射状态;其他故障:500"| n34 + n42 --> n43 + n43 -->|"编码失败:500"| n34 + n43 --> n44 + n44 -->|"提交失败"| n45 + n44 --> n46 + n46 --> n47 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.verify | [api/admin/manage.dever:3:18](api/admin/manage.dever#L3,18) | [user.account.verify](../../user/account/link.md#flow-aaaacb87d61f819c-1) | +| content.category.disable | [api/admin/manage.dever:3:18](api/admin/manage.dever#L3,18) | [content.category.disable](link.md#flow-85101fdf961a71c3-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [content.category.disable](app.dever#L23) | 领域内 | API 绑定 | [api/admin/manage.dever:3:18](api/admin/manage.dever#L3,18) | +| [user.account.verify](../../user/account/app/account.dever#L133) | 跨领域 [user.account](../../user/account/link.md) | 身份验证 | [api/admin/manage.dever:3:18](api/admin/manage.dever#L3,18) | + +### content.category.create + +定义:[app.dever:8:1](app.dever#L8,1) + + + +```mermaid +flowchart TD + n0(["create"]) + click n0 href "app.dever#L8,1" "打开源码" + n1(["返回"]) + n3["content.category.model.create"] + click n3 href "app.dever#L9,12" "打开源码" + n4["赋值 stored"] + click n4 href "app.dever#L9,12" "打开源码" + n5["赋值 category"] + click n5 href "app.dever#L15,14" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n5 + n5 --> n1 +``` + +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [content.category.model.create](model.dever#L6) | 领域内 | 内建操作 | [app.dever:9:12](app.dever#L9,12) | + +### content.category.disable + +定义:[app.dever:23:1](app.dever#L23,1) + + + +```mermaid +flowchart TD + n0(["disable"]) + click n0 href "app.dever#L23,1" "打开源码" + n1(["返回"]) + n3["content.category.model.update"] + click n3 href "app.dever#L24,13" "打开源码" + n4["赋值 changed"] + click n4 href "app.dever#L24,13" "打开源码" + n5["赋值 discarded"] + click n5 href "app.dever#L30,15" "打开源码" + n6["赋值 ok"] + click n6 href "app.dever#L31,8" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n5 + n5 --> n6 + n6 --> n1 +``` + +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [content.category.model.update](model.dever#L6) | 领域内 | 内建操作 | [app.dever:24:13](app.dever#L24,13) | + +### content.category.model.Category + +定义:[model.dever:6:1](model.dever#L6,1) + +没有已收录的下游调用。 + +### content.category.model.CategoryStatus + +定义:[model.dever:1:1](model.dever#L1,1) + +没有已收录的下游调用。 + +### content.category.requireActive + +定义:[app.dever:34:1](app.dever#L34,1)、[app.dever:39:1](app.dever#L39,1) + + + +```mermaid +flowchart TD + n0(["requireActive"]) + click n0 href "app.dever#L34,1" "打开源码" + n1(["返回"]) + n3{"按实参匹配分句(互斥)"} + n4["id: model.id"] + click n4 href "app.dever#L34,1" "打开源码" + n5["content.category.model.get"] + click n5 href "app.dever#L35,12" "打开源码" + n6["赋值 stored"] + click n6 href "app.dever#L35,12" "打开源码" + n7[["dever.api.require"]] + click n7 href "app.dever#L36,3" "打开源码" + n8["id: null"] + click n8 href "app.dever#L39,1" "打开源码" + n0 --> n3 + n3 -->|"id: model.id"| n4 + n4 --> n5 + n5 --> n6 + n6 --> n7 + n7 --> n1 + n3 -->|"id: null"| n8 + n8 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| dever.api.require | [app.dever:36:3](app.dever#L36,3) | 依赖或生成的分派(本页不展开) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [content.category.model.get](model.dever#L6) | 领域内 | 内建操作 | [app.dever:35:12](app.dever#L35,12) | +| `dever.api.require` | 依赖 | 调用 | [app.dever:36:3](app.dever#L36,3) | diff --git a/examples/cms/dever/module/content/category/model.dever b/examples/cms/dever/module/content/category/model.dever new file mode 100644 index 0000000..f427555 --- /dev/null +++ b/examples/cms/dever/module/content/category/model.dever @@ -0,0 +1,10 @@ +type CategoryStatus { + Active = "启用" + Disabled = "禁用" +} + +type Category { + name: Text(1, 80) + slug: Text(1, 100) lower unique + status: CategoryStatus default CategoryStatus.Active index +} diff --git a/examples/cms/dever/module/content/revision/README.md b/examples/cms/dever/module/content/revision/README.md new file mode 100644 index 0000000..b58dd36 --- /dev/null +++ b/examples/cms/dever/module/content/revision/README.md @@ -0,0 +1,8 @@ +# 内容修订 + +在编辑文章的同一事务中保存版本快照。 + +- [修订接口](doc/api-admin-manage.md) +- [内容修订应用](doc/app.md) +- [内容修订模型](doc/model.md) +- [调用流程](link.md) diff --git a/examples/cms/dever/module/content/revision/api/admin/manage.dever b/examples/cms/dever/module/content/revision/api/admin/manage.dever new file mode 100644 index 0000000..5ea4a25 --- /dev/null +++ b/examples/cms/dever/module/content/revision/api/admin/manage.dever @@ -0,0 +1 @@ +post create = app.create diff --git a/examples/cms/dever/module/content/revision/app.dever b/examples/cms/dever/module/content/revision/app.dever new file mode 100644 index 0000000..c2ade52 --- /dev/null +++ b/examples/cms/dever/module/content/revision/app.dever @@ -0,0 +1,45 @@ +type RevisionView { + id: model.id + article_id: news.article.model.id + version: Int + title: Text + body: Text + status: Text +} + +transaction create( + selected_article_id: news.article.model.id, + expected_version: Int, + title: Text, + body: Text, + category_id: content.category.model.id?, + asset_id: media.asset.model.id? +) (revision: RevisionView) { + edited = news.article.edit( + selected_article_id, + expected_version, + title, + body, + category_id, + asset_id + ) + stored = model.create( + { + article_id = selected_article_id + version = edited.version + title = edited.title + body = edited.body + category_id = edited.category_id + asset_id = edited.asset_id + status = model.RevisionStatus.Draft + } + ) + revision = RevisionView { + id = stored.id + article_id = stored.article_id + version = stored.version + title = stored.title + body = stored.body + status = "draft" + } +} diff --git a/examples/cms/dever/module/content/revision/doc/api-admin-manage.md b/examples/cms/dever/module/content/revision/doc/api-admin-manage.md new file mode 100644 index 0000000..eda6ed7 --- /dev/null +++ b/examples/cms/dever/module/content/revision/doc/api-admin-manage.md @@ -0,0 +1,5 @@ +# 修订接口 + +## content.revision.api.admin.manage.create + +按版本编辑文章,并保存修订快照。 diff --git a/examples/cms/dever/module/content/revision/doc/app.md b/examples/cms/dever/module/content/revision/doc/app.md new file mode 100644 index 0000000..36434be --- /dev/null +++ b/examples/cms/dever/module/content/revision/doc/app.md @@ -0,0 +1,9 @@ +# 内容修订应用 + +## content.revision.RevisionView + +编辑后返回的修订编号、文章编号、版本及内容。 + +## content.revision.create + +按 expected_version 编辑文章并保存修订快照,两步在同一事务中完成。版本冲突、分类或媒体不可用时整次操作失败。 diff --git a/examples/cms/dever/module/content/revision/doc/model.md b/examples/cms/dever/module/content/revision/doc/model.md new file mode 100644 index 0000000..d70b9ea --- /dev/null +++ b/examples/cms/dever/module/content/revision/doc/model.md @@ -0,0 +1,9 @@ +# 内容修订模型 + +## content.revision.model.RevisionStatus + +修订的草稿或已发布状态。 + +## content.revision.model.Revision + +文章某一版本的内容快照;同一文章和版本只能保存一条。 diff --git a/examples/cms/dever/module/content/revision/link.md b/examples/cms/dever/module/content/revision/link.md new file mode 100644 index 0000000..e1a4e6e --- /dev/null +++ b/examples/cms/dever/module/content/revision/link.md @@ -0,0 +1,212 @@ + + + +# content.revision + +按求值顺序展示调用、分支和任务调度,包含 API 与自动 REST 入口。点击节点打开源码;双框为调用,虚线为调度或错误路径。外部库和驱动不展开。 + +图不是某次运行的日志,分句和 Adapter 按输入或配置选择。修改源码后运行 `dever inspect . --links`。 + +## 入口 + +- API [content.revision.api.admin.manage.create](api/admin/manage.dever#L1) → [content.revision.create](app.dever#L10) + +## 外部调用方 + +当前加载的源码中没有已收录的外部调用。 + +## 能力与内部调用 + +### content.revision.RevisionView + +定义:[app.dever:1:1](app.dever#L1,1) + +没有已收录的下游调用。 + +### content.revision.api.admin.manage.create + +定义:[api/admin/manage.dever:1:1](api/admin/manage.dever#L1,1) + + + +```mermaid +flowchart TD + n0(["content.revision.api.admin.manage.create"]) + click n0 href "api/admin/manage.dever#L1,15" "打开源码" + n1{"路由 POST /content/revision/admin/manage/create,topic #34;admin/manage#34; 接受请求?"} + n2(["返回 HTTP 404"]) + n3["进入 API"] + n4["取得认证时间"] + n5(["返回 HTTP 500"]) + n6["按 topic #34;admin/manage#34; 选择站点与 Provider"] + n7(["返回 HTTP 403"]) + n8{"读取 Bearer 或站点 Cookie;两者互斥"} + n9(["返回 HTTP 401"]) + n10["认证上下文就绪"] + n11(["返回 HTTP 401"]) + n12["校验 Token 的签名、有效期及站点"] + n13{"Cookie 且非 GET / HEAD / OPTIONS?"} + n14["检查站点 Origin"] + n15(["返回 HTTP 403"]) + n16(["返回 HTTP 401"]) + n17{"按 Provider.verify 选择验证函数"} + n18[["user.account.verify"]] + click n18 href "api/admin/manage.dever#L1,15" "打开源码" + n19(["返回 HTTP 业务错误映射状态;其他故障 500"]) + n20["用验证结果建立身份"] + n21(["返回 HTTP 认证错误状态"]) + n22(["返回 HTTP 500"]) + n23["建立请求数据库上下文"] + n24{"有已验证租户?"} + n25["数据库上下文就绪"] + n26{"部署启用了租户存储?"} + n27(["返回 HTTP 403"]) + n28["按已验证租户选择数据库"] + n29(["返回 HTTP 503"]) + n30{"检查租户组件启用状态:content, media, news"} + n31(["返回 HTTP 403"]) + n32(["返回 HTTP 503"]) + n33["进入写入口事务"] + n34["失败:保留 HTTP 错误状态"] + n35["回滚入口拥有的事务"] + n36(["返回 HTTP 500"]) + n37(["返回错误响应"]) + n38{"权限检查:content.revision.{site}.create"} + n39["权限通过"] + n40["读取 API 输入"] + n41["解码具体参数并检查字段合同"] + n42[["content.revision.create"]] + click n42 href "api/admin/manage.dever#L1,15" "打开源码" + n43["按输出类型检查可序列化值并编码响应"] + n44["提交入口拥有的事务"] + n45(["返回 HTTP 500"]) + n46["发布暂存的认证 Header / Cookie"] + n47(["返回成功响应"]) + n0 --> n1 + n1 -->|"未匹配此入口:继续路由;均未匹配"| n2 + n1 --> n3 + n3 --> n4 + n4 -->|"失败"| n5 + n4 --> n6 + n6 -->|"无站点绑定"| n7 + n6 --> n8 + n8 -->|"凭据格式错误或冲突"| n9 + n8 -->|"无凭据"| n11 + n8 -->|"有凭据"| n13 + n13 -->|"否"| n12 + n13 -->|"是"| n14 + n14 -->|"不符合"| n15 + n14 -->|"通过"| n12 + n12 -->|"无 Provider 或 Token 无效"| n16 + n12 --> n17 + n17 -->|"user.account.verify"| n18 + n18 -->|"验证函数失败"| n19 + n18 --> n20 + n20 -->|"身份不合法"| n21 + n20 -->|"通过"| n10 + n17 -->|"无匹配验证函数"| n22 + n10 --> n23 + n23 --> n24 + n24 -->|"否"| n26 + n26 -->|"是:缺少租户身份"| n27 + n26 -->|"否"| n25 + n24 -->|"是"| n28 + n28 -->|"选择失败"| n29 + n28 --> n30 + n30 -->|"未启用"| n31 + n30 -->|"查询失败"| n32 + n30 -->|"允许"| n25 + n25 --> n33 + n34 --> n35 + n35 -->|"事务结束失败"| n36 + n35 --> n37 + n33 --> n38 + n38 -->|"拒绝:Access 状态;查询故障:500"| n34 + n38 -->|"允许"| n39 + n39 --> n40 + n40 -->|"失败:输入错误状态"| n34 + n40 --> n41 + n41 -->|"失败:400"| n34 + n41 --> n42 + n42 -->|"业务错误映射状态;其他故障:500"| n34 + n42 --> n43 + n43 -->|"编码失败:500"| n34 + n43 --> n44 + n44 -->|"提交失败"| n45 + n44 --> n46 + n46 --> n47 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.verify | [api/admin/manage.dever:1:15](api/admin/manage.dever#L1,15) | [user.account.verify](../../user/account/link.md#flow-aaaacb87d61f819c-1) | +| content.revision.create | [api/admin/manage.dever:1:15](api/admin/manage.dever#L1,15) | [content.revision.create](link.md#flow-8c2beaa3a2e4fcf0-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [content.revision.create](app.dever#L10) | 领域内 | API 绑定 | [api/admin/manage.dever:1:15](api/admin/manage.dever#L1,15) | +| [user.account.verify](../../user/account/app/account.dever#L133) | 跨领域 [user.account](../../user/account/link.md) | 身份验证 | [api/admin/manage.dever:1:15](api/admin/manage.dever#L1,15) | + +### content.revision.create + +定义:[app.dever:10:1](app.dever#L10,1) + + + +```mermaid +flowchart TD + n0(["create"]) + click n0 href "app.dever#L10,1" "打开源码" + n1["开始事务"] + n2(["提交事务,返回"]) + n3(["回滚事务,传播错误"]) + n4[["news.article.edit"]] + click n4 href "app.dever#L18,12" "打开源码" + n5["赋值 edited"] + click n5 href "app.dever#L18,12" "打开源码" + n6["content.revision.model.create"] + click n6 href "app.dever#L26,12" "打开源码" + n7["赋值 stored"] + click n7 href "app.dever#L26,12" "打开源码" + n8["赋值 revision"] + click n8 href "app.dever#L37,14" "打开源码" + n0 --> n1 + n1 -.->|"事务体任一未捕获错误"| n3 + n1 --> n4 + n4 --> n5 + n5 --> n6 + n6 --> n7 + n7 --> n8 + n8 --> n2 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| news.article.edit | [app.dever:18:12](app.dever#L18,12) | [news.article.edit](../../news/article/link.md#flow-2c557350a87b85a1-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [news.article.edit](../../news/article/app/editing.dever#L77) | 跨领域 [news.article](../../news/article/link.md) | 调用 | [app.dever:18:12](app.dever#L18,12) | +| [content.revision.model.create](model.dever#L6) | 领域内 | 内建操作 | [app.dever:26:12](app.dever#L26,12) | + +### content.revision.model.Revision + +定义:[model.dever:6:1](model.dever#L6,1) + +没有已收录的下游调用。 + +### content.revision.model.RevisionStatus + +定义:[model.dever:1:1](model.dever#L1,1) + +没有已收录的下游调用。 diff --git a/examples/cms/dever/module/content/revision/model.dever b/examples/cms/dever/module/content/revision/model.dever new file mode 100644 index 0000000..76efa0f --- /dev/null +++ b/examples/cms/dever/module/content/revision/model.dever @@ -0,0 +1,16 @@ +type RevisionStatus { + Draft = "草稿" + Published = "已发布" +} + +type Revision { + article_id: news.article.model.id index + version: Int + title: Text(1, 160) + body: Text + category_id: content.category.model.id? + asset_id: media.asset.model.id? + status: RevisionStatus default RevisionStatus.Draft index +} + +unique(article_id, version) diff --git a/examples/cms/dever/module/media/asset/README.md b/examples/cms/dever/module/media/asset/README.md new file mode 100644 index 0000000..418749f --- /dev/null +++ b/examples/cms/dever/module/media/asset/README.md @@ -0,0 +1,10 @@ +# 媒体资源 + +保存上传文件及元数据,供文章引用可用媒体。 + +- [媒体本地适配器](doc/adapter.md) +- [上传接口](doc/api-admin-manage.md) +- [媒体资源应用](doc/app.md) +- [媒体资源模型](doc/model.md) +- [媒体存储端口](doc/port.md) +- [调用流程](link.md) diff --git a/examples/cms/dever/module/media/asset/adapter.dever b/examples/cms/dever/module/media/asset/adapter.dever new file mode 100644 index 0000000..0bb2c05 --- /dev/null +++ b/examples/cms/dever/module/media/asset/adapter.dever @@ -0,0 +1,3 @@ +port.store(file: Upload) (key: Uuid) { + key = dever.storage.put(file) +} diff --git a/examples/cms/dever/module/media/asset/api/admin/manage.dever b/examples/cms/dever/module/media/asset/api/admin/manage.dever new file mode 100644 index 0000000..537f49f --- /dev/null +++ b/examples/cms/dever/module/media/asset/api/admin/manage.dever @@ -0,0 +1 @@ +long post upload = app.store diff --git a/examples/cms/dever/module/media/asset/app.dever b/examples/cms/dever/module/media/asset/app.dever new file mode 100644 index 0000000..4843814 --- /dev/null +++ b/examples/cms/dever/module/media/asset/app.dever @@ -0,0 +1,41 @@ +type StoreResult { + id: model.id + storage_key: Uuid + filename: Text + content_type: Text + size: Int +} + +store(file: Upload) (result: StoreResult) { + filename = dever.api.uploadFilename(file) + content_type = dever.api.uploadContentType(file) + size = dever.api.uploadSize(file) + key = port.store(file) + result = record(key, filename, content_type, size) +} + +transaction record(key: Uuid, filename: Text, content_type: Text, size: Int) (result: StoreResult) { + asset = model.create( + { + storage_key = key + filename = filename + content_type = content_type + size = size + } + ) + audit.operation.record(audit.operation.model.OperationKind.Upload, "asset", filename) + result = StoreResult { + id = asset.id + storage_key = asset.storage_key + filename = asset.filename + content_type = asset.content_type + size = asset.size + } +} + +requireReady(id: model.id) () { + stored = model.get(id) + dever.api.require(stored.status == model.AssetStatus.Ready, dever.api.Error.Conflict) +} + +requireReady(id: null) () {} diff --git a/examples/cms/dever/module/media/asset/doc/adapter.md b/examples/cms/dever/module/media/asset/doc/adapter.md new file mode 100644 index 0000000..afe7b62 --- /dev/null +++ b/examples/cms/dever/module/media/asset/doc/adapter.md @@ -0,0 +1,5 @@ +# 媒体本地适配器 + +## media.asset.adapter.port.store + +使用本地 storage 保存上传内容,返回文件 Uuid。 diff --git a/examples/cms/dever/module/media/asset/doc/api-admin-manage.md b/examples/cms/dever/module/media/asset/doc/api-admin-manage.md new file mode 100644 index 0000000..5ac68d2 --- /dev/null +++ b/examples/cms/dever/module/media/asset/doc/api-admin-manage.md @@ -0,0 +1,5 @@ +# 上传接口 + +## media.asset.api.admin.manage.upload + +接收 multipart 文件上传,保存文件及资源元数据。 diff --git a/examples/cms/dever/module/media/asset/doc/app.md b/examples/cms/dever/module/media/asset/doc/app.md new file mode 100644 index 0000000..d8e33eb --- /dev/null +++ b/examples/cms/dever/module/media/asset/doc/app.md @@ -0,0 +1,17 @@ +# 媒体资源应用 + +## media.asset.StoreResult + +上传完成后返回资源编号、存储键、文件名、内容类型和字节数。 + +## media.asset.store + +读取上传元数据并保存文件,再写入资源记录。文件存储不占用数据库事务。 + +## media.asset.record + +在同一事务中写入资源元数据和上传审计。 + +## media.asset.requireReady + +媒体编号为空时跳过;否则要求资源处于 Ready 状态,不可用时返回冲突。 diff --git a/examples/cms/dever/module/media/asset/doc/model.md b/examples/cms/dever/module/media/asset/doc/model.md new file mode 100644 index 0000000..9d6588e --- /dev/null +++ b/examples/cms/dever/module/media/asset/doc/model.md @@ -0,0 +1,9 @@ +# 媒体资源模型 + +## media.asset.model.AssetStatus + +媒体资源的就绪或已删除状态。 + +## media.asset.model.Asset + +已存文件的唯一存储键、元数据及状态。 diff --git a/examples/cms/dever/module/media/asset/doc/port.md b/examples/cms/dever/module/media/asset/doc/port.md new file mode 100644 index 0000000..7125519 --- /dev/null +++ b/examples/cms/dever/module/media/asset/doc/port.md @@ -0,0 +1,5 @@ +# 媒体存储端口 + +## media.asset.port.store + +保存一次性上传内容,返回存储键;失败使用 dever.storage.PutResult。 diff --git a/examples/cms/dever/module/media/asset/link.md b/examples/cms/dever/module/media/asset/link.md new file mode 100644 index 0000000..650eb87 --- /dev/null +++ b/examples/cms/dever/module/media/asset/link.md @@ -0,0 +1,350 @@ + + + +# media.asset + +按求值顺序展示调用、分支和任务调度,包含 API 与自动 REST 入口。点击节点打开源码;双框为调用,虚线为调度或错误路径。外部库和驱动不展开。 + +图不是某次运行的日志,分句和 Adapter 按输入或配置选择。修改源码后运行 `dever inspect . --links`。 + +## 入口 + +- API [media.asset.api.admin.manage.upload](api/admin/manage.dever#L1) → [media.asset.store](app.dever#L9) + +## 外部调用方 + +| 能力 | 调用方 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [media.asset.requireReady](app.dever#L36) | [news.article.create](../../news/article/app/editing.dever#L16) | 调用 | [../../news/article/app/editing.dever:25:3](../../news/article/app/editing.dever#L25,3) | +| [media.asset.requireReady](app.dever#L36) | [news.article.edit](../../news/article/app/editing.dever#L77) | 调用 | [../../news/article/app/editing.dever:90:3](../../news/article/app/editing.dever#L90,3) | +| [media.asset.requireReady](app.dever#L36) | [news.article.publishDraft](../../news/article/app/publication.dever#L48) | 调用 | [../../news/article/app/publication.dever:52:3](../../news/article/app/publication.dever#L52,3) | + +## 能力与内部调用 + +### media.asset.StoreResult + +定义:[app.dever:1:1](app.dever#L1,1) + +没有已收录的下游调用。 + +### media.asset.adapter.port.store + +定义:[adapter.dever:1:1](adapter.dever#L1,1) + + + +```mermaid +flowchart TD + n0(["port.store"]) + click n0 href "adapter.dever#L1,1" "打开源码" + n1(["返回"]) + n3[["dever.storage.put"]] + click n3 href "adapter.dever#L2,9" "打开源码" + n4["赋值 key"] + click n4 href "adapter.dever#L2,9" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| dever.storage.put | [adapter.dever:2:9](adapter.dever#L2,9) | 依赖或生成的分派(本页不展开) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| `dever.storage.put` | 依赖 | 调用 | [adapter.dever:2:9](adapter.dever#L2,9) | + +### media.asset.api.admin.manage.upload + +定义:[api/admin/manage.dever:1:1](api/admin/manage.dever#L1,1) + + + +```mermaid +flowchart TD + n0(["media.asset.api.admin.manage.upload"]) + click n0 href "api/admin/manage.dever#L1,20" "打开源码" + n1{"路由 POST /media/asset/admin/manage/upload,topic #34;admin/manage#34; 接受请求?"} + n2(["返回 HTTP 404"]) + n3["进入 API"] + n4["取得认证时间"] + n5(["返回 HTTP 500"]) + n6["按 topic #34;admin/manage#34; 选择站点与 Provider"] + n7(["返回 HTTP 403"]) + n8{"读取 Bearer 或站点 Cookie;两者互斥"} + n9(["返回 HTTP 401"]) + n10["认证上下文就绪"] + n11(["返回 HTTP 401"]) + n12["校验 Token 的签名、有效期及站点"] + n13{"Cookie 且非 GET / HEAD / OPTIONS?"} + n14["检查站点 Origin"] + n15(["返回 HTTP 403"]) + n16(["返回 HTTP 401"]) + n17{"按 Provider.verify 选择验证函数"} + n18[["user.account.verify"]] + click n18 href "api/admin/manage.dever#L1,20" "打开源码" + n19(["返回 HTTP 业务错误映射状态;其他故障 500"]) + n20["用验证结果建立身份"] + n21(["返回 HTTP 认证错误状态"]) + n22(["返回 HTTP 500"]) + n23["建立请求数据库上下文"] + n24{"有已验证租户?"} + n25["数据库上下文就绪"] + n26{"部署启用了租户存储?"} + n27(["返回 HTTP 403"]) + n28["按已验证租户选择数据库"] + n29(["返回 HTTP 503"]) + n30{"检查租户组件启用状态:audit, media"} + n31(["返回 HTTP 403"]) + n32(["返回 HTTP 503"]) + n33["失败:保留 HTTP 错误状态"] + n34(["返回错误响应"]) + n35{"权限检查:media.asset.{site}.upload"} + n36["权限通过"] + n37["读取 multipart、取得 Upload 和字段"] + n38["解码具体参数并检查字段合同"] + n39[["media.asset.store"]] + click n39 href "api/admin/manage.dever#L1,20" "打开源码" + n40["按输出类型检查可序列化值并编码响应"] + n41["发布暂存的认证 Header / Cookie"] + n42(["返回成功响应"]) + n0 --> n1 + n1 -->|"未匹配此入口:继续路由;均未匹配"| n2 + n1 --> n3 + n3 --> n4 + n4 -->|"失败"| n5 + n4 --> n6 + n6 -->|"无站点绑定"| n7 + n6 --> n8 + n8 -->|"凭据格式错误或冲突"| n9 + n8 -->|"无凭据"| n11 + n8 -->|"有凭据"| n13 + n13 -->|"否"| n12 + n13 -->|"是"| n14 + n14 -->|"不符合"| n15 + n14 -->|"通过"| n12 + n12 -->|"无 Provider 或 Token 无效"| n16 + n12 --> n17 + n17 -->|"user.account.verify"| n18 + n18 -->|"验证函数失败"| n19 + n18 --> n20 + n20 -->|"身份不合法"| n21 + n20 -->|"通过"| n10 + n17 -->|"无匹配验证函数"| n22 + n10 --> n23 + n23 --> n24 + n24 -->|"否"| n26 + n26 -->|"是:缺少租户身份"| n27 + n26 -->|"否"| n25 + n24 -->|"是"| n28 + n28 -->|"选择失败"| n29 + n28 --> n30 + n30 -->|"未启用"| n31 + n30 -->|"查询失败"| n32 + n30 -->|"允许"| n25 + n33 --> n34 + n25 --> n35 + n35 -->|"拒绝:Access 状态;查询故障:500"| n33 + n35 -->|"允许"| n36 + n36 --> n37 + n37 -->|"失败:上传错误状态"| n33 + n37 --> n38 + n38 -->|"失败:400"| n33 + n38 --> n39 + n39 -->|"业务错误映射状态;其他故障:500"| n33 + n39 --> n40 + n40 -->|"编码失败:500"| n33 + n40 --> n41 + n41 --> n42 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.verify | [api/admin/manage.dever:1:20](api/admin/manage.dever#L1,20) | [user.account.verify](../../user/account/link.md#flow-aaaacb87d61f819c-1) | +| media.asset.store | [api/admin/manage.dever:1:20](api/admin/manage.dever#L1,20) | [media.asset.store](link.md#flow-9ce0fcefee26c58b-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [media.asset.store](app.dever#L9) | 领域内 | API 绑定 | [api/admin/manage.dever:1:20](api/admin/manage.dever#L1,20) | +| [user.account.verify](../../user/account/app/account.dever#L133) | 跨领域 [user.account](../../user/account/link.md) | 身份验证 | [api/admin/manage.dever:1:20](api/admin/manage.dever#L1,20) | + +### media.asset.model.Asset + +定义:[model.dever:6:1](model.dever#L6,1) + +没有已收录的下游调用。 + +### media.asset.model.AssetStatus + +定义:[model.dever:1:1](model.dever#L1,1) + +没有已收录的下游调用。 + +### media.asset.port.store + +定义:[port.dever:1:1](port.dever#L1,1) + +没有已收录的下游调用。 + +### media.asset.record + +定义:[app.dever:17:1](app.dever#L17,1) + + + +```mermaid +flowchart TD + n0(["record"]) + click n0 href "app.dever#L17,1" "打开源码" + n1["开始事务"] + n2(["提交事务,返回"]) + n3(["回滚事务,传播错误"]) + n4["media.asset.model.create"] + click n4 href "app.dever#L18,11" "打开源码" + n5["赋值 asset"] + click n5 href "app.dever#L18,11" "打开源码" + n6[["audit.operation.record"]] + click n6 href "app.dever#L26,3" "打开源码" + n7["赋值 result"] + click n7 href "app.dever#L27,12" "打开源码" + n0 --> n1 + n1 -.->|"事务体任一未捕获错误"| n3 + n1 --> n4 + n4 --> n5 + n5 --> n6 + n6 --> n7 + n7 --> n2 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| audit.operation.record | [app.dever:26:3](app.dever#L26,3) | [audit.operation.record](../../audit/operation/link.md#flow-c1586d946bef8bfb-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [media.asset.model.create](model.dever#L6) | 领域内 | 内建操作 | [app.dever:18:11](app.dever#L18,11) | +| [audit.operation.record](../../audit/operation/app.dever#L1) | 跨领域 [audit.operation](../../audit/operation/link.md) | 调用 | [app.dever:26:3](app.dever#L26,3) | + +### media.asset.requireReady + +定义:[app.dever:36:1](app.dever#L36,1)、[app.dever:41:1](app.dever#L41,1) + + + +```mermaid +flowchart TD + n0(["requireReady"]) + click n0 href "app.dever#L36,1" "打开源码" + n1(["返回"]) + n3{"按实参匹配分句(互斥)"} + n4["id: model.id"] + click n4 href "app.dever#L36,1" "打开源码" + n5["media.asset.model.get"] + click n5 href "app.dever#L37,12" "打开源码" + n6["赋值 stored"] + click n6 href "app.dever#L37,12" "打开源码" + n7[["dever.api.require"]] + click n7 href "app.dever#L38,3" "打开源码" + n8["id: null"] + click n8 href "app.dever#L41,1" "打开源码" + n0 --> n3 + n3 -->|"id: model.id"| n4 + n4 --> n5 + n5 --> n6 + n6 --> n7 + n7 --> n1 + n3 -->|"id: null"| n8 + n8 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| dever.api.require | [app.dever:38:3](app.dever#L38,3) | 依赖或生成的分派(本页不展开) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [media.asset.model.get](model.dever#L6) | 领域内 | 内建操作 | [app.dever:37:12](app.dever#L37,12) | +| `dever.api.require` | 依赖 | 调用 | [app.dever:38:3](app.dever#L38,3) | + +### media.asset.store + +定义:[app.dever:9:1](app.dever#L9,1) + + + +```mermaid +flowchart TD + n0(["store"]) + click n0 href "app.dever#L9,1" "打开源码" + n1(["返回"]) + n3["dever.api.uploadFilename"] + click n3 href "app.dever#L10,14" "打开源码" + n4["赋值 filename"] + click n4 href "app.dever#L10,14" "打开源码" + n5["dever.api.uploadContentType"] + click n5 href "app.dever#L11,18" "打开源码" + n6["赋值 content_type"] + click n6 href "app.dever#L11,18" "打开源码" + n7["dever.api.uploadSize"] + click n7 href "app.dever#L12,10" "打开源码" + n8["赋值 size"] + click n8 href "app.dever#L12,10" "打开源码" + n9[["media.asset.adapter.port.store"]] + click n9 href "app.dever#L13,9" "打开源码" + n10["赋值 key"] + click n10 href "app.dever#L13,9" "打开源码" + n11[["media.asset.record"]] + click n11 href "app.dever#L14,12" "打开源码" + n12["赋值 result"] + click n12 href "app.dever#L14,12" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n5 + n5 --> n6 + n6 --> n7 + n7 --> n8 + n8 --> n9 + n9 --> n10 + n10 --> n11 + n11 --> n12 + n12 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| media.asset.adapter.port.store | [app.dever:13:9](app.dever#L13,9) | [media.asset.adapter.port.store](link.md#flow-eb9faa6e4f022f76-1) | +| media.asset.record | [app.dever:14:12](app.dever#L14,12) | [media.asset.record](link.md#flow-b00da9b3a217d427-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| `dever.api.uploadFilename` | 内建操作 | 内建操作 | [app.dever:10:14](app.dever#L10,14) | +| `dever.api.uploadContentType` | 内建操作 | 内建操作 | [app.dever:11:18](app.dever#L11,18) | +| `dever.api.uploadSize` | 内建操作 | 内建操作 | [app.dever:12:10](app.dever#L12,10) | +| [media.asset.adapter.port.store](adapter.dever#L1) | 领域内 | 调用 | [app.dever:13:9](app.dever#L13,9) | +| [media.asset.record](app.dever#L17) | 领域内 | 调用 | [app.dever:14:12](app.dever#L14,12) | diff --git a/examples/cms/dever/module/media/asset/model.dever b/examples/cms/dever/module/media/asset/model.dever new file mode 100644 index 0000000..715867b --- /dev/null +++ b/examples/cms/dever/module/media/asset/model.dever @@ -0,0 +1,12 @@ +type AssetStatus { + Ready = "就绪" + Removed = "已删除" +} + +type Asset { + storage_key: Uuid unique + filename: Text(1, 255) + content_type: Text(1, 160) + size: Int + status: AssetStatus default AssetStatus.Ready index +} diff --git a/examples/cms/dever/module/media/asset/port.dever b/examples/cms/dever/module/media/asset/port.dever new file mode 100644 index 0000000..252efd2 --- /dev/null +++ b/examples/cms/dever/module/media/asset/port.dever @@ -0,0 +1 @@ +store(file: Upload) (key: Uuid) fails dever.storage.PutResult diff --git a/examples/cms/dever/module/news/article/README.md b/examples/cms/dever/module/news/article/README.md new file mode 100644 index 0000000..5d70391 --- /dev/null +++ b/examples/cms/dever/module/news/article/README.md @@ -0,0 +1,12 @@ +# 文章 + +管理作者草稿和版本,手动或定时发布为前台快照。 + +- [文章管理接口](doc/api-admin-manage.md) +- [文章浏览接口](doc/api-front-browse.md) +- [文章草稿](doc/app-editing.md) +- [文章发布](doc/app-publication.md) +- [文章规则](doc/domain.md) +- [文章公开读模型](doc/model-publication.md) +- [文章模型](doc/model.md) +- [调用流程](link.md) diff --git a/examples/cms/dever/module/news/article/api/admin/manage.dever b/examples/cms/dever/module/news/article/api/admin/manage.dever new file mode 100644 index 0000000..3ce98a5 --- /dev/null +++ b/examples/cms/dever/module/news/article/api/admin/manage.dever @@ -0,0 +1,9 @@ +get list = app.list + +get detail = app.detail + +post create = app.create + +delete remove = app.remove + +post publish = app.publish diff --git a/examples/cms/dever/module/news/article/api/front/browse.dever b/examples/cms/dever/module/news/article/api/front/browse.dever new file mode 100644 index 0000000..e310d48 --- /dev/null +++ b/examples/cms/dever/module/news/article/api/front/browse.dever @@ -0,0 +1,3 @@ +get list = app.published + +get detail = app.publishedDetail diff --git a/examples/cms/dever/module/news/article/app/editing.dever b/examples/cms/dever/module/news/article/app/editing.dever new file mode 100644 index 0000000..048fba6 --- /dev/null +++ b/examples/cms/dever/module/news/article/app/editing.dever @@ -0,0 +1,111 @@ +type ArticleView { + id: model.id + slug: Text + title: Text + body: Text + version: Int + category_id: content.category.model.id? + asset_id: media.asset.model.id? +} + +type ArticlePage { + rows: List + total: Int +} + +create( + slug: Text, + title: Text, + body: Text, + category_id: content.category.model.id?, + asset_id: media.asset.model.id? +) (article: ArticleView) { + dever.api.require(text.trim(body) != "", dever.api.Error.Invalid) + content.category.requireActive(category_id) + media.asset.requireReady(asset_id) + stored = model.create( + { + author_id = user.account.currentUser() + slug = slug + title = title + body = body + category_id = category_id + asset_id = asset_id + } + ) + article = detail(stored.id) +} + +list(size: Int) (articles: ArticlePage) { + author = user.account.currentUser() + articles = ArticlePage { + rows = model.ownedArticles(author, domain.pageSize(size)) + total = model.count( + { + where = author_id == author + } + ) + } +} + +detail(id: model.id) (article: ArticleView) { + stored = model.get(id) + dever.api.require(stored.author_id == user.account.currentUser(), dever.api.Error.Forbidden) + article = ArticleView { + id = stored.id + slug = stored.slug + title = stored.title + body = stored.body + version = stored.version + category_id = stored.category_id + asset_id = stored.asset_id + } +} + +remove(id: model.id) (removed: Int) { + stored = model.get(id) + dever.api.require(stored.author_id == user.account.currentUser(), dever.api.Error.Forbidden) + dever.api.require(stored.status == model.ArticleStatus.Draft, dever.api.Error.Conflict) + removed = model.delete( + { + where = id == stored.id and version == stored.version + } + ) + domain.requireUpdated(removed) +} + +edit( + id: model.id, + expected_version: Int, + title: Text, + body: Text, + category_id: content.category.model.id?, + asset_id: media.asset.model.id? +) (article: ArticleView) { + stored = model.get(id) + dever.api.require(stored.author_id == user.account.currentUser(), dever.api.Error.Forbidden) + dever.api.require(stored.status == model.ArticleStatus.Draft, dever.api.Error.Conflict) + dever.api.require(text.trim(body) != "", dever.api.Error.Invalid) + content.category.requireActive(category_id) + media.asset.requireReady(asset_id) + changed = model.update( + { + where = id == stored.id and version == expected_version and status == model.ArticleStatus.Draft + }, + { + title = title + body = body + category_id = category_id + asset_id = asset_id + version = expected_version + 1 + } + ) + domain.requireUpdated(changed) + article = detail(stored.id) +} + +requireSchedulable(article_id: model.id) () { + stored = model.get(article_id) + dever.api.require(stored.author_id == user.account.currentUser(), dever.api.Error.Forbidden) + dever.api.require(stored.status == model.ArticleStatus.Draft, dever.api.Error.Conflict) +} diff --git a/examples/cms/dever/module/news/article/app/publication.dever b/examples/cms/dever/module/news/article/app/publication.dever new file mode 100644 index 0000000..5275313 --- /dev/null +++ b/examples/cms/dever/module/news/article/app/publication.dever @@ -0,0 +1,95 @@ +transaction publish(article_id: model.id) (publication: model.publication.Publication) { + stored = model.get(article_id) + dever.api.require(stored.author_id == user.account.currentUser(), dever.api.Error.Forbidden) + dever.api.require(stored.status == model.ArticleStatus.Draft, dever.api.Error.Conflict) + publication = publishDraft(article_id) +} + +published(size: Int) (articles: model.publication.Page) { + articles = model.publication.list( + { + order = [published_at.desc, id.desc] + size = domain.pageSize(size) + } + ) +} + +publishedDetail(id: model.publication.id) (article: model.publication.Publication) { + article = model.publication.get(id) + dever.api.require(text.trim(article.body) != "", dever.api.Error.NotFound) +} + +transaction publishScheduled(article_id: model.id) ( + publication: model.publication.Publication +) { + stored = model.get(article_id) + publication = scheduledPublication(stored.status, article_id) +} + +scheduledPublication(status: model.ArticleStatus.Published, article_id: model.id) ( + publication: model.publication.Publication +) { + stored = model.get(article_id) + publication = existingPublication( + model.publication.first( + { + where = slug == stored.slug + } + ) + ) +} + +scheduledPublication(status: model.ArticleStatus.Draft, article_id: model.id) ( + publication: model.publication.Publication +) { + publication = publishDraft(article_id) +} + +publishDraft(article_id: model.id) (publication: model.publication.Publication) { + stored = model.get(article_id) + dever.api.require(stored.status == model.ArticleStatus.Draft, dever.api.Error.Conflict) + content.category.requireActive(stored.category_id) + media.asset.requireReady(stored.asset_id) + published_at = dever.time.now() + changed = model.update( + { + where = id == stored.id and version == stored.version and status == model.ArticleStatus.Draft + }, + { + status = model.ArticleStatus.Published + published_at = published_at + version = stored.version + 1 + } + ) + domain.requireUpdated(changed) + audit.operation.record( + audit.operation.model.OperationKind.Publish, + "article", + stored.slug + ) + publication = model.publication.upsert( + { + slug = stored.slug + }, + { + title = stored.title + body = stored.body + published_at = published_at + }, + { + title = stored.title + body = stored.body + published_at = published_at + } + ) +} + +existingPublication(stored: model.publication.Publication) ( + publication: model.publication.Publication +) { + publication = stored +} + +existingPublication(stored: null) (publication: model.publication.Publication) { + fail(dever.api.Error.NotFound) +} diff --git a/examples/cms/dever/module/news/article/doc/api-admin-manage.md b/examples/cms/dever/module/news/article/doc/api-admin-manage.md new file mode 100644 index 0000000..5fa8383 --- /dev/null +++ b/examples/cms/dever/module/news/article/doc/api-admin-manage.md @@ -0,0 +1,21 @@ +# 文章管理接口 + +## news.article.api.admin.manage.list + +读取当前作者的文章列表与总数。 + +## news.article.api.admin.manage.detail + +读取当前作者的文章详情。 + +## news.article.api.admin.manage.create + +创建当前作者的草稿。 + +## news.article.api.admin.manage.remove + +删除当前作者的草稿。 + +## news.article.api.admin.manage.publish + +发布当前作者的草稿。 diff --git a/examples/cms/dever/module/news/article/doc/api-front-browse.md b/examples/cms/dever/module/news/article/doc/api-front-browse.md new file mode 100644 index 0000000..1deffb4 --- /dev/null +++ b/examples/cms/dever/module/news/article/doc/api-front-browse.md @@ -0,0 +1,9 @@ +# 文章浏览接口 + +## news.article.api.front.browse.list + +读取当前租户的已发布文章。 + +## news.article.api.front.browse.detail + +读取已发布文章详情。 diff --git a/examples/cms/dever/module/news/article/doc/app-editing.md b/examples/cms/dever/module/news/article/doc/app-editing.md new file mode 100644 index 0000000..ea3c1fe --- /dev/null +++ b/examples/cms/dever/module/news/article/doc/app-editing.md @@ -0,0 +1,33 @@ +# 文章草稿 + +## news.article.ArticleView + +编辑接口返回的文章内容、版本、分类和媒体编号。 + +## news.article.ArticlePage + +当前作者的文章列表及总数。 + +## news.article.create + +为当前作者创建草稿。正文不能为空白,所选分类和媒体须可用;正文保留原文。 + +## news.article.list + +读取当前作者的最新文章和总数,size 必须为 1–100。 + +## news.article.detail + +读取当前作者的一篇文章,访问其他作者的文章返回禁止访问。 + +## news.article.remove + +删除当前作者的草稿,并检查版本未被并发修改。已发布文章不能删除。 + +## news.article.edit + +按 expected_version 修改当前作者的草稿,成功后版本加一。正文为空白或分类、媒体不可用时失败。 + +## news.article.requireSchedulable + +要求文章属于当前作者且仍为草稿,供创建发布计划使用。 diff --git a/examples/cms/dever/module/news/article/doc/app-publication.md b/examples/cms/dever/module/news/article/doc/app-publication.md new file mode 100644 index 0000000..fe8ef4c --- /dev/null +++ b/examples/cms/dever/module/news/article/doc/app-publication.md @@ -0,0 +1,29 @@ +# 文章发布 + +## news.article.publish + +发布当前作者的草稿;状态更新、审计和公开快照在同一事务中完成。 + +## news.article.published + +按发布时间和编号倒序读取发布快照,size 必须为 1–100。 + +## news.article.publishedDetail + +读取一份发布快照;正文为空白时返回不存在。 + +## news.article.publishScheduled + +执行定时发布。已发布文章复用现有快照,草稿进入发布流程。 + +## news.article.scheduledPublication + +根据文章状态选择复用快照或发布草稿。 + +## news.article.publishDraft + +检查草稿、分类和媒体,更新文章状态与版本,写入审计并保存发布快照;并发修改返回冲突。 + +## news.article.existingPublication + +取出已有发布快照;找不到时返回不存在。 diff --git a/examples/cms/dever/module/news/article/doc/domain.md b/examples/cms/dever/module/news/article/doc/domain.md new file mode 100644 index 0000000..0573771 --- /dev/null +++ b/examples/cms/dever/module/news/article/doc/domain.md @@ -0,0 +1,9 @@ +# 文章规则 + +## news.article.domain.pageSize + +只接受 1–100 的列表大小,其他值返回请求无效。 + +## news.article.domain.requireUpdated + +要求数据库恰好修改一条记录,否则返回冲突。 diff --git a/examples/cms/dever/module/news/article/doc/model-publication.md b/examples/cms/dever/module/news/article/doc/model-publication.md new file mode 100644 index 0000000..8d326a3 --- /dev/null +++ b/examples/cms/dever/module/news/article/doc/model-publication.md @@ -0,0 +1,5 @@ +# 文章公开读模型 + +## news.article.model.publication.Publication + +前台读取的发布快照,保存 slug、标题、正文和发布时间。 diff --git a/examples/cms/dever/module/news/article/doc/model.md b/examples/cms/dever/module/news/article/doc/model.md new file mode 100644 index 0000000..1c406ae --- /dev/null +++ b/examples/cms/dever/module/news/article/doc/model.md @@ -0,0 +1,9 @@ +# 文章模型 + +## news.article.model.ArticleStatus + +文章的草稿或已发布状态。 + +## news.article.model.Article + +作者所属的文章草稿和发布状态。版本、作者与发布状态不直接作为公开字段返回。 diff --git a/examples/cms/dever/module/news/article/domain.dever b/examples/cms/dever/module/news/article/domain.dever new file mode 100644 index 0000000..47cde7d --- /dev/null +++ b/examples/cms/dever/module/news/article/domain.dever @@ -0,0 +1,13 @@ +pageSize(value: Int >= 1 and <= 100) (size: Int) pure { + size = value +} + +pageSize(value: other) (size: Int) pure { + fail(dever.api.Error.Invalid) +} + +requireUpdated(changed: 1) () pure {} + +requireUpdated(changed: other) () pure { + fail(dever.api.Error.Conflict) +} diff --git a/examples/cms/dever/module/news/article/link.md b/examples/cms/dever/module/news/article/link.md new file mode 100644 index 0000000..372bc3b --- /dev/null +++ b/examples/cms/dever/module/news/article/link.md @@ -0,0 +1,1694 @@ + + + +# news.article + +按求值顺序展示调用、分支和任务调度,包含 API 与自动 REST 入口。点击节点打开源码;双框为调用,虚线为调度或错误路径。外部库和驱动不展开。 + +图不是某次运行的日志,分句和 Adapter 按输入或配置选择。修改源码后运行 `dever inspect . --links`。 + +## 入口 + +- API [news.article.api.admin.manage.create](api/admin/manage.dever#L5) → [news.article.create](app/editing.dever#L16) +- API [news.article.api.admin.manage.detail](api/admin/manage.dever#L3) → [news.article.detail](app/editing.dever#L51) +- API [news.article.api.admin.manage.list](api/admin/manage.dever#L1) → [news.article.list](app/editing.dever#L39) +- API [news.article.api.admin.manage.publish](api/admin/manage.dever#L9) → [news.article.publish](app/publication.dever#L1) +- API [news.article.api.admin.manage.remove](api/admin/manage.dever#L7) → [news.article.remove](app/editing.dever#L65) +- API [news.article.api.front.browse.detail](api/front/browse.dever#L3) → [news.article.publishedDetail](app/publication.dever#L17) +- API [news.article.api.front.browse.list](api/front/browse.dever#L1) → [news.article.published](app/publication.dever#L8) + +## 外部调用方 + +| 能力 | 调用方 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [news.article.edit](app/editing.dever#L77) | [content.revision.create](../../content/revision/app.dever#L10) | 调用 | [../../content/revision/app.dever:18:12](../../content/revision/app.dever#L18,12) | +| [news.article.publishScheduled](app/publication.dever#L22) | [publishing.schedule.publishClaimed](../../publishing/schedule/app.dever#L66) | 调用 | [../../publishing/schedule/app.dever:69:17](../../publishing/schedule/app.dever#L69,17) | +| [news.article.requireSchedulable](app/editing.dever#L107) | [publishing.schedule.create](../../publishing/schedule/app.dever#L12) | 调用 | [../../publishing/schedule/app.dever:15:3](../../publishing/schedule/app.dever#L15,3) | + +## 能力与内部调用 + +### news.article.ArticlePage + +定义:[app/editing.dever:11:1](app/editing.dever#L11,1) + +没有已收录的下游调用。 + +### news.article.ArticleView + +定义:[app/editing.dever:1:1](app/editing.dever#L1,1) + +没有已收录的下游调用。 + +### news.article.api.admin.manage.create + +定义:[api/admin/manage.dever:5:1](api/admin/manage.dever#L5,1) + + + +```mermaid +flowchart TD + n0(["news.article.api.admin.manage.create"]) + click n0 href "api/admin/manage.dever#L5,15" "打开源码" + n1{"路由 POST /news/article/admin/manage/create,topic #34;admin/manage#34; 接受请求?"} + n2(["返回 HTTP 404"]) + n3["进入 API"] + n4["取得认证时间"] + n5(["返回 HTTP 500"]) + n6["按 topic #34;admin/manage#34; 选择站点与 Provider"] + n7(["返回 HTTP 403"]) + n8{"读取 Bearer 或站点 Cookie;两者互斥"} + n9(["返回 HTTP 401"]) + n10["认证上下文就绪"] + n11(["返回 HTTP 401"]) + n12["校验 Token 的签名、有效期及站点"] + n13{"Cookie 且非 GET / HEAD / OPTIONS?"} + n14["检查站点 Origin"] + n15(["返回 HTTP 403"]) + n16(["返回 HTTP 401"]) + n17{"按 Provider.verify 选择验证函数"} + n18[["user.account.verify"]] + click n18 href "api/admin/manage.dever#L5,15" "打开源码" + n19(["返回 HTTP 业务错误映射状态;其他故障 500"]) + n20["用验证结果建立身份"] + n21(["返回 HTTP 认证错误状态"]) + n22(["返回 HTTP 500"]) + n23["建立请求数据库上下文"] + n24{"有已验证租户?"} + n25["数据库上下文就绪"] + n26{"部署启用了租户存储?"} + n27(["返回 HTTP 403"]) + n28["按已验证租户选择数据库"] + n29(["返回 HTTP 503"]) + n30{"检查租户组件启用状态:content, media, news"} + n31(["返回 HTTP 403"]) + n32(["返回 HTTP 503"]) + n33["进入写入口事务"] + n34["失败:保留 HTTP 错误状态"] + n35["回滚入口拥有的事务"] + n36(["返回 HTTP 500"]) + n37(["返回错误响应"]) + n38{"权限检查:news.article.{site}.create"} + n39["权限通过"] + n40["读取 API 输入"] + n41["解码具体参数并检查字段合同"] + n42[["news.article.create"]] + click n42 href "api/admin/manage.dever#L5,15" "打开源码" + n43["按输出类型检查可序列化值并编码响应"] + n44["提交入口拥有的事务"] + n45(["返回 HTTP 500"]) + n46["发布暂存的认证 Header / Cookie"] + n47(["返回成功响应"]) + n0 --> n1 + n1 -->|"未匹配此入口:继续路由;均未匹配"| n2 + n1 --> n3 + n3 --> n4 + n4 -->|"失败"| n5 + n4 --> n6 + n6 -->|"无站点绑定"| n7 + n6 --> n8 + n8 -->|"凭据格式错误或冲突"| n9 + n8 -->|"无凭据"| n11 + n8 -->|"有凭据"| n13 + n13 -->|"否"| n12 + n13 -->|"是"| n14 + n14 -->|"不符合"| n15 + n14 -->|"通过"| n12 + n12 -->|"无 Provider 或 Token 无效"| n16 + n12 --> n17 + n17 -->|"user.account.verify"| n18 + n18 -->|"验证函数失败"| n19 + n18 --> n20 + n20 -->|"身份不合法"| n21 + n20 -->|"通过"| n10 + n17 -->|"无匹配验证函数"| n22 + n10 --> n23 + n23 --> n24 + n24 -->|"否"| n26 + n26 -->|"是:缺少租户身份"| n27 + n26 -->|"否"| n25 + n24 -->|"是"| n28 + n28 -->|"选择失败"| n29 + n28 --> n30 + n30 -->|"未启用"| n31 + n30 -->|"查询失败"| n32 + n30 -->|"允许"| n25 + n25 --> n33 + n34 --> n35 + n35 -->|"事务结束失败"| n36 + n35 --> n37 + n33 --> n38 + n38 -->|"拒绝:Access 状态;查询故障:500"| n34 + n38 -->|"允许"| n39 + n39 --> n40 + n40 -->|"失败:输入错误状态"| n34 + n40 --> n41 + n41 -->|"失败:400"| n34 + n41 --> n42 + n42 -->|"业务错误映射状态;其他故障:500"| n34 + n42 --> n43 + n43 -->|"编码失败:500"| n34 + n43 --> n44 + n44 -->|"提交失败"| n45 + n44 --> n46 + n46 --> n47 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.verify | [api/admin/manage.dever:5:15](api/admin/manage.dever#L5,15) | [user.account.verify](../../user/account/link.md#flow-aaaacb87d61f819c-1) | +| news.article.create | [api/admin/manage.dever:5:15](api/admin/manage.dever#L5,15) | [news.article.create](link.md#flow-3c4f891cd8fd1dd3-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [news.article.create](app/editing.dever#L16) | 领域内 | API 绑定 | [api/admin/manage.dever:5:15](api/admin/manage.dever#L5,15) | +| [user.account.verify](../../user/account/app/account.dever#L133) | 跨领域 [user.account](../../user/account/link.md) | 身份验证 | [api/admin/manage.dever:5:15](api/admin/manage.dever#L5,15) | + +### news.article.api.admin.manage.detail + +定义:[api/admin/manage.dever:3:1](api/admin/manage.dever#L3,1) + + + +```mermaid +flowchart TD + n0(["news.article.api.admin.manage.detail"]) + click n0 href "api/admin/manage.dever#L3,14" "打开源码" + n1{"路由 GET /news/article/admin/manage/detail,topic #34;admin/manage#34; 接受请求?"} + n2(["返回 HTTP 404"]) + n3["进入 API"] + n4["取得认证时间"] + n5(["返回 HTTP 500"]) + n6["按 topic #34;admin/manage#34; 选择站点与 Provider"] + n7(["返回 HTTP 403"]) + n8{"读取 Bearer 或站点 Cookie;两者互斥"} + n9(["返回 HTTP 401"]) + n10["认证上下文就绪"] + n11(["返回 HTTP 401"]) + n12["校验 Token 的签名、有效期及站点"] + n13{"Cookie 且非 GET / HEAD / OPTIONS?"} + n14["检查站点 Origin"] + n15(["返回 HTTP 403"]) + n16(["返回 HTTP 401"]) + n17{"按 Provider.verify 选择验证函数"} + n18[["user.account.verify"]] + click n18 href "api/admin/manage.dever#L3,14" "打开源码" + n19(["返回 HTTP 业务错误映射状态;其他故障 500"]) + n20["用验证结果建立身份"] + n21(["返回 HTTP 认证错误状态"]) + n22(["返回 HTTP 500"]) + n23["建立请求数据库上下文"] + n24{"有已验证租户?"} + n25["数据库上下文就绪"] + n26{"部署启用了租户存储?"} + n27(["返回 HTTP 403"]) + n28["按已验证租户选择数据库"] + n29(["返回 HTTP 503"]) + n30{"检查租户组件启用状态:news"} + n31(["返回 HTTP 403"]) + n32(["返回 HTTP 503"]) + n33["失败:保留 HTTP 错误状态"] + n34(["返回错误响应"]) + n35{"权限检查:news.article.{site}.detail"} + n36["权限通过"] + n37["读取 API 输入"] + n38["解码具体参数并检查字段合同"] + n39[["news.article.detail"]] + click n39 href "api/admin/manage.dever#L3,14" "打开源码" + n40["按输出类型检查可序列化值并编码响应"] + n41["发布暂存的认证 Header / Cookie"] + n42(["返回成功响应"]) + n0 --> n1 + n1 -->|"未匹配此入口:继续路由;均未匹配"| n2 + n1 --> n3 + n3 --> n4 + n4 -->|"失败"| n5 + n4 --> n6 + n6 -->|"无站点绑定"| n7 + n6 --> n8 + n8 -->|"凭据格式错误或冲突"| n9 + n8 -->|"无凭据"| n11 + n8 -->|"有凭据"| n13 + n13 -->|"否"| n12 + n13 -->|"是"| n14 + n14 -->|"不符合"| n15 + n14 -->|"通过"| n12 + n12 -->|"无 Provider 或 Token 无效"| n16 + n12 --> n17 + n17 -->|"user.account.verify"| n18 + n18 -->|"验证函数失败"| n19 + n18 --> n20 + n20 -->|"身份不合法"| n21 + n20 -->|"通过"| n10 + n17 -->|"无匹配验证函数"| n22 + n10 --> n23 + n23 --> n24 + n24 -->|"否"| n26 + n26 -->|"是:缺少租户身份"| n27 + n26 -->|"否"| n25 + n24 -->|"是"| n28 + n28 -->|"选择失败"| n29 + n28 --> n30 + n30 -->|"未启用"| n31 + n30 -->|"查询失败"| n32 + n30 -->|"允许"| n25 + n33 --> n34 + n25 --> n35 + n35 -->|"拒绝:Access 状态;查询故障:500"| n33 + n35 -->|"允许"| n36 + n36 --> n37 + n37 -->|"失败:输入错误状态"| n33 + n37 --> n38 + n38 -->|"失败:400"| n33 + n38 --> n39 + n39 -->|"业务错误映射状态;其他故障:500"| n33 + n39 --> n40 + n40 -->|"编码失败:500"| n33 + n40 --> n41 + n41 --> n42 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.verify | [api/admin/manage.dever:3:14](api/admin/manage.dever#L3,14) | [user.account.verify](../../user/account/link.md#flow-aaaacb87d61f819c-1) | +| news.article.detail | [api/admin/manage.dever:3:14](api/admin/manage.dever#L3,14) | [news.article.detail](link.md#flow-15251ffe01bb516b-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [news.article.detail](app/editing.dever#L51) | 领域内 | API 绑定 | [api/admin/manage.dever:3:14](api/admin/manage.dever#L3,14) | +| [user.account.verify](../../user/account/app/account.dever#L133) | 跨领域 [user.account](../../user/account/link.md) | 身份验证 | [api/admin/manage.dever:3:14](api/admin/manage.dever#L3,14) | + +### news.article.api.admin.manage.list + +定义:[api/admin/manage.dever:1:1](api/admin/manage.dever#L1,1) + + + +```mermaid +flowchart TD + n0(["news.article.api.admin.manage.list"]) + click n0 href "api/admin/manage.dever#L1,12" "打开源码" + n1{"路由 GET /news/article/admin/manage/list,topic #34;admin/manage#34; 接受请求?"} + n2(["返回 HTTP 404"]) + n3["进入 API"] + n4["取得认证时间"] + n5(["返回 HTTP 500"]) + n6["按 topic #34;admin/manage#34; 选择站点与 Provider"] + n7(["返回 HTTP 403"]) + n8{"读取 Bearer 或站点 Cookie;两者互斥"} + n9(["返回 HTTP 401"]) + n10["认证上下文就绪"] + n11(["返回 HTTP 401"]) + n12["校验 Token 的签名、有效期及站点"] + n13{"Cookie 且非 GET / HEAD / OPTIONS?"} + n14["检查站点 Origin"] + n15(["返回 HTTP 403"]) + n16(["返回 HTTP 401"]) + n17{"按 Provider.verify 选择验证函数"} + n18[["user.account.verify"]] + click n18 href "api/admin/manage.dever#L1,12" "打开源码" + n19(["返回 HTTP 业务错误映射状态;其他故障 500"]) + n20["用验证结果建立身份"] + n21(["返回 HTTP 认证错误状态"]) + n22(["返回 HTTP 500"]) + n23["建立请求数据库上下文"] + n24{"有已验证租户?"} + n25["数据库上下文就绪"] + n26{"部署启用了租户存储?"} + n27(["返回 HTTP 403"]) + n28["按已验证租户选择数据库"] + n29(["返回 HTTP 503"]) + n30{"检查租户组件启用状态:news"} + n31(["返回 HTTP 403"]) + n32(["返回 HTTP 503"]) + n33["失败:保留 HTTP 错误状态"] + n34(["返回错误响应"]) + n35{"权限检查:news.article.{site}.list"} + n36["权限通过"] + n37["读取 API 输入"] + n38["解码具体参数并检查字段合同"] + n39[["news.article.list"]] + click n39 href "api/admin/manage.dever#L1,12" "打开源码" + n40["按输出类型检查可序列化值并编码响应"] + n41["发布暂存的认证 Header / Cookie"] + n42(["返回成功响应"]) + n0 --> n1 + n1 -->|"未匹配此入口:继续路由;均未匹配"| n2 + n1 --> n3 + n3 --> n4 + n4 -->|"失败"| n5 + n4 --> n6 + n6 -->|"无站点绑定"| n7 + n6 --> n8 + n8 -->|"凭据格式错误或冲突"| n9 + n8 -->|"无凭据"| n11 + n8 -->|"有凭据"| n13 + n13 -->|"否"| n12 + n13 -->|"是"| n14 + n14 -->|"不符合"| n15 + n14 -->|"通过"| n12 + n12 -->|"无 Provider 或 Token 无效"| n16 + n12 --> n17 + n17 -->|"user.account.verify"| n18 + n18 -->|"验证函数失败"| n19 + n18 --> n20 + n20 -->|"身份不合法"| n21 + n20 -->|"通过"| n10 + n17 -->|"无匹配验证函数"| n22 + n10 --> n23 + n23 --> n24 + n24 -->|"否"| n26 + n26 -->|"是:缺少租户身份"| n27 + n26 -->|"否"| n25 + n24 -->|"是"| n28 + n28 -->|"选择失败"| n29 + n28 --> n30 + n30 -->|"未启用"| n31 + n30 -->|"查询失败"| n32 + n30 -->|"允许"| n25 + n33 --> n34 + n25 --> n35 + n35 -->|"拒绝:Access 状态;查询故障:500"| n33 + n35 -->|"允许"| n36 + n36 --> n37 + n37 -->|"失败:输入错误状态"| n33 + n37 --> n38 + n38 -->|"失败:400"| n33 + n38 --> n39 + n39 -->|"业务错误映射状态;其他故障:500"| n33 + n39 --> n40 + n40 -->|"编码失败:500"| n33 + n40 --> n41 + n41 --> n42 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.verify | [api/admin/manage.dever:1:12](api/admin/manage.dever#L1,12) | [user.account.verify](../../user/account/link.md#flow-aaaacb87d61f819c-1) | +| news.article.list | [api/admin/manage.dever:1:12](api/admin/manage.dever#L1,12) | [news.article.list](link.md#flow-a6bf82421ba64e15-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [news.article.list](app/editing.dever#L39) | 领域内 | API 绑定 | [api/admin/manage.dever:1:12](api/admin/manage.dever#L1,12) | +| [user.account.verify](../../user/account/app/account.dever#L133) | 跨领域 [user.account](../../user/account/link.md) | 身份验证 | [api/admin/manage.dever:1:12](api/admin/manage.dever#L1,12) | + +### news.article.api.admin.manage.publish + +定义:[api/admin/manage.dever:9:1](api/admin/manage.dever#L9,1) + + + +```mermaid +flowchart TD + n0(["news.article.api.admin.manage.publish"]) + click n0 href "api/admin/manage.dever#L9,16" "打开源码" + n1{"路由 POST /news/article/admin/manage/publish,topic #34;admin/manage#34; 接受请求?"} + n2(["返回 HTTP 404"]) + n3["进入 API"] + n4["取得认证时间"] + n5(["返回 HTTP 500"]) + n6["按 topic #34;admin/manage#34; 选择站点与 Provider"] + n7(["返回 HTTP 403"]) + n8{"读取 Bearer 或站点 Cookie;两者互斥"} + n9(["返回 HTTP 401"]) + n10["认证上下文就绪"] + n11(["返回 HTTP 401"]) + n12["校验 Token 的签名、有效期及站点"] + n13{"Cookie 且非 GET / HEAD / OPTIONS?"} + n14["检查站点 Origin"] + n15(["返回 HTTP 403"]) + n16(["返回 HTTP 401"]) + n17{"按 Provider.verify 选择验证函数"} + n18[["user.account.verify"]] + click n18 href "api/admin/manage.dever#L9,16" "打开源码" + n19(["返回 HTTP 业务错误映射状态;其他故障 500"]) + n20["用验证结果建立身份"] + n21(["返回 HTTP 认证错误状态"]) + n22(["返回 HTTP 500"]) + n23["建立请求数据库上下文"] + n24{"有已验证租户?"} + n25["数据库上下文就绪"] + n26{"部署启用了租户存储?"} + n27(["返回 HTTP 403"]) + n28["按已验证租户选择数据库"] + n29(["返回 HTTP 503"]) + n30{"检查租户组件启用状态:audit, content, media, news"} + n31(["返回 HTTP 403"]) + n32(["返回 HTTP 503"]) + n33["进入写入口事务"] + n34["失败:保留 HTTP 错误状态"] + n35["回滚入口拥有的事务"] + n36(["返回 HTTP 500"]) + n37(["返回错误响应"]) + n38{"权限检查:news.article.{site}.publish"} + n39["权限通过"] + n40["读取 API 输入"] + n41["解码具体参数并检查字段合同"] + n42[["news.article.publish"]] + click n42 href "api/admin/manage.dever#L9,16" "打开源码" + n43["按输出类型检查可序列化值并编码响应"] + n44["提交入口拥有的事务"] + n45(["返回 HTTP 500"]) + n46["发布暂存的认证 Header / Cookie"] + n47(["返回成功响应"]) + n0 --> n1 + n1 -->|"未匹配此入口:继续路由;均未匹配"| n2 + n1 --> n3 + n3 --> n4 + n4 -->|"失败"| n5 + n4 --> n6 + n6 -->|"无站点绑定"| n7 + n6 --> n8 + n8 -->|"凭据格式错误或冲突"| n9 + n8 -->|"无凭据"| n11 + n8 -->|"有凭据"| n13 + n13 -->|"否"| n12 + n13 -->|"是"| n14 + n14 -->|"不符合"| n15 + n14 -->|"通过"| n12 + n12 -->|"无 Provider 或 Token 无效"| n16 + n12 --> n17 + n17 -->|"user.account.verify"| n18 + n18 -->|"验证函数失败"| n19 + n18 --> n20 + n20 -->|"身份不合法"| n21 + n20 -->|"通过"| n10 + n17 -->|"无匹配验证函数"| n22 + n10 --> n23 + n23 --> n24 + n24 -->|"否"| n26 + n26 -->|"是:缺少租户身份"| n27 + n26 -->|"否"| n25 + n24 -->|"是"| n28 + n28 -->|"选择失败"| n29 + n28 --> n30 + n30 -->|"未启用"| n31 + n30 -->|"查询失败"| n32 + n30 -->|"允许"| n25 + n25 --> n33 + n34 --> n35 + n35 -->|"事务结束失败"| n36 + n35 --> n37 + n33 --> n38 + n38 -->|"拒绝:Access 状态;查询故障:500"| n34 + n38 -->|"允许"| n39 + n39 --> n40 + n40 -->|"失败:输入错误状态"| n34 + n40 --> n41 + n41 -->|"失败:400"| n34 + n41 --> n42 + n42 -->|"业务错误映射状态;其他故障:500"| n34 + n42 --> n43 + n43 -->|"编码失败:500"| n34 + n43 --> n44 + n44 -->|"提交失败"| n45 + n44 --> n46 + n46 --> n47 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.verify | [api/admin/manage.dever:9:16](api/admin/manage.dever#L9,16) | [user.account.verify](../../user/account/link.md#flow-aaaacb87d61f819c-1) | +| news.article.publish | [api/admin/manage.dever:9:16](api/admin/manage.dever#L9,16) | [news.article.publish](link.md#flow-d6183a8c2ad23235-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [news.article.publish](app/publication.dever#L1) | 领域内 | API 绑定 | [api/admin/manage.dever:9:16](api/admin/manage.dever#L9,16) | +| [user.account.verify](../../user/account/app/account.dever#L133) | 跨领域 [user.account](../../user/account/link.md) | 身份验证 | [api/admin/manage.dever:9:16](api/admin/manage.dever#L9,16) | + +### news.article.api.admin.manage.remove + +定义:[api/admin/manage.dever:7:1](api/admin/manage.dever#L7,1) + + + +```mermaid +flowchart TD + n0(["news.article.api.admin.manage.remove"]) + click n0 href "api/admin/manage.dever#L7,17" "打开源码" + n1{"路由 DELETE /news/article/admin/manage/remove,topic #34;admin/manage#34; 接受请求?"} + n2(["返回 HTTP 404"]) + n3["进入 API"] + n4["取得认证时间"] + n5(["返回 HTTP 500"]) + n6["按 topic #34;admin/manage#34; 选择站点与 Provider"] + n7(["返回 HTTP 403"]) + n8{"读取 Bearer 或站点 Cookie;两者互斥"} + n9(["返回 HTTP 401"]) + n10["认证上下文就绪"] + n11(["返回 HTTP 401"]) + n12["校验 Token 的签名、有效期及站点"] + n13{"Cookie 且非 GET / HEAD / OPTIONS?"} + n14["检查站点 Origin"] + n15(["返回 HTTP 403"]) + n16(["返回 HTTP 401"]) + n17{"按 Provider.verify 选择验证函数"} + n18[["user.account.verify"]] + click n18 href "api/admin/manage.dever#L7,17" "打开源码" + n19(["返回 HTTP 业务错误映射状态;其他故障 500"]) + n20["用验证结果建立身份"] + n21(["返回 HTTP 认证错误状态"]) + n22(["返回 HTTP 500"]) + n23["建立请求数据库上下文"] + n24{"有已验证租户?"} + n25["数据库上下文就绪"] + n26{"部署启用了租户存储?"} + n27(["返回 HTTP 403"]) + n28["按已验证租户选择数据库"] + n29(["返回 HTTP 503"]) + n30{"检查租户组件启用状态:news"} + n31(["返回 HTTP 403"]) + n32(["返回 HTTP 503"]) + n33["进入写入口事务"] + n34["失败:保留 HTTP 错误状态"] + n35["回滚入口拥有的事务"] + n36(["返回 HTTP 500"]) + n37(["返回错误响应"]) + n38{"权限检查:news.article.{site}.remove"} + n39["权限通过"] + n40["读取 API 输入"] + n41["解码具体参数并检查字段合同"] + n42[["news.article.remove"]] + click n42 href "api/admin/manage.dever#L7,17" "打开源码" + n43["按输出类型检查可序列化值并编码响应"] + n44["提交入口拥有的事务"] + n45(["返回 HTTP 500"]) + n46["发布暂存的认证 Header / Cookie"] + n47(["返回成功响应"]) + n0 --> n1 + n1 -->|"未匹配此入口:继续路由;均未匹配"| n2 + n1 --> n3 + n3 --> n4 + n4 -->|"失败"| n5 + n4 --> n6 + n6 -->|"无站点绑定"| n7 + n6 --> n8 + n8 -->|"凭据格式错误或冲突"| n9 + n8 -->|"无凭据"| n11 + n8 -->|"有凭据"| n13 + n13 -->|"否"| n12 + n13 -->|"是"| n14 + n14 -->|"不符合"| n15 + n14 -->|"通过"| n12 + n12 -->|"无 Provider 或 Token 无效"| n16 + n12 --> n17 + n17 -->|"user.account.verify"| n18 + n18 -->|"验证函数失败"| n19 + n18 --> n20 + n20 -->|"身份不合法"| n21 + n20 -->|"通过"| n10 + n17 -->|"无匹配验证函数"| n22 + n10 --> n23 + n23 --> n24 + n24 -->|"否"| n26 + n26 -->|"是:缺少租户身份"| n27 + n26 -->|"否"| n25 + n24 -->|"是"| n28 + n28 -->|"选择失败"| n29 + n28 --> n30 + n30 -->|"未启用"| n31 + n30 -->|"查询失败"| n32 + n30 -->|"允许"| n25 + n25 --> n33 + n34 --> n35 + n35 -->|"事务结束失败"| n36 + n35 --> n37 + n33 --> n38 + n38 -->|"拒绝:Access 状态;查询故障:500"| n34 + n38 -->|"允许"| n39 + n39 --> n40 + n40 -->|"失败:输入错误状态"| n34 + n40 --> n41 + n41 -->|"失败:400"| n34 + n41 --> n42 + n42 -->|"业务错误映射状态;其他故障:500"| n34 + n42 --> n43 + n43 -->|"编码失败:500"| n34 + n43 --> n44 + n44 -->|"提交失败"| n45 + n44 --> n46 + n46 --> n47 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.verify | [api/admin/manage.dever:7:17](api/admin/manage.dever#L7,17) | [user.account.verify](../../user/account/link.md#flow-aaaacb87d61f819c-1) | +| news.article.remove | [api/admin/manage.dever:7:17](api/admin/manage.dever#L7,17) | [news.article.remove](link.md#flow-2fa8af0a8d73375d-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [news.article.remove](app/editing.dever#L65) | 领域内 | API 绑定 | [api/admin/manage.dever:7:17](api/admin/manage.dever#L7,17) | +| [user.account.verify](../../user/account/app/account.dever#L133) | 跨领域 [user.account](../../user/account/link.md) | 身份验证 | [api/admin/manage.dever:7:17](api/admin/manage.dever#L7,17) | + +### news.article.api.front.browse.detail + +定义:[api/front/browse.dever:3:1](api/front/browse.dever#L3,1) + + + +```mermaid +flowchart TD + n0(["news.article.api.front.browse.detail"]) + click n0 href "api/front/browse.dever#L3,14" "打开源码" + n1{"路由 GET /news/article/front/browse/detail,topic #34;front/browse#34; 接受请求?"} + n2(["返回 HTTP 404"]) + n3["进入 API"] + n4["取得认证时间"] + n5(["返回 HTTP 500"]) + n6["按 topic #34;front/browse#34; 选择站点与 Provider"] + n7(["返回 HTTP 403"]) + n8{"读取 Bearer 或站点 Cookie;两者互斥"} + n9(["返回 HTTP 401"]) + n10["认证上下文就绪"] + n11(["返回 HTTP 401"]) + n12["校验 Token 的签名、有效期及站点"] + n13{"Cookie 且非 GET / HEAD / OPTIONS?"} + n14["检查站点 Origin"] + n15(["返回 HTTP 403"]) + n16(["返回 HTTP 401"]) + n17{"按 Provider.verify 选择验证函数"} + n18[["user.account.verify"]] + click n18 href "api/front/browse.dever#L3,14" "打开源码" + n19(["返回 HTTP 业务错误映射状态;其他故障 500"]) + n20["用验证结果建立身份"] + n21(["返回 HTTP 认证错误状态"]) + n22(["返回 HTTP 500"]) + n23["建立请求数据库上下文"] + n24{"有已验证租户?"} + n25["数据库上下文就绪"] + n26{"部署启用了租户存储?"} + n27(["返回 HTTP 403"]) + n28["按已验证租户选择数据库"] + n29(["返回 HTTP 503"]) + n30{"检查租户组件启用状态:news"} + n31(["返回 HTTP 403"]) + n32(["返回 HTTP 503"]) + n33["失败:保留 HTTP 错误状态"] + n34(["返回错误响应"]) + n35{"权限检查:news.article.{site}.detail"} + n36["权限通过"] + n37["读取 API 输入"] + n38["解码具体参数并检查字段合同"] + n39[["news.article.publishedDetail"]] + click n39 href "api/front/browse.dever#L3,14" "打开源码" + n40["按输出类型检查可序列化值并编码响应"] + n41["发布暂存的认证 Header / Cookie"] + n42(["返回成功响应"]) + n0 --> n1 + n1 -->|"未匹配此入口:继续路由;均未匹配"| n2 + n1 --> n3 + n3 --> n4 + n4 -->|"失败"| n5 + n4 --> n6 + n6 -->|"无站点绑定"| n7 + n6 --> n8 + n8 -->|"凭据格式错误或冲突"| n9 + n8 -->|"无凭据"| n11 + n8 -->|"有凭据"| n13 + n13 -->|"否"| n12 + n13 -->|"是"| n14 + n14 -->|"不符合"| n15 + n14 -->|"通过"| n12 + n12 -->|"无 Provider 或 Token 无效"| n16 + n12 --> n17 + n17 -->|"user.account.verify"| n18 + n18 -->|"验证函数失败"| n19 + n18 --> n20 + n20 -->|"身份不合法"| n21 + n20 -->|"通过"| n10 + n17 -->|"无匹配验证函数"| n22 + n10 --> n23 + n23 --> n24 + n24 -->|"否"| n26 + n26 -->|"是:缺少租户身份"| n27 + n26 -->|"否"| n25 + n24 -->|"是"| n28 + n28 -->|"选择失败"| n29 + n28 --> n30 + n30 -->|"未启用"| n31 + n30 -->|"查询失败"| n32 + n30 -->|"允许"| n25 + n33 --> n34 + n25 --> n35 + n35 -->|"拒绝:Access 状态;查询故障:500"| n33 + n35 -->|"允许"| n36 + n36 --> n37 + n37 -->|"失败:输入错误状态"| n33 + n37 --> n38 + n38 -->|"失败:400"| n33 + n38 --> n39 + n39 -->|"业务错误映射状态;其他故障:500"| n33 + n39 --> n40 + n40 -->|"编码失败:500"| n33 + n40 --> n41 + n41 --> n42 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.verify | [api/front/browse.dever:3:14](api/front/browse.dever#L3,14) | [user.account.verify](../../user/account/link.md#flow-aaaacb87d61f819c-1) | +| news.article.publishedDetail | [api/front/browse.dever:3:14](api/front/browse.dever#L3,14) | [news.article.publishedDetail](link.md#flow-22c7e0f85e410646-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [news.article.publishedDetail](app/publication.dever#L17) | 领域内 | API 绑定 | [api/front/browse.dever:3:14](api/front/browse.dever#L3,14) | +| [user.account.verify](../../user/account/app/account.dever#L133) | 跨领域 [user.account](../../user/account/link.md) | 身份验证 | [api/front/browse.dever:3:14](api/front/browse.dever#L3,14) | + +### news.article.api.front.browse.list + +定义:[api/front/browse.dever:1:1](api/front/browse.dever#L1,1) + + + +```mermaid +flowchart TD + n0(["news.article.api.front.browse.list"]) + click n0 href "api/front/browse.dever#L1,12" "打开源码" + n1{"路由 GET /news/article/front/browse/list,topic #34;front/browse#34; 接受请求?"} + n2(["返回 HTTP 404"]) + n3["进入 API"] + n4["取得认证时间"] + n5(["返回 HTTP 500"]) + n6["按 topic #34;front/browse#34; 选择站点与 Provider"] + n7(["返回 HTTP 403"]) + n8{"读取 Bearer 或站点 Cookie;两者互斥"} + n9(["返回 HTTP 401"]) + n10["认证上下文就绪"] + n11(["返回 HTTP 401"]) + n12["校验 Token 的签名、有效期及站点"] + n13{"Cookie 且非 GET / HEAD / OPTIONS?"} + n14["检查站点 Origin"] + n15(["返回 HTTP 403"]) + n16(["返回 HTTP 401"]) + n17{"按 Provider.verify 选择验证函数"} + n18[["user.account.verify"]] + click n18 href "api/front/browse.dever#L1,12" "打开源码" + n19(["返回 HTTP 业务错误映射状态;其他故障 500"]) + n20["用验证结果建立身份"] + n21(["返回 HTTP 认证错误状态"]) + n22(["返回 HTTP 500"]) + n23["建立请求数据库上下文"] + n24{"有已验证租户?"} + n25["数据库上下文就绪"] + n26{"部署启用了租户存储?"} + n27(["返回 HTTP 403"]) + n28["按已验证租户选择数据库"] + n29(["返回 HTTP 503"]) + n30{"检查租户组件启用状态:news"} + n31(["返回 HTTP 403"]) + n32(["返回 HTTP 503"]) + n33["失败:保留 HTTP 错误状态"] + n34(["返回错误响应"]) + n35{"权限检查:news.article.{site}.list"} + n36["权限通过"] + n37["读取 API 输入"] + n38["解码具体参数并检查字段合同"] + n39[["news.article.published"]] + click n39 href "api/front/browse.dever#L1,12" "打开源码" + n40["按输出类型检查可序列化值并编码响应"] + n41["发布暂存的认证 Header / Cookie"] + n42(["返回成功响应"]) + n0 --> n1 + n1 -->|"未匹配此入口:继续路由;均未匹配"| n2 + n1 --> n3 + n3 --> n4 + n4 -->|"失败"| n5 + n4 --> n6 + n6 -->|"无站点绑定"| n7 + n6 --> n8 + n8 -->|"凭据格式错误或冲突"| n9 + n8 -->|"无凭据"| n11 + n8 -->|"有凭据"| n13 + n13 -->|"否"| n12 + n13 -->|"是"| n14 + n14 -->|"不符合"| n15 + n14 -->|"通过"| n12 + n12 -->|"无 Provider 或 Token 无效"| n16 + n12 --> n17 + n17 -->|"user.account.verify"| n18 + n18 -->|"验证函数失败"| n19 + n18 --> n20 + n20 -->|"身份不合法"| n21 + n20 -->|"通过"| n10 + n17 -->|"无匹配验证函数"| n22 + n10 --> n23 + n23 --> n24 + n24 -->|"否"| n26 + n26 -->|"是:缺少租户身份"| n27 + n26 -->|"否"| n25 + n24 -->|"是"| n28 + n28 -->|"选择失败"| n29 + n28 --> n30 + n30 -->|"未启用"| n31 + n30 -->|"查询失败"| n32 + n30 -->|"允许"| n25 + n33 --> n34 + n25 --> n35 + n35 -->|"拒绝:Access 状态;查询故障:500"| n33 + n35 -->|"允许"| n36 + n36 --> n37 + n37 -->|"失败:输入错误状态"| n33 + n37 --> n38 + n38 -->|"失败:400"| n33 + n38 --> n39 + n39 -->|"业务错误映射状态;其他故障:500"| n33 + n39 --> n40 + n40 -->|"编码失败:500"| n33 + n40 --> n41 + n41 --> n42 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.verify | [api/front/browse.dever:1:12](api/front/browse.dever#L1,12) | [user.account.verify](../../user/account/link.md#flow-aaaacb87d61f819c-1) | +| news.article.published | [api/front/browse.dever:1:12](api/front/browse.dever#L1,12) | [news.article.published](link.md#flow-607cab8a8d9ca46f-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [news.article.published](app/publication.dever#L8) | 领域内 | API 绑定 | [api/front/browse.dever:1:12](api/front/browse.dever#L1,12) | +| [user.account.verify](../../user/account/app/account.dever#L133) | 跨领域 [user.account](../../user/account/link.md) | 身份验证 | [api/front/browse.dever:1:12](api/front/browse.dever#L1,12) | + +### news.article.create + +定义:[app/editing.dever:16:1](app/editing.dever#L16,1) + + + +```mermaid +flowchart TD + n0(["create"]) + click n0 href "app/editing.dever#L16,1" "打开源码" + n1(["返回"]) + n3[["text.trim"]] + click n3 href "app/editing.dever#L23,21" "打开源码" + n4[["dever.api.require"]] + click n4 href "app/editing.dever#L23,3" "打开源码" + n5[["content.category.requireActive"]] + click n5 href "app/editing.dever#L24,3" "打开源码" + n6[["media.asset.requireReady"]] + click n6 href "app/editing.dever#L25,3" "打开源码" + n7[["user.account.currentUser"]] + click n7 href "app/editing.dever#L28,19" "打开源码" + n8["news.article.model.create"] + click n8 href "app/editing.dever#L26,12" "打开源码" + n9["赋值 stored"] + click n9 href "app/editing.dever#L26,12" "打开源码" + n10[["news.article.detail"]] + click n10 href "app/editing.dever#L36,13" "打开源码" + n11["赋值 article"] + click n11 href "app/editing.dever#L36,13" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n5 + n5 --> n6 + n6 --> n7 + n7 --> n8 + n8 --> n9 + n9 --> n10 + n10 --> n11 + n11 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| text.trim | [app/editing.dever:23:21](app/editing.dever#L23,21) | 依赖或生成的分派(本页不展开) | +| dever.api.require | [app/editing.dever:23:3](app/editing.dever#L23,3) | 依赖或生成的分派(本页不展开) | +| content.category.requireActive | [app/editing.dever:24:3](app/editing.dever#L24,3) | [content.category.requireActive](../../content/category/link.md#flow-eee0931d93e09e79-1) | +| media.asset.requireReady | [app/editing.dever:25:3](app/editing.dever#L25,3) | [media.asset.requireReady](../../media/asset/link.md#flow-2c00b9e4b7136711-1) | +| user.account.currentUser | [app/editing.dever:28:19](app/editing.dever#L28,19) | [user.account.currentUser](../../user/account/link.md#flow-625d5c0e14864dd6-1) | +| news.article.detail | [app/editing.dever:36:13](app/editing.dever#L36,13) | [news.article.detail](link.md#flow-15251ffe01bb516b-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| `dever.api.require` | 依赖 | 调用 | [app/editing.dever:23:3](app/editing.dever#L23,3) | +| `text.trim` | 依赖 | 调用 | [app/editing.dever:23:21](app/editing.dever#L23,21) | +| [content.category.requireActive](../../content/category/app.dever#L34) | 跨领域 [content.category](../../content/category/link.md) | 调用 | [app/editing.dever:24:3](app/editing.dever#L24,3) | +| [media.asset.requireReady](../../media/asset/app.dever#L36) | 跨领域 [media.asset](../../media/asset/link.md) | 调用 | [app/editing.dever:25:3](app/editing.dever#L25,3) | +| [news.article.model.create](model.dever#L6) | 领域内 | 内建操作 | [app/editing.dever:26:12](app/editing.dever#L26,12) | +| [user.account.currentUser](../../user/account/app/account.dever#L153) | 跨领域 [user.account](../../user/account/link.md) | 调用 | [app/editing.dever:28:19](app/editing.dever#L28,19) | +| [news.article.detail](app/editing.dever#L51) | 领域内 | 调用 | [app/editing.dever:36:13](app/editing.dever#L36,13) | + +### news.article.detail + +定义:[app/editing.dever:51:1](app/editing.dever#L51,1) + + + +```mermaid +flowchart TD + n0(["detail"]) + click n0 href "app/editing.dever#L51,1" "打开源码" + n1(["返回"]) + n3["news.article.model.get"] + click n3 href "app/editing.dever#L52,12" "打开源码" + n4["赋值 stored"] + click n4 href "app/editing.dever#L52,12" "打开源码" + n5[["user.account.currentUser"]] + click n5 href "app/editing.dever#L53,41" "打开源码" + n6[["dever.api.require"]] + click n6 href "app/editing.dever#L53,3" "打开源码" + n7["赋值 article"] + click n7 href "app/editing.dever#L54,13" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n5 + n5 --> n6 + n6 --> n7 + n7 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.currentUser | [app/editing.dever:53:41](app/editing.dever#L53,41) | [user.account.currentUser](../../user/account/link.md#flow-625d5c0e14864dd6-1) | +| dever.api.require | [app/editing.dever:53:3](app/editing.dever#L53,3) | 依赖或生成的分派(本页不展开) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [news.article.model.get](model.dever#L6) | 领域内 | 内建操作 | [app/editing.dever:52:12](app/editing.dever#L52,12) | +| `dever.api.require` | 依赖 | 调用 | [app/editing.dever:53:3](app/editing.dever#L53,3) | +| [user.account.currentUser](../../user/account/app/account.dever#L153) | 跨领域 [user.account](../../user/account/link.md) | 调用 | [app/editing.dever:53:41](app/editing.dever#L53,41) | + +### news.article.domain.pageSize + +定义:[domain.dever:1:1](domain.dever#L1,1)、[domain.dever:5:1](domain.dever#L5,1) + + + +```mermaid +flowchart TD + n0(["pageSize"]) + click n0 href "domain.dever#L1,1" "打开源码" + n1(["返回"]) + n2(["传播错误"]) + n3{"按实参匹配分句(互斥)"} + n4["value: Int #62;= 1 #60;= 100"] + click n4 href "domain.dever#L1,1" "打开源码" + n5["赋值 size"] + click n5 href "domain.dever#L2,10" "打开源码" + n6["value: other"] + click n6 href "domain.dever#L5,1" "打开源码" + n7["fail dever.api.Error.Invalid"] + click n7 href "domain.dever#L6,8" "打开源码" + n0 --> n3 + n3 -->|"value: Int #62;= 1 #60;= 100"| n4 + n4 --> n5 + n5 --> n1 + n3 -->|"value: other"| n6 + n6 --> n7 + n7 --> n2 +``` + +没有已收录的下游调用。 + +### news.article.domain.requireUpdated + +定义:[domain.dever:9:1](domain.dever#L9,1)、[domain.dever:11:1](domain.dever#L11,1) + + + +```mermaid +flowchart TD + n0(["requireUpdated"]) + click n0 href "domain.dever#L9,1" "打开源码" + n1(["返回"]) + n2(["传播错误"]) + n3{"按实参匹配分句(互斥)"} + n4["changed: 1"] + click n4 href "domain.dever#L9,1" "打开源码" + n5["changed: other"] + click n5 href "domain.dever#L11,1" "打开源码" + n6["fail dever.api.Error.Conflict"] + click n6 href "domain.dever#L12,8" "打开源码" + n0 --> n3 + n3 -->|"changed: 1"| n4 + n4 --> n1 + n3 -->|"changed: other"| n5 + n5 --> n6 + n6 --> n2 +``` + +没有已收录的下游调用。 + +### news.article.edit + +定义:[app/editing.dever:77:1](app/editing.dever#L77,1) + + + +```mermaid +flowchart TD + n0(["edit"]) + click n0 href "app/editing.dever#L77,1" "打开源码" + n1(["返回"]) + n3["news.article.model.get"] + click n3 href "app/editing.dever#L85,12" "打开源码" + n4["赋值 stored"] + click n4 href "app/editing.dever#L85,12" "打开源码" + n5[["user.account.currentUser"]] + click n5 href "app/editing.dever#L86,41" "打开源码" + n6[["dever.api.require"]] + click n6 href "app/editing.dever#L86,3" "打开源码" + n7[["dever.api.require"]] + click n7 href "app/editing.dever#L87,3" "打开源码" + n8[["text.trim"]] + click n8 href "app/editing.dever#L88,21" "打开源码" + n9[["dever.api.require"]] + click n9 href "app/editing.dever#L88,3" "打开源码" + n10[["content.category.requireActive"]] + click n10 href "app/editing.dever#L89,3" "打开源码" + n11[["media.asset.requireReady"]] + click n11 href "app/editing.dever#L90,3" "打开源码" + n12["news.article.model.update"] + click n12 href "app/editing.dever#L91,13" "打开源码" + n13["赋值 changed"] + click n13 href "app/editing.dever#L91,13" "打开源码" + n14[["news.article.domain.requireUpdated"]] + click n14 href "app/editing.dever#L103,3" "打开源码" + n15[["news.article.detail"]] + click n15 href "app/editing.dever#L104,13" "打开源码" + n16["赋值 article"] + click n16 href "app/editing.dever#L104,13" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n5 + n5 --> n6 + n6 --> n7 + n7 --> n8 + n8 --> n9 + n9 --> n10 + n10 --> n11 + n11 --> n12 + n12 --> n13 + n13 --> n14 + n14 --> n15 + n15 --> n16 + n16 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.currentUser | [app/editing.dever:86:41](app/editing.dever#L86,41) | [user.account.currentUser](../../user/account/link.md#flow-625d5c0e14864dd6-1) | +| dever.api.require | [app/editing.dever:86:3](app/editing.dever#L86,3) | 依赖或生成的分派(本页不展开) | +| dever.api.require | [app/editing.dever:87:3](app/editing.dever#L87,3) | 依赖或生成的分派(本页不展开) | +| text.trim | [app/editing.dever:88:21](app/editing.dever#L88,21) | 依赖或生成的分派(本页不展开) | +| dever.api.require | [app/editing.dever:88:3](app/editing.dever#L88,3) | 依赖或生成的分派(本页不展开) | +| content.category.requireActive | [app/editing.dever:89:3](app/editing.dever#L89,3) | [content.category.requireActive](../../content/category/link.md#flow-eee0931d93e09e79-1) | +| media.asset.requireReady | [app/editing.dever:90:3](app/editing.dever#L90,3) | [media.asset.requireReady](../../media/asset/link.md#flow-2c00b9e4b7136711-1) | +| news.article.domain.requireUpdated | [app/editing.dever:103:3](app/editing.dever#L103,3) | [news.article.domain.requireUpdated](link.md#flow-2fc7e89da8af74d2-1) | +| news.article.detail | [app/editing.dever:104:13](app/editing.dever#L104,13) | [news.article.detail](link.md#flow-15251ffe01bb516b-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [news.article.model.get](model.dever#L6) | 领域内 | 内建操作 | [app/editing.dever:85:12](app/editing.dever#L85,12) | +| `dever.api.require` | 依赖 | 调用 | [app/editing.dever:86:3](app/editing.dever#L86,3) | +| [user.account.currentUser](../../user/account/app/account.dever#L153) | 跨领域 [user.account](../../user/account/link.md) | 调用 | [app/editing.dever:86:41](app/editing.dever#L86,41) | +| `dever.api.require` | 依赖 | 调用 | [app/editing.dever:87:3](app/editing.dever#L87,3) | +| `dever.api.require` | 依赖 | 调用 | [app/editing.dever:88:3](app/editing.dever#L88,3) | +| `text.trim` | 依赖 | 调用 | [app/editing.dever:88:21](app/editing.dever#L88,21) | +| [content.category.requireActive](../../content/category/app.dever#L34) | 跨领域 [content.category](../../content/category/link.md) | 调用 | [app/editing.dever:89:3](app/editing.dever#L89,3) | +| [media.asset.requireReady](../../media/asset/app.dever#L36) | 跨领域 [media.asset](../../media/asset/link.md) | 调用 | [app/editing.dever:90:3](app/editing.dever#L90,3) | +| [news.article.model.update](model.dever#L6) | 领域内 | 内建操作 | [app/editing.dever:91:13](app/editing.dever#L91,13) | +| [news.article.domain.requireUpdated](domain.dever#L9) | 领域内 | 调用 | [app/editing.dever:103:3](app/editing.dever#L103,3) | +| [news.article.detail](app/editing.dever#L51) | 领域内 | 调用 | [app/editing.dever:104:13](app/editing.dever#L104,13) | + +### news.article.existingPublication + +定义:[app/publication.dever:87:1](app/publication.dever#L87,1)、[app/publication.dever:93:1](app/publication.dever#L93,1) + + + +```mermaid +flowchart TD + n0(["existingPublication"]) + click n0 href "app/publication.dever#L87,1" "打开源码" + n1(["返回"]) + n2(["传播错误"]) + n3{"按实参匹配分句(互斥)"} + n4["stored: model.publication.Publication"] + click n4 href "app/publication.dever#L87,1" "打开源码" + n5["赋值 publication"] + click n5 href "app/publication.dever#L90,17" "打开源码" + n6["stored: null"] + click n6 href "app/publication.dever#L93,1" "打开源码" + n7["fail dever.api.Error.NotFound"] + click n7 href "app/publication.dever#L94,8" "打开源码" + n0 --> n3 + n3 -->|"stored: model.publication.Publication"| n4 + n4 --> n5 + n5 --> n1 + n3 -->|"stored: null"| n6 + n6 --> n7 + n7 --> n2 +``` + +没有已收录的下游调用。 + +### news.article.list + +定义:[app/editing.dever:39:1](app/editing.dever#L39,1) + + + +```mermaid +flowchart TD + n0(["list"]) + click n0 href "app/editing.dever#L39,1" "打开源码" + n1(["返回"]) + n3[["user.account.currentUser"]] + click n3 href "app/editing.dever#L40,12" "打开源码" + n4["赋值 author"] + click n4 href "app/editing.dever#L40,12" "打开源码" + n5[["news.article.domain.pageSize"]] + click n5 href "app/editing.dever#L42,40" "打开源码" + n6["news.article.model.ownedArticles"] + click n6 href "app/editing.dever#L42,12" "打开源码" + n7["news.article.model.count"] + click n7 href "app/editing.dever#L43,13" "打开源码" + n8["赋值 articles"] + click n8 href "app/editing.dever#L41,14" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n5 + n5 --> n6 + n6 --> n7 + n7 --> n8 + n8 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.currentUser | [app/editing.dever:40:12](app/editing.dever#L40,12) | [user.account.currentUser](../../user/account/link.md#flow-625d5c0e14864dd6-1) | +| news.article.domain.pageSize | [app/editing.dever:42:40](app/editing.dever#L42,40) | [news.article.domain.pageSize](link.md#flow-143dd236c95f450d-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [user.account.currentUser](../../user/account/app/account.dever#L153) | 跨领域 [user.account](../../user/account/link.md) | 调用 | [app/editing.dever:40:12](app/editing.dever#L40,12) | +| [news.article.model.ownedArticles](model.dever#L20) | 领域内 | 内建操作 | [app/editing.dever:42:12](app/editing.dever#L42,12) | +| [news.article.domain.pageSize](domain.dever#L1) | 领域内 | 调用 | [app/editing.dever:42:40](app/editing.dever#L42,40) | +| [news.article.model.count](model.dever#L6) | 领域内 | 内建操作 | [app/editing.dever:43:13](app/editing.dever#L43,13) | + +### news.article.model.Article + +定义:[model.dever:6:1](model.dever#L6,1) + +没有已收录的下游调用。 + +### news.article.model.ArticleStatus + +定义:[model.dever:1:1](model.dever#L1,1) + +没有已收录的下游调用。 + +### news.article.model.ownedArticles + +定义:[model.dever:20:1](model.dever#L20,1) + +没有已收录的下游调用。 + +### news.article.model.publication.Publication + +定义:[model/publication.dever:1:1](model/publication.dever#L1,1) + +没有已收录的下游调用。 + +### news.article.publish + +定义:[app/publication.dever:1:1](app/publication.dever#L1,1) + + + +```mermaid +flowchart TD + n0(["publish"]) + click n0 href "app/publication.dever#L1,1" "打开源码" + n1["开始事务"] + n2(["提交事务,返回"]) + n3(["回滚事务,传播错误"]) + n4["news.article.model.get"] + click n4 href "app/publication.dever#L2,12" "打开源码" + n5["赋值 stored"] + click n5 href "app/publication.dever#L2,12" "打开源码" + n6[["user.account.currentUser"]] + click n6 href "app/publication.dever#L3,41" "打开源码" + n7[["dever.api.require"]] + click n7 href "app/publication.dever#L3,3" "打开源码" + n8[["dever.api.require"]] + click n8 href "app/publication.dever#L4,3" "打开源码" + n9[["news.article.publishDraft"]] + click n9 href "app/publication.dever#L5,17" "打开源码" + n10["赋值 publication"] + click n10 href "app/publication.dever#L5,17" "打开源码" + n0 --> n1 + n1 -.->|"事务体任一未捕获错误"| n3 + n1 --> n4 + n4 --> n5 + n5 --> n6 + n6 --> n7 + n7 --> n8 + n8 --> n9 + n9 --> n10 + n10 --> n2 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.currentUser | [app/publication.dever:3:41](app/publication.dever#L3,41) | [user.account.currentUser](../../user/account/link.md#flow-625d5c0e14864dd6-1) | +| dever.api.require | [app/publication.dever:3:3](app/publication.dever#L3,3) | 依赖或生成的分派(本页不展开) | +| dever.api.require | [app/publication.dever:4:3](app/publication.dever#L4,3) | 依赖或生成的分派(本页不展开) | +| news.article.publishDraft | [app/publication.dever:5:17](app/publication.dever#L5,17) | [news.article.publishDraft](link.md#flow-e1d3187b2aeb81da-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [news.article.model.get](model.dever#L6) | 领域内 | 内建操作 | [app/publication.dever:2:12](app/publication.dever#L2,12) | +| `dever.api.require` | 依赖 | 调用 | [app/publication.dever:3:3](app/publication.dever#L3,3) | +| [user.account.currentUser](../../user/account/app/account.dever#L153) | 跨领域 [user.account](../../user/account/link.md) | 调用 | [app/publication.dever:3:41](app/publication.dever#L3,41) | +| `dever.api.require` | 依赖 | 调用 | [app/publication.dever:4:3](app/publication.dever#L4,3) | +| [news.article.publishDraft](app/publication.dever#L48) | 领域内 | 调用 | [app/publication.dever:5:17](app/publication.dever#L5,17) | + +### news.article.publishDraft + +定义:[app/publication.dever:48:1](app/publication.dever#L48,1) + + + +```mermaid +flowchart TD + n0(["publishDraft"]) + click n0 href "app/publication.dever#L48,1" "打开源码" + n1(["返回"]) + n3["news.article.model.get"] + click n3 href "app/publication.dever#L49,12" "打开源码" + n4["赋值 stored"] + click n4 href "app/publication.dever#L49,12" "打开源码" + n5[["dever.api.require"]] + click n5 href "app/publication.dever#L50,3" "打开源码" + n6[["content.category.requireActive"]] + click n6 href "app/publication.dever#L51,3" "打开源码" + n7[["media.asset.requireReady"]] + click n7 href "app/publication.dever#L52,3" "打开源码" + n8[["dever.time.now"]] + click n8 href "app/publication.dever#L53,18" "打开源码" + n9["赋值 published_at"] + click n9 href "app/publication.dever#L53,18" "打开源码" + n10["news.article.model.update"] + click n10 href "app/publication.dever#L54,13" "打开源码" + n11["赋值 changed"] + click n11 href "app/publication.dever#L54,13" "打开源码" + n12[["news.article.domain.requireUpdated"]] + click n12 href "app/publication.dever#L64,3" "打开源码" + n13[["audit.operation.record"]] + click n13 href "app/publication.dever#L65,3" "打开源码" + n14["news.article.model.publication.upsert"] + click n14 href "app/publication.dever#L70,17" "打开源码" + n15["赋值 publication"] + click n15 href "app/publication.dever#L70,17" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n5 + n5 --> n6 + n6 --> n7 + n7 --> n8 + n8 --> n9 + n9 --> n10 + n10 --> n11 + n11 --> n12 + n12 --> n13 + n13 --> n14 + n14 --> n15 + n15 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| dever.api.require | [app/publication.dever:50:3](app/publication.dever#L50,3) | 依赖或生成的分派(本页不展开) | +| content.category.requireActive | [app/publication.dever:51:3](app/publication.dever#L51,3) | [content.category.requireActive](../../content/category/link.md#flow-eee0931d93e09e79-1) | +| media.asset.requireReady | [app/publication.dever:52:3](app/publication.dever#L52,3) | [media.asset.requireReady](../../media/asset/link.md#flow-2c00b9e4b7136711-1) | +| dever.time.now | [app/publication.dever:53:18](app/publication.dever#L53,18) | 依赖或生成的分派(本页不展开) | +| news.article.domain.requireUpdated | [app/publication.dever:64:3](app/publication.dever#L64,3) | [news.article.domain.requireUpdated](link.md#flow-2fc7e89da8af74d2-1) | +| audit.operation.record | [app/publication.dever:65:3](app/publication.dever#L65,3) | [audit.operation.record](../../audit/operation/link.md#flow-c1586d946bef8bfb-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [news.article.model.get](model.dever#L6) | 领域内 | 内建操作 | [app/publication.dever:49:12](app/publication.dever#L49,12) | +| `dever.api.require` | 依赖 | 调用 | [app/publication.dever:50:3](app/publication.dever#L50,3) | +| [content.category.requireActive](../../content/category/app.dever#L34) | 跨领域 [content.category](../../content/category/link.md) | 调用 | [app/publication.dever:51:3](app/publication.dever#L51,3) | +| [media.asset.requireReady](../../media/asset/app.dever#L36) | 跨领域 [media.asset](../../media/asset/link.md) | 调用 | [app/publication.dever:52:3](app/publication.dever#L52,3) | +| `dever.time.now` | 依赖 | 调用 | [app/publication.dever:53:18](app/publication.dever#L53,18) | +| [news.article.model.update](model.dever#L6) | 领域内 | 内建操作 | [app/publication.dever:54:13](app/publication.dever#L54,13) | +| [news.article.domain.requireUpdated](domain.dever#L9) | 领域内 | 调用 | [app/publication.dever:64:3](app/publication.dever#L64,3) | +| [audit.operation.record](../../audit/operation/app.dever#L1) | 跨领域 [audit.operation](../../audit/operation/link.md) | 调用 | [app/publication.dever:65:3](app/publication.dever#L65,3) | +| [news.article.model.publication.upsert](model/publication.dever#L1) | 领域内 | 内建操作 | [app/publication.dever:70:17](app/publication.dever#L70,17) | + +### news.article.publishScheduled + +定义:[app/publication.dever:22:1](app/publication.dever#L22,1) + + + +```mermaid +flowchart TD + n0(["publishScheduled"]) + click n0 href "app/publication.dever#L22,1" "打开源码" + n1["开始事务"] + n2(["提交事务,返回"]) + n3(["回滚事务,传播错误"]) + n4["news.article.model.get"] + click n4 href "app/publication.dever#L25,12" "打开源码" + n5["赋值 stored"] + click n5 href "app/publication.dever#L25,12" "打开源码" + n6[["news.article.scheduledPublication"]] + click n6 href "app/publication.dever#L26,17" "打开源码" + n7["赋值 publication"] + click n7 href "app/publication.dever#L26,17" "打开源码" + n0 --> n1 + n1 -.->|"事务体任一未捕获错误"| n3 + n1 --> n4 + n4 --> n5 + n5 --> n6 + n6 --> n7 + n7 --> n2 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| news.article.scheduledPublication | [app/publication.dever:26:17](app/publication.dever#L26,17) | [news.article.scheduledPublication](link.md#flow-b16baf4a00ea45dc-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [news.article.model.get](model.dever#L6) | 领域内 | 内建操作 | [app/publication.dever:25:12](app/publication.dever#L25,12) | +| [news.article.scheduledPublication](app/publication.dever#L29) | 领域内 | 调用 | [app/publication.dever:26:17](app/publication.dever#L26,17) | + +### news.article.published + +定义:[app/publication.dever:8:1](app/publication.dever#L8,1) + + + +```mermaid +flowchart TD + n0(["published"]) + click n0 href "app/publication.dever#L8,1" "打开源码" + n1(["返回"]) + n3[["news.article.domain.pageSize"]] + click n3 href "app/publication.dever#L12,14" "打开源码" + n4["news.article.model.publication.list"] + click n4 href "app/publication.dever#L9,14" "打开源码" + n5["赋值 articles"] + click n5 href "app/publication.dever#L9,14" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n5 + n5 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| news.article.domain.pageSize | [app/publication.dever:12:14](app/publication.dever#L12,14) | [news.article.domain.pageSize](link.md#flow-143dd236c95f450d-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [news.article.model.publication.list](model/publication.dever#L1) | 领域内 | 内建操作 | [app/publication.dever:9:14](app/publication.dever#L9,14) | +| [news.article.domain.pageSize](domain.dever#L1) | 领域内 | 调用 | [app/publication.dever:12:14](app/publication.dever#L12,14) | + +### news.article.publishedDetail + +定义:[app/publication.dever:17:1](app/publication.dever#L17,1) + + + +```mermaid +flowchart TD + n0(["publishedDetail"]) + click n0 href "app/publication.dever#L17,1" "打开源码" + n1(["返回"]) + n3["news.article.model.publication.get"] + click n3 href "app/publication.dever#L18,13" "打开源码" + n4["赋值 article"] + click n4 href "app/publication.dever#L18,13" "打开源码" + n5[["text.trim"]] + click n5 href "app/publication.dever#L19,21" "打开源码" + n6[["dever.api.require"]] + click n6 href "app/publication.dever#L19,3" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n5 + n5 --> n6 + n6 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| text.trim | [app/publication.dever:19:21](app/publication.dever#L19,21) | 依赖或生成的分派(本页不展开) | +| dever.api.require | [app/publication.dever:19:3](app/publication.dever#L19,3) | 依赖或生成的分派(本页不展开) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [news.article.model.publication.get](model/publication.dever#L1) | 领域内 | 内建操作 | [app/publication.dever:18:13](app/publication.dever#L18,13) | +| `dever.api.require` | 依赖 | 调用 | [app/publication.dever:19:3](app/publication.dever#L19,3) | +| `text.trim` | 依赖 | 调用 | [app/publication.dever:19:21](app/publication.dever#L19,21) | + +### news.article.remove + +定义:[app/editing.dever:65:1](app/editing.dever#L65,1) + + + +```mermaid +flowchart TD + n0(["remove"]) + click n0 href "app/editing.dever#L65,1" "打开源码" + n1(["返回"]) + n3["news.article.model.get"] + click n3 href "app/editing.dever#L66,12" "打开源码" + n4["赋值 stored"] + click n4 href "app/editing.dever#L66,12" "打开源码" + n5[["user.account.currentUser"]] + click n5 href "app/editing.dever#L67,41" "打开源码" + n6[["dever.api.require"]] + click n6 href "app/editing.dever#L67,3" "打开源码" + n7[["dever.api.require"]] + click n7 href "app/editing.dever#L68,3" "打开源码" + n8["news.article.model.delete"] + click n8 href "app/editing.dever#L69,13" "打开源码" + n9["赋值 removed"] + click n9 href "app/editing.dever#L69,13" "打开源码" + n10[["news.article.domain.requireUpdated"]] + click n10 href "app/editing.dever#L74,3" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n5 + n5 --> n6 + n6 --> n7 + n7 --> n8 + n8 --> n9 + n9 --> n10 + n10 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.currentUser | [app/editing.dever:67:41](app/editing.dever#L67,41) | [user.account.currentUser](../../user/account/link.md#flow-625d5c0e14864dd6-1) | +| dever.api.require | [app/editing.dever:67:3](app/editing.dever#L67,3) | 依赖或生成的分派(本页不展开) | +| dever.api.require | [app/editing.dever:68:3](app/editing.dever#L68,3) | 依赖或生成的分派(本页不展开) | +| news.article.domain.requireUpdated | [app/editing.dever:74:3](app/editing.dever#L74,3) | [news.article.domain.requireUpdated](link.md#flow-2fc7e89da8af74d2-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [news.article.model.get](model.dever#L6) | 领域内 | 内建操作 | [app/editing.dever:66:12](app/editing.dever#L66,12) | +| `dever.api.require` | 依赖 | 调用 | [app/editing.dever:67:3](app/editing.dever#L67,3) | +| [user.account.currentUser](../../user/account/app/account.dever#L153) | 跨领域 [user.account](../../user/account/link.md) | 调用 | [app/editing.dever:67:41](app/editing.dever#L67,41) | +| `dever.api.require` | 依赖 | 调用 | [app/editing.dever:68:3](app/editing.dever#L68,3) | +| [news.article.model.delete](model.dever#L6) | 领域内 | 内建操作 | [app/editing.dever:69:13](app/editing.dever#L69,13) | +| [news.article.domain.requireUpdated](domain.dever#L9) | 领域内 | 调用 | [app/editing.dever:74:3](app/editing.dever#L74,3) | + +### news.article.requireSchedulable + +定义:[app/editing.dever:107:1](app/editing.dever#L107,1) + + + +```mermaid +flowchart TD + n0(["requireSchedulable"]) + click n0 href "app/editing.dever#L107,1" "打开源码" + n1(["返回"]) + n3["news.article.model.get"] + click n3 href "app/editing.dever#L108,12" "打开源码" + n4["赋值 stored"] + click n4 href "app/editing.dever#L108,12" "打开源码" + n5[["user.account.currentUser"]] + click n5 href "app/editing.dever#L109,41" "打开源码" + n6[["dever.api.require"]] + click n6 href "app/editing.dever#L109,3" "打开源码" + n7[["dever.api.require"]] + click n7 href "app/editing.dever#L110,3" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n5 + n5 --> n6 + n6 --> n7 + n7 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.currentUser | [app/editing.dever:109:41](app/editing.dever#L109,41) | [user.account.currentUser](../../user/account/link.md#flow-625d5c0e14864dd6-1) | +| dever.api.require | [app/editing.dever:109:3](app/editing.dever#L109,3) | 依赖或生成的分派(本页不展开) | +| dever.api.require | [app/editing.dever:110:3](app/editing.dever#L110,3) | 依赖或生成的分派(本页不展开) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [news.article.model.get](model.dever#L6) | 领域内 | 内建操作 | [app/editing.dever:108:12](app/editing.dever#L108,12) | +| `dever.api.require` | 依赖 | 调用 | [app/editing.dever:109:3](app/editing.dever#L109,3) | +| [user.account.currentUser](../../user/account/app/account.dever#L153) | 跨领域 [user.account](../../user/account/link.md) | 调用 | [app/editing.dever:109:41](app/editing.dever#L109,41) | +| `dever.api.require` | 依赖 | 调用 | [app/editing.dever:110:3](app/editing.dever#L110,3) | + +### news.article.scheduledPublication + +定义:[app/publication.dever:29:1](app/publication.dever#L29,1)、[app/publication.dever:42:1](app/publication.dever#L42,1) + + + +```mermaid +flowchart TD + n0(["scheduledPublication"]) + click n0 href "app/publication.dever#L29,1" "打开源码" + n1(["返回"]) + n3{"按实参匹配分句(互斥)"} + n4["status: model.ArticleStatus.Published"] + click n4 href "app/publication.dever#L29,1" "打开源码" + n5["news.article.model.get"] + click n5 href "app/publication.dever#L32,12" "打开源码" + n6["赋值 stored"] + click n6 href "app/publication.dever#L32,12" "打开源码" + n7["news.article.model.publication.first"] + click n7 href "app/publication.dever#L34,5" "打开源码" + n8[["news.article.existingPublication"]] + click n8 href "app/publication.dever#L33,17" "打开源码" + n9["赋值 publication"] + click n9 href "app/publication.dever#L33,17" "打开源码" + n10["status: model.ArticleStatus.Draft"] + click n10 href "app/publication.dever#L42,1" "打开源码" + n11[["news.article.publishDraft"]] + click n11 href "app/publication.dever#L45,17" "打开源码" + n12["赋值 publication"] + click n12 href "app/publication.dever#L45,17" "打开源码" + n0 --> n3 + n3 -->|"status: model.ArticleStatus.Published"| n4 + n4 --> n5 + n5 --> n6 + n6 --> n7 + n7 --> n8 + n8 --> n9 + n9 --> n1 + n3 -->|"status: model.ArticleStatus.Draft"| n10 + n10 --> n11 + n11 --> n12 + n12 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| news.article.existingPublication | [app/publication.dever:33:17](app/publication.dever#L33,17) | [news.article.existingPublication](link.md#flow-b0204a5b9ed65643-1) | +| news.article.publishDraft | [app/publication.dever:45:17](app/publication.dever#L45,17) | [news.article.publishDraft](link.md#flow-e1d3187b2aeb81da-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [news.article.model.get](model.dever#L6) | 领域内 | 内建操作 | [app/publication.dever:32:12](app/publication.dever#L32,12) | +| [news.article.existingPublication](app/publication.dever#L87) | 领域内 | 调用 | [app/publication.dever:33:17](app/publication.dever#L33,17) | +| [news.article.model.publication.first](model/publication.dever#L1) | 领域内 | 内建操作 | [app/publication.dever:34:5](app/publication.dever#L34,5) | +| [news.article.publishDraft](app/publication.dever#L48) | 领域内 | 调用 | [app/publication.dever:45:17](app/publication.dever#L45,17) | diff --git a/examples/cms/dever/module/news/article/model.dever b/examples/cms/dever/module/news/article/model.dever new file mode 100644 index 0000000..339d01d --- /dev/null +++ b/examples/cms/dever/module/news/article/model.dever @@ -0,0 +1,25 @@ +type ArticleStatus { + Draft = "草稿" + Published = "已发布" +} + +type Article { + private owner author_id: Int = dever.auth.userId() + slug: Text(1, 180) lower unique + title: Text(1, 160) + body: Text raw + category_id: content.category.model.id? + asset_id: media.asset.model.id? + private version: Int default 1 + private status: ArticleStatus default ArticleStatus.Draft index + private published_at: DateTime? +} + +index(author_id, created_at) + +sql ownedArticles(author: Int, size: Int) ( + articles: List +) { + sqlite = "SELECT id, slug, title, body, version, category_id, asset_id FROM news_article_model WHERE author_id = ?1 ORDER BY id DESC LIMIT ?2" + postgres = "SELECT id, slug, title, body, version, category_id, asset_id FROM news_article_model WHERE author_id = $1 ORDER BY id DESC LIMIT $2" +} diff --git a/examples/cms/dever/module/news/article/model/publication.dever b/examples/cms/dever/module/news/article/model/publication.dever new file mode 100644 index 0000000..1ff42f4 --- /dev/null +++ b/examples/cms/dever/module/news/article/model/publication.dever @@ -0,0 +1,6 @@ +type Publication { + slug: Text(1, 180) unique + title: Text(1, 160) + body: Text + published_at: DateTime index +} diff --git a/examples/cms/dever/module/platform/tenant/README.md b/examples/cms/dever/module/platform/tenant/README.md new file mode 100644 index 0000000..0091974 --- /dev/null +++ b/examples/cms/dever/module/platform/tenant/README.md @@ -0,0 +1,8 @@ +# 租户 + +管理平台租户记录和启用状态,数据库准备由部署命令完成。 + +- [租户应用](doc/app.md) +- [租户规则](doc/domain.md) +- [租户模型](doc/model.md) +- [调用流程](link.md) diff --git a/examples/cms/dever/module/platform/tenant/app.dever b/examples/cms/dever/module/platform/tenant/app.dever new file mode 100644 index 0000000..1d9a79f --- /dev/null +++ b/examples/cms/dever/module/platform/tenant/app.dever @@ -0,0 +1,41 @@ +type TenantView { + id: model.id + key: Text + name: Text +} + +provision(key: Text, name: Text) (tenant: TenantView) { + stored = model.create( + { + key = key + name = name + } + ) + tenant = TenantView { + id = stored.id + key = stored.key + name = stored.name + } +} + +active(tenant_key: Text) (tenant: TenantView) { + stored = model.first( + { + where = key == domain.normalizeKey(tenant_key) + } + ) + tenant = activeView(stored) +} + +activeView(stored: model.Tenant) (tenant: TenantView) { + dever.api.require(stored.status == model.TenantStatus.Active, dever.api.Error.Unauthorized) + tenant = TenantView { + id = stored.id + key = stored.key + name = stored.name + } +} + +activeView(stored: null) (tenant: TenantView) { + fail(dever.api.Error.Unauthorized) +} diff --git a/examples/cms/dever/module/platform/tenant/doc/app.md b/examples/cms/dever/module/platform/tenant/doc/app.md new file mode 100644 index 0000000..e67f3c7 --- /dev/null +++ b/examples/cms/dever/module/platform/tenant/doc/app.md @@ -0,0 +1,17 @@ +# 租户应用 + +## platform.tenant.TenantView + +租户编号、唯一键和显示名称。 + +## platform.tenant.provision + +创建租户记录,key 去除首尾空白后转为小写并保持唯一。 + +## platform.tenant.active + +按规范化 key 查找启用租户,不存在或已禁用时返回未授权。 + +## platform.tenant.activeView + +检查租户启用状态并转换为 TenantView,空记录返回未授权。 diff --git a/examples/cms/dever/module/platform/tenant/doc/domain.md b/examples/cms/dever/module/platform/tenant/doc/domain.md new file mode 100644 index 0000000..f49153e --- /dev/null +++ b/examples/cms/dever/module/platform/tenant/doc/domain.md @@ -0,0 +1,5 @@ +# 租户规则 + +## platform.tenant.domain.normalizeKey + +去掉租户键两端空白并转为小写,用于与 Model 写入规则一致地查询。 diff --git a/examples/cms/dever/module/platform/tenant/doc/model.md b/examples/cms/dever/module/platform/tenant/doc/model.md new file mode 100644 index 0000000..d27fc7c --- /dev/null +++ b/examples/cms/dever/module/platform/tenant/doc/model.md @@ -0,0 +1,9 @@ +# 租户模型 + +## platform.tenant.model.TenantStatus + +租户的启用或禁用状态。 + +## platform.tenant.model.Tenant + +平台库中的租户记录;key 唯一,UUID 自动生成。 diff --git a/examples/cms/dever/module/platform/tenant/domain.dever b/examples/cms/dever/module/platform/tenant/domain.dever new file mode 100644 index 0000000..45e1af0 --- /dev/null +++ b/examples/cms/dever/module/platform/tenant/domain.dever @@ -0,0 +1,3 @@ +normalizeKey(value: Text) (key: Text) pure { + key = text.lower(text.trim(value)) +} diff --git a/examples/cms/dever/module/platform/tenant/link.md b/examples/cms/dever/module/platform/tenant/link.md new file mode 100644 index 0000000..4021537 --- /dev/null +++ b/examples/cms/dever/module/platform/tenant/link.md @@ -0,0 +1,197 @@ + + + +# platform.tenant + +按求值顺序展示调用、分支和任务调度,包含 API 与自动 REST 入口。点击节点打开源码;双框为调用,虚线为调度或错误路径。外部库和驱动不展开。 + +图不是某次运行的日志,分句和 Adapter 按输入或配置选择。修改源码后运行 `dever inspect . --links`。 + +## 入口 + +没有已收录的入口。 + +## 外部调用方 + +| 能力 | 调用方 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [platform.tenant.active](app.dever#L21) | [user.account.login](../../user/account/app/account.dever#L70) | 调用 | [../../user/account/app/account.dever:78:14](../../user/account/app/account.dever#L78,14) | +| [platform.tenant.active](app.dever#L21) | [user.account.establishLogin](../../user/account/app/account.dever#L91) | 调用 | [../../user/account/app/account.dever:99:14](../../user/account/app/account.dever#L99,14) | +| [platform.tenant.active](app.dever#L21) | [user.account.verify](../../user/account/app/account.dever#L133) | 调用 | [../../user/account/app/account.dever:137:14](../../user/account/app/account.dever#L137,14) | +| [platform.tenant.provision](app.dever#L7) | [user.account.registerAccount](../../user/account/app/account.dever#L41) | 调用 | [../../user/account/app/account.dever:48:12](../../user/account/app/account.dever#L48,12) | + +## 能力与内部调用 + +### platform.tenant.TenantView + +定义:[app.dever:1:1](app.dever#L1,1) + +没有已收录的下游调用。 + +### platform.tenant.active + +定义:[app.dever:21:1](app.dever#L21,1) + + + +```mermaid +flowchart TD + n0(["active"]) + click n0 href "app.dever#L21,1" "打开源码" + n1(["返回"]) + n3[["platform.tenant.domain.normalizeKey"]] + click n3 href "app.dever#L24,22" "打开源码" + n4["platform.tenant.model.first"] + click n4 href "app.dever#L22,12" "打开源码" + n5["赋值 stored"] + click n5 href "app.dever#L22,12" "打开源码" + n6[["platform.tenant.activeView"]] + click n6 href "app.dever#L27,12" "打开源码" + n7["赋值 tenant"] + click n7 href "app.dever#L27,12" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n5 + n5 --> n6 + n6 --> n7 + n7 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| platform.tenant.domain.normalizeKey | [app.dever:24:22](app.dever#L24,22) | [platform.tenant.domain.normalizeKey](link.md#flow-675cb56e9567397f-1) | +| platform.tenant.activeView | [app.dever:27:12](app.dever#L27,12) | [platform.tenant.activeView](link.md#flow-b450477f16968f30-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [platform.tenant.model.first](model.dever#L6) | 领域内 | 内建操作 | [app.dever:22:12](app.dever#L22,12) | +| [platform.tenant.domain.normalizeKey](domain.dever#L1) | 领域内 | 调用 | [app.dever:24:22](app.dever#L24,22) | +| [platform.tenant.activeView](app.dever#L30) | 领域内 | 调用 | [app.dever:27:12](app.dever#L27,12) | + +### platform.tenant.activeView + +定义:[app.dever:30:1](app.dever#L30,1)、[app.dever:39:1](app.dever#L39,1) + + + +```mermaid +flowchart TD + n0(["activeView"]) + click n0 href "app.dever#L30,1" "打开源码" + n1(["返回"]) + n2(["传播错误"]) + n3{"按实参匹配分句(互斥)"} + n4["stored: model.Tenant"] + click n4 href "app.dever#L30,1" "打开源码" + n5[["dever.api.require"]] + click n5 href "app.dever#L31,3" "打开源码" + n6["赋值 tenant"] + click n6 href "app.dever#L32,12" "打开源码" + n7["stored: null"] + click n7 href "app.dever#L39,1" "打开源码" + n8["fail dever.api.Error.Unauthorized"] + click n8 href "app.dever#L40,8" "打开源码" + n0 --> n3 + n3 -->|"stored: model.Tenant"| n4 + n4 --> n5 + n5 --> n6 + n6 --> n1 + n3 -->|"stored: null"| n7 + n7 --> n8 + n8 --> n2 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| dever.api.require | [app.dever:31:3](app.dever#L31,3) | 依赖或生成的分派(本页不展开) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| `dever.api.require` | 依赖 | 调用 | [app.dever:31:3](app.dever#L31,3) | + +### platform.tenant.domain.normalizeKey + +定义:[domain.dever:1:1](domain.dever#L1,1) + + + +```mermaid +flowchart TD + n0(["normalizeKey"]) + click n0 href "domain.dever#L1,1" "打开源码" + n1(["返回"]) + n3[["text.trim"]] + click n3 href "domain.dever#L2,20" "打开源码" + n4[["text.lower"]] + click n4 href "domain.dever#L2,9" "打开源码" + n5["赋值 key"] + click n5 href "domain.dever#L2,9" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n5 + n5 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| text.trim | [domain.dever:2:20](domain.dever#L2,20) | 依赖或生成的分派(本页不展开) | +| text.lower | [domain.dever:2:9](domain.dever#L2,9) | 依赖或生成的分派(本页不展开) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| `text.lower` | 依赖 | 调用 | [domain.dever:2:9](domain.dever#L2,9) | +| `text.trim` | 依赖 | 调用 | [domain.dever:2:20](domain.dever#L2,20) | + +### platform.tenant.model.Tenant + +定义:[model.dever:6:1](model.dever#L6,1) + +没有已收录的下游调用。 + +### platform.tenant.model.TenantStatus + +定义:[model.dever:1:1](model.dever#L1,1) + +没有已收录的下游调用。 + +### platform.tenant.provision + +定义:[app.dever:7:1](app.dever#L7,1) + + + +```mermaid +flowchart TD + n0(["provision"]) + click n0 href "app.dever#L7,1" "打开源码" + n1(["返回"]) + n3["platform.tenant.model.create"] + click n3 href "app.dever#L8,12" "打开源码" + n4["赋值 stored"] + click n4 href "app.dever#L8,12" "打开源码" + n5["赋值 tenant"] + click n5 href "app.dever#L14,12" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n5 + n5 --> n1 +``` + +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [platform.tenant.model.create](model.dever#L6) | 领域内 | 内建操作 | [app.dever:8:12](app.dever#L8,12) | diff --git a/examples/cms/dever/module/platform/tenant/model.dever b/examples/cms/dever/module/platform/tenant/model.dever new file mode 100644 index 0000000..c1de8e1 --- /dev/null +++ b/examples/cms/dever/module/platform/tenant/model.dever @@ -0,0 +1,11 @@ +type TenantStatus { + Active = "启用" + Disabled = "禁用" +} + +global type Tenant { + uuid: Uuid generated unique + key: Text(1, 64) lower unique + name: Text(1, 100) + status: TenantStatus default TenantStatus.Active index +} diff --git a/examples/cms/dever/module/publishing/schedule/README.md b/examples/cms/dever/module/publishing/schedule/README.md new file mode 100644 index 0000000..4ab2b9a --- /dev/null +++ b/examples/cms/dever/module/publishing/schedule/README.md @@ -0,0 +1,9 @@ +# 发布计划 + +为文章安排一次定时发布,重复任务不会重复发布。 + +- [发布计划接口](doc/api-admin-manage.md) +- [发布计划应用](doc/app.md) +- [发布计划任务](doc/job.md) +- [发布计划模型](doc/model.md) +- [调用流程](link.md) diff --git a/examples/cms/dever/module/publishing/schedule/api/admin/manage.dever b/examples/cms/dever/module/publishing/schedule/api/admin/manage.dever new file mode 100644 index 0000000..98a524a --- /dev/null +++ b/examples/cms/dever/module/publishing/schedule/api/admin/manage.dever @@ -0,0 +1 @@ +post schedule = app.create diff --git a/examples/cms/dever/module/publishing/schedule/app.dever b/examples/cms/dever/module/publishing/schedule/app.dever new file mode 100644 index 0000000..e251302 --- /dev/null +++ b/examples/cms/dever/module/publishing/schedule/app.dever @@ -0,0 +1,74 @@ +type PublishRequest { + schedule_id: model.id +} + +type ScheduleView { + id: model.id + article_id: news.article.model.id + publish_at: DateTime + status: Text +} + +transaction create(article_id: news.article.model.id, publish_at: DateTime) ( + schedule: ScheduleView +) { + news.article.requireSchedulable(article_id) + stored = model.create( + { + article_id = article_id + publish_at = publish_at + } + ) + queued = dever.job.enqueueAt( + job.publish, + PublishRequest { + schedule_id = stored.id + }, + uuid.toText(stored.uuid), + publish_at + ) + discarded = queued + schedule = ScheduleView { + id = stored.id + article_id = stored.article_id + publish_at = stored.publish_at + status = "pending" + } +} + +transaction publish(input: PublishRequest) () { + scheduled = model.get(input.schedule_id) + complete(scheduled.status, scheduled.id) +} + +complete(status: model.ScheduleStatus.Completed, schedule_id: model.id) () {} + +complete(status: model.ScheduleStatus.Cancelled, schedule_id: model.id) () {} + +complete(status: model.ScheduleStatus.Pending, schedule_id: model.id) () { + scheduled = model.get(schedule_id) + dever.api.require(model.exists( + { + where = id == schedule_id and publish_at <= dever.time.now() + } + ), dever.api.Error.Invalid) + changed = model.update( + { + where = id == scheduled.id and status == model.ScheduleStatus.Pending + }, + { + status = model.ScheduleStatus.Completed + } + ) + publishClaimed(changed, scheduled.article_id) +} + +publishClaimed(changed: 0, article_id: news.article.model.id) () {} + +publishClaimed(changed: 1, article_id: news.article.model.id) () { + publication = news.article.publishScheduled(article_id) +} + +publishClaimed(changed: other, article_id: news.article.model.id) () { + fail(dever.api.Error.Conflict) +} diff --git a/examples/cms/dever/module/publishing/schedule/doc/api-admin-manage.md b/examples/cms/dever/module/publishing/schedule/doc/api-admin-manage.md new file mode 100644 index 0000000..0504a60 --- /dev/null +++ b/examples/cms/dever/module/publishing/schedule/doc/api-admin-manage.md @@ -0,0 +1,5 @@ +# 发布计划接口 + +## publishing.schedule.api.admin.manage.schedule + +为当前作者的草稿建立定时发布计划。 diff --git a/examples/cms/dever/module/publishing/schedule/doc/app.md b/examples/cms/dever/module/publishing/schedule/doc/app.md new file mode 100644 index 0000000..1b01327 --- /dev/null +++ b/examples/cms/dever/module/publishing/schedule/doc/app.md @@ -0,0 +1,25 @@ +# 发布计划应用 + +## publishing.schedule.PublishRequest + +发布任务的输入,只保存计划编号。 + +## publishing.schedule.ScheduleView + +创建计划后返回文章编号、计划时间和状态。 + +## publishing.schedule.create + +为当前作者的草稿建立发布计划并按时入队;计划记录和入队在同一事务内完成。每篇文章只能有一条计划。 + +## publishing.schedule.publish + +加载发布计划并按其状态执行,供 Job 调用。 + +## publishing.schedule.complete + +仅处理已经到时的 Pending 计划;已完成或取消的计划不再执行。状态更新与文章发布在同一事务中完成。 + +## publishing.schedule.publishClaimed + +成功更新一条计划后发布文章;零条表示无需重复执行,其他数量返回冲突。 diff --git a/examples/cms/dever/module/publishing/schedule/doc/job.md b/examples/cms/dever/module/publishing/schedule/doc/job.md new file mode 100644 index 0000000..2335b97 --- /dev/null +++ b/examples/cms/dever/module/publishing/schedule/doc/job.md @@ -0,0 +1,5 @@ +# 发布计划任务 + +## publishing.schedule.job.publish + +执行发布计划,最多执行 3 次(含首次),单次期限 30 秒。 diff --git a/examples/cms/dever/module/publishing/schedule/doc/model.md b/examples/cms/dever/module/publishing/schedule/doc/model.md new file mode 100644 index 0000000..81bc034 --- /dev/null +++ b/examples/cms/dever/module/publishing/schedule/doc/model.md @@ -0,0 +1,9 @@ +# 发布计划模型 + +## publishing.schedule.model.ScheduleStatus + +计划的待发布、已完成或已取消状态。 + +## publishing.schedule.model.Schedule + +每篇文章的唯一发布计划,保存执行时间和状态。 diff --git a/examples/cms/dever/module/publishing/schedule/domain.dever b/examples/cms/dever/module/publishing/schedule/domain.dever new file mode 100644 index 0000000..e69de29 diff --git a/examples/cms/dever/module/publishing/schedule/job.dever b/examples/cms/dever/module/publishing/schedule/job.dever new file mode 100644 index 0000000..d6e95d7 --- /dev/null +++ b/examples/cms/dever/module/publishing/schedule/job.dever @@ -0,0 +1,5 @@ +database default + +job publish(input: app.PublishRequest) () retry(3) timeout(30000) { + app.publish(input) +} diff --git a/examples/cms/dever/module/publishing/schedule/link.md b/examples/cms/dever/module/publishing/schedule/link.md new file mode 100644 index 0000000..8cac1c3 --- /dev/null +++ b/examples/cms/dever/module/publishing/schedule/link.md @@ -0,0 +1,430 @@ + + + +# publishing.schedule + +按求值顺序展示调用、分支和任务调度,包含 API 与自动 REST 入口。点击节点打开源码;双框为调用,虚线为调度或错误路径。外部库和驱动不展开。 + +图不是某次运行的日志,分句和 Adapter 按输入或配置选择。修改源码后运行 `dever inspect . --links`。 + +## 入口 + +- API [publishing.schedule.api.admin.manage.schedule](api/admin/manage.dever#L1) → [publishing.schedule.create](app.dever#L12) +- Job [publishing.schedule.job.publish](job.dever#L3) + +## 外部调用方 + +当前加载的源码中没有已收录的外部调用。 + +## 能力与内部调用 + +### publishing.schedule.PublishRequest + +定义:[app.dever:1:1](app.dever#L1,1) + +没有已收录的下游调用。 + +### publishing.schedule.ScheduleView + +定义:[app.dever:5:1](app.dever#L5,1) + +没有已收录的下游调用。 + +### publishing.schedule.api.admin.manage.schedule + +定义:[api/admin/manage.dever:1:1](api/admin/manage.dever#L1,1) + + + +```mermaid +flowchart TD + n0(["publishing.schedule.api.admin.manage.schedule"]) + click n0 href "api/admin/manage.dever#L1,17" "打开源码" + n1{"路由 POST /publishing/schedule/admin/manage/schedule,topic #34;admin/manage#34; 接受请求?"} + n2(["返回 HTTP 404"]) + n3["进入 API"] + n4["取得认证时间"] + n5(["返回 HTTP 500"]) + n6["按 topic #34;admin/manage#34; 选择站点与 Provider"] + n7(["返回 HTTP 403"]) + n8{"读取 Bearer 或站点 Cookie;两者互斥"} + n9(["返回 HTTP 401"]) + n10["认证上下文就绪"] + n11(["返回 HTTP 401"]) + n12["校验 Token 的签名、有效期及站点"] + n13{"Cookie 且非 GET / HEAD / OPTIONS?"} + n14["检查站点 Origin"] + n15(["返回 HTTP 403"]) + n16(["返回 HTTP 401"]) + n17{"按 Provider.verify 选择验证函数"} + n18[["user.account.verify"]] + click n18 href "api/admin/manage.dever#L1,17" "打开源码" + n19(["返回 HTTP 业务错误映射状态;其他故障 500"]) + n20["用验证结果建立身份"] + n21(["返回 HTTP 认证错误状态"]) + n22(["返回 HTTP 500"]) + n23["建立请求数据库上下文"] + n24{"有已验证租户?"} + n25["数据库上下文就绪"] + n26{"部署启用了租户存储?"} + n27(["返回 HTTP 403"]) + n28["按已验证租户选择数据库"] + n29(["返回 HTTP 503"]) + n30{"检查租户组件启用状态:news, publishing"} + n31(["返回 HTTP 403"]) + n32(["返回 HTTP 503"]) + n33["进入写入口事务"] + n34["失败:保留 HTTP 错误状态"] + n35["回滚入口拥有的事务"] + n36(["返回 HTTP 500"]) + n37(["返回错误响应"]) + n38{"权限检查:publishing.schedule.{site}.schedule"} + n39["权限通过"] + n40["从可信身份建立 Job 执行上下文"] + n41["读取 API 输入"] + n42["解码具体参数并检查字段合同"] + n43[["publishing.schedule.create"]] + click n43 href "api/admin/manage.dever#L1,17" "打开源码" + n44["按输出类型检查可序列化值并编码响应"] + n45["提交入口拥有的事务"] + n46(["返回 HTTP 500"]) + n47["发布暂存的认证 Header / Cookie"] + n48(["返回成功响应"]) + n0 --> n1 + n1 -->|"未匹配此入口:继续路由;均未匹配"| n2 + n1 --> n3 + n3 --> n4 + n4 -->|"失败"| n5 + n4 --> n6 + n6 -->|"无站点绑定"| n7 + n6 --> n8 + n8 -->|"凭据格式错误或冲突"| n9 + n8 -->|"无凭据"| n11 + n8 -->|"有凭据"| n13 + n13 -->|"否"| n12 + n13 -->|"是"| n14 + n14 -->|"不符合"| n15 + n14 -->|"通过"| n12 + n12 -->|"无 Provider 或 Token 无效"| n16 + n12 --> n17 + n17 -->|"user.account.verify"| n18 + n18 -->|"验证函数失败"| n19 + n18 --> n20 + n20 -->|"身份不合法"| n21 + n20 -->|"通过"| n10 + n17 -->|"无匹配验证函数"| n22 + n10 --> n23 + n23 --> n24 + n24 -->|"否"| n26 + n26 -->|"是:缺少租户身份"| n27 + n26 -->|"否"| n25 + n24 -->|"是"| n28 + n28 -->|"选择失败"| n29 + n28 --> n30 + n30 -->|"未启用"| n31 + n30 -->|"查询失败"| n32 + n30 -->|"允许"| n25 + n25 --> n33 + n34 --> n35 + n35 -->|"事务结束失败"| n36 + n35 --> n37 + n33 --> n38 + n38 -->|"拒绝:Access 状态;查询故障:500"| n34 + n38 -->|"允许"| n39 + n39 --> n40 + n40 -->|"失败:认证错误状态"| n34 + n40 --> n41 + n41 -->|"失败:输入错误状态"| n34 + n41 --> n42 + n42 -->|"失败:400"| n34 + n42 --> n43 + n43 -->|"业务错误映射状态;其他故障:500"| n34 + n43 --> n44 + n44 -->|"编码失败:500"| n34 + n44 --> n45 + n45 -->|"提交失败"| n46 + n45 --> n47 + n47 --> n48 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.verify | [api/admin/manage.dever:1:17](api/admin/manage.dever#L1,17) | [user.account.verify](../../user/account/link.md#flow-aaaacb87d61f819c-1) | +| publishing.schedule.create | [api/admin/manage.dever:1:17](api/admin/manage.dever#L1,17) | [publishing.schedule.create](link.md#flow-a5c2828f7faa5041-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [publishing.schedule.create](app.dever#L12) | 领域内 | API 绑定 | [api/admin/manage.dever:1:17](api/admin/manage.dever#L1,17) | +| [user.account.verify](../../user/account/app/account.dever#L133) | 跨领域 [user.account](../../user/account/link.md) | 身份验证 | [api/admin/manage.dever:1:17](api/admin/manage.dever#L1,17) | + +### publishing.schedule.complete + +定义:[app.dever:44:1](app.dever#L44,1)、[app.dever:46:1](app.dever#L46,1)、[app.dever:48:1](app.dever#L48,1) + + + +```mermaid +flowchart TD + n0(["complete"]) + click n0 href "app.dever#L44,1" "打开源码" + n1(["返回"]) + n3{"按实参匹配分句(互斥)"} + n4["status: model.ScheduleStatus.Completed"] + click n4 href "app.dever#L44,1" "打开源码" + n5["status: model.ScheduleStatus.Cancelled"] + click n5 href "app.dever#L46,1" "打开源码" + n6["status: model.ScheduleStatus.Pending"] + click n6 href "app.dever#L48,1" "打开源码" + n7["publishing.schedule.model.get"] + click n7 href "app.dever#L49,15" "打开源码" + n8["赋值 scheduled"] + click n8 href "app.dever#L49,15" "打开源码" + n9[["dever.time.now"]] + click n9 href "app.dever#L52,53" "打开源码" + n10["publishing.schedule.model.exists"] + click n10 href "app.dever#L50,21" "打开源码" + n11[["dever.api.require"]] + click n11 href "app.dever#L50,3" "打开源码" + n12["publishing.schedule.model.update"] + click n12 href "app.dever#L55,13" "打开源码" + n13["赋值 changed"] + click n13 href "app.dever#L55,13" "打开源码" + n14[["publishing.schedule.publishClaimed"]] + click n14 href "app.dever#L63,3" "打开源码" + n0 --> n3 + n3 -->|"status: model.ScheduleStatus.Completed"| n4 + n4 --> n1 + n3 -->|"status: model.ScheduleStatus.Cancelled"| n5 + n5 --> n1 + n3 -->|"status: model.ScheduleStatus.Pending"| n6 + n6 --> n7 + n7 --> n8 + n8 --> n9 + n9 --> n10 + n10 --> n11 + n11 --> n12 + n12 --> n13 + n13 --> n14 + n14 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| dever.time.now | [app.dever:52:53](app.dever#L52,53) | 依赖或生成的分派(本页不展开) | +| dever.api.require | [app.dever:50:3](app.dever#L50,3) | 依赖或生成的分派(本页不展开) | +| publishing.schedule.publishClaimed | [app.dever:63:3](app.dever#L63,3) | [publishing.schedule.publishClaimed](link.md#flow-c4a292464addac3f-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [publishing.schedule.model.get](model.dever#L7) | 领域内 | 内建操作 | [app.dever:49:15](app.dever#L49,15) | +| `dever.api.require` | 依赖 | 调用 | [app.dever:50:3](app.dever#L50,3) | +| [publishing.schedule.model.exists](model.dever#L7) | 领域内 | 内建操作 | [app.dever:50:21](app.dever#L50,21) | +| `dever.time.now` | 依赖 | 调用 | [app.dever:52:53](app.dever#L52,53) | +| [publishing.schedule.model.update](model.dever#L7) | 领域内 | 内建操作 | [app.dever:55:13](app.dever#L55,13) | +| [publishing.schedule.publishClaimed](app.dever#L66) | 领域内 | 调用 | [app.dever:63:3](app.dever#L63,3) | + +### publishing.schedule.create + +定义:[app.dever:12:1](app.dever#L12,1) + + + +```mermaid +flowchart TD + n0(["create"]) + click n0 href "app.dever#L12,1" "打开源码" + n1["开始事务"] + n2(["提交事务,返回"]) + n3(["回滚事务,传播错误"]) + n4[["news.article.requireSchedulable"]] + click n4 href "app.dever#L15,3" "打开源码" + n5["publishing.schedule.model.create"] + click n5 href "app.dever#L16,12" "打开源码" + n6["赋值 stored"] + click n6 href "app.dever#L16,12" "打开源码" + n7[["uuid.toText"]] + click n7 href "app.dever#L27,5" "打开源码" + n8["持久化任务入队"] + click n8 href "app.dever#L22,12" "打开源码" + n9[["Job:publishing.schedule.job.publish"]] + click n9 href "app.dever#L22,12" "打开源码" + n10["赋值 queued"] + click n10 href "app.dever#L22,12" "打开源码" + n11["赋值 discarded"] + click n11 href "app.dever#L30,15" "打开源码" + n12["赋值 schedule"] + click n12 href "app.dever#L31,14" "打开源码" + n0 --> n1 + n1 -.->|"事务体任一未捕获错误"| n3 + n1 --> n4 + n4 --> n5 + n5 --> n6 + n6 --> n7 + n7 --> n8 + n8 -.->|"由 Worker 后续调度,不等待执行"| n9 + n8 --> n10 + n10 --> n11 + n11 --> n12 + n12 --> n2 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| news.article.requireSchedulable | [app.dever:15:3](app.dever#L15,3) | [news.article.requireSchedulable](../../news/article/link.md#flow-dc698efe57d88983-1) | +| uuid.toText | [app.dever:27:5](app.dever#L27,5) | 依赖或生成的分派(本页不展开) | +| Job:publishing.schedule.job.publish | [app.dever:22:12](app.dever#L22,12) | [publishing.schedule.job.publish](link.md#flow-c9df8cbe8f8cd013-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [news.article.requireSchedulable](../../news/article/app/editing.dever#L107) | 跨领域 [news.article](../../news/article/link.md) | 调用 | [app.dever:15:3](app.dever#L15,3) | +| [publishing.schedule.model.create](model.dever#L7) | 领域内 | 内建操作 | [app.dever:16:12](app.dever#L16,12) | +| [publishing.schedule.job.publish](job.dever#L3) | 领域内 | 入队 | [app.dever:23:5](app.dever#L23,5) | +| `uuid.toText` | 依赖 | 调用 | [app.dever:27:5](app.dever#L27,5) | + +### publishing.schedule.job.publish + +定义:[job.dever:3:1](job.dever#L3,1) + + + +```mermaid +flowchart TD + n0(["publish"]) + click n0 href "job.dever#L3,1" "打开源码" + n1(["返回"]) + n3[["publishing.schedule.publish"]] + click n3 href "job.dever#L4,3" "打开源码" + n0 --> n3 + n3 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| publishing.schedule.publish | [job.dever:4:3](job.dever#L4,3) | [publishing.schedule.publish](link.md#flow-1452f352cf5266e6-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [publishing.schedule.publish](app.dever#L39) | 领域内 | 调用 | [job.dever:4:3](job.dever#L4,3) | + +### publishing.schedule.model.Schedule + +定义:[model.dever:7:1](model.dever#L7,1) + +没有已收录的下游调用。 + +### publishing.schedule.model.ScheduleStatus + +定义:[model.dever:1:1](model.dever#L1,1) + +没有已收录的下游调用。 + +### publishing.schedule.publish + +定义:[app.dever:39:1](app.dever#L39,1) + + + +```mermaid +flowchart TD + n0(["publish"]) + click n0 href "app.dever#L39,1" "打开源码" + n1["开始事务"] + n2(["提交事务,返回"]) + n3(["回滚事务,传播错误"]) + n4["publishing.schedule.model.get"] + click n4 href "app.dever#L40,15" "打开源码" + n5["赋值 scheduled"] + click n5 href "app.dever#L40,15" "打开源码" + n6[["publishing.schedule.complete"]] + click n6 href "app.dever#L41,3" "打开源码" + n0 --> n1 + n1 -.->|"事务体任一未捕获错误"| n3 + n1 --> n4 + n4 --> n5 + n5 --> n6 + n6 --> n2 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| publishing.schedule.complete | [app.dever:41:3](app.dever#L41,3) | [publishing.schedule.complete](link.md#flow-87bb9cb4f5f0c2f6-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [publishing.schedule.model.get](model.dever#L7) | 领域内 | 内建操作 | [app.dever:40:15](app.dever#L40,15) | +| [publishing.schedule.complete](app.dever#L44) | 领域内 | 调用 | [app.dever:41:3](app.dever#L41,3) | + +### publishing.schedule.publishClaimed + +定义:[app.dever:66:1](app.dever#L66,1)、[app.dever:68:1](app.dever#L68,1)、[app.dever:72:1](app.dever#L72,1) + + + +```mermaid +flowchart TD + n0(["publishClaimed"]) + click n0 href "app.dever#L66,1" "打开源码" + n1(["返回"]) + n2(["传播错误"]) + n3{"按实参匹配分句(互斥)"} + n4["changed: 0"] + click n4 href "app.dever#L66,1" "打开源码" + n5["changed: 1"] + click n5 href "app.dever#L68,1" "打开源码" + n6[["news.article.publishScheduled"]] + click n6 href "app.dever#L69,17" "打开源码" + n7["赋值 publication"] + click n7 href "app.dever#L69,17" "打开源码" + n8["changed: other"] + click n8 href "app.dever#L72,1" "打开源码" + n9["fail dever.api.Error.Conflict"] + click n9 href "app.dever#L73,8" "打开源码" + n0 --> n3 + n3 -->|"changed: 0"| n4 + n4 --> n1 + n3 -->|"changed: 1"| n5 + n5 --> n6 + n6 --> n7 + n7 --> n1 + n3 -->|"changed: other"| n8 + n8 --> n9 + n9 --> n2 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| news.article.publishScheduled | [app.dever:69:17](app.dever#L69,17) | [news.article.publishScheduled](../../news/article/link.md#flow-5ba4105cff8222e5-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [news.article.publishScheduled](../../news/article/app/publication.dever#L22) | 跨领域 [news.article](../../news/article/link.md) | 调用 | [app.dever:69:17](app.dever#L69,17) | diff --git a/examples/cms/dever/module/publishing/schedule/model.dever b/examples/cms/dever/module/publishing/schedule/model.dever new file mode 100644 index 0000000..c24cd9f --- /dev/null +++ b/examples/cms/dever/module/publishing/schedule/model.dever @@ -0,0 +1,12 @@ +type ScheduleStatus { + Pending = "待发布" + Completed = "已完成" + Cancelled = "已取消" +} + +type Schedule { + uuid: Uuid generated unique + article_id: news.article.model.id unique + publish_at: DateTime index + status: ScheduleStatus default ScheduleStatus.Pending index +} diff --git a/examples/cms/dever/module/system/health/README.md b/examples/cms/dever/module/system/health/README.md new file mode 100644 index 0000000..5492386 --- /dev/null +++ b/examples/cms/dever/module/system/health/README.md @@ -0,0 +1,7 @@ +# 健康检查 + +提供命令行健康检查。 + +- [健康检查命令](doc/api.md) +- [系统健康](doc/app.md) +- [调用流程](link.md) diff --git a/examples/cms/dever/module/system/health/api.dever b/examples/cms/dever/module/system/health/api.dever new file mode 100644 index 0000000..795781c --- /dev/null +++ b/examples/cms/dever/module/system/health/api.dever @@ -0,0 +1 @@ +cmd ping = app.ping diff --git a/examples/cms/dever/module/system/health/app.dever b/examples/cms/dever/module/system/health/app.dever new file mode 100644 index 0000000..b7e6e67 --- /dev/null +++ b/examples/cms/dever/module/system/health/app.dever @@ -0,0 +1,9 @@ +type HealthView { + status: Text +} + +ping() (result: HealthView) { + result = HealthView { + status = "ready" + } +} diff --git a/examples/cms/dever/module/system/health/doc/api.md b/examples/cms/dever/module/system/health/doc/api.md new file mode 100644 index 0000000..5fc6513 --- /dev/null +++ b/examples/cms/dever/module/system/health/doc/api.md @@ -0,0 +1,5 @@ +# 健康检查命令 + +## system.health.api.ping + +返回 ready 状态。 diff --git a/examples/cms/dever/module/system/health/doc/app.md b/examples/cms/dever/module/system/health/doc/app.md new file mode 100644 index 0000000..3cdffa0 --- /dev/null +++ b/examples/cms/dever/module/system/health/doc/app.md @@ -0,0 +1,9 @@ +# 系统健康 + +## system.health.HealthView + +健康检查返回的状态。 + +## system.health.ping + +返回 ready,供命令行健康检查使用。 diff --git a/examples/cms/dever/module/system/health/link.md b/examples/cms/dever/module/system/health/link.md new file mode 100644 index 0000000..aaf4c99 --- /dev/null +++ b/examples/cms/dever/module/system/health/link.md @@ -0,0 +1,73 @@ + + + +# system.health + +按求值顺序展示调用、分支和任务调度,包含 API 与自动 REST 入口。点击节点打开源码;双框为调用,虚线为调度或错误路径。外部库和驱动不展开。 + +图不是某次运行的日志,分句和 Adapter 按输入或配置选择。修改源码后运行 `dever inspect . --links`。 + +## 入口 + +- CMD [system.health.api.ping](api.dever#L1) → [system.health.ping](app.dever#L5) + +## 外部调用方 + +当前加载的源码中没有已收录的外部调用。 + +## 能力与内部调用 + +### system.health.HealthView + +定义:[app.dever:1:1](app.dever#L1,1) + +没有已收录的下游调用。 + +### system.health.api.ping + +定义:[api.dever:1:1](api.dever#L1,1) + + + +```mermaid +flowchart TD + n0(["system.health.api.ping"]) + click n0 href "api.dever#L1,12" "打开源码" + n1[["system.health.ping"]] + click n1 href "api.dever#L1,12" "打开源码" + n2(["返回命令结果;未捕获错误终止命令"]) + n0 --> n1 + n1 --> n2 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| system.health.ping | [api.dever:1:12](api.dever#L1,12) | [system.health.ping](link.md#flow-e4086f731ad69c62-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [system.health.ping](app.dever#L5) | 领域内 | CMD 绑定 | [api.dever:1:12](api.dever#L1,12) | + +### system.health.ping + +定义:[app.dever:5:1](app.dever#L5,1) + + + +```mermaid +flowchart TD + n0(["ping"]) + click n0 href "app.dever#L5,1" "打开源码" + n1(["返回"]) + n3["赋值 result"] + click n3 href "app.dever#L6,12" "打开源码" + n0 --> n3 + n3 --> n1 +``` + +没有已收录的下游调用。 diff --git a/examples/cms/dever/module/user/account/README.md b/examples/cms/dever/module/user/account/README.md new file mode 100644 index 0000000..68485ce --- /dev/null +++ b/examples/cms/dever/module/user/account/README.md @@ -0,0 +1,17 @@ +# 账户与会话 + +管理用户、凭据、租户成员和站点会话。 + +- [后台会话接口](doc/api-admin-session.md) +- [前台会话接口](doc/api-front-session.md) +- [账户引导命令](doc/api.md) +- [账户应用](doc/app.md) +- [账户规则](doc/domain.md) +- [用户主体模型](doc/model-principal.md) +- [账户凭据模型](doc/model.md) +- [成员应用](doc/membership-app.md) +- [租户成员模型](doc/membership-model.md) +- [会话应用](doc/session-app.md) +- [会话模型](doc/session-model.md) +- [会话清理任务](doc/session-job.md) +- [调用流程](link.md) diff --git a/examples/cms/dever/module/user/account/api.dever b/examples/cms/dever/module/user/account/api.dever new file mode 100644 index 0000000..a74e2da --- /dev/null +++ b/examples/cms/dever/module/user/account/api.dever @@ -0,0 +1 @@ +long cmd bootstrap = app.bootstrap diff --git a/examples/cms/dever/module/user/account/api/admin/session.dever b/examples/cms/dever/module/user/account/api/admin/session.dever new file mode 100644 index 0000000..4172a42 --- /dev/null +++ b/examples/cms/dever/module/user/account/api/admin/session.dever @@ -0,0 +1,3 @@ +public long post login = app.login + +post logout = app.logout diff --git a/examples/cms/dever/module/user/account/api/front/session.dever b/examples/cms/dever/module/user/account/api/front/session.dever new file mode 100644 index 0000000..4172a42 --- /dev/null +++ b/examples/cms/dever/module/user/account/api/front/session.dever @@ -0,0 +1,3 @@ +public long post login = app.login + +post logout = app.logout diff --git a/examples/cms/dever/module/user/account/app/account.dever b/examples/cms/dever/module/user/account/app/account.dever new file mode 100644 index 0000000..05fa38b --- /dev/null +++ b/examples/cms/dever/module/user/account/app/account.dever @@ -0,0 +1,163 @@ +type AccountView { + id: model.principal.id + uuid: Uuid + email: Text + display_name: Text +} + +type BootstrapView { + tenant_id: platform.tenant.model.id + user_id: model.principal.id + credential_id: model.id +} + +type LoginView { + account: AccountView + tenant_id: platform.tenant.model.id + site: Text +} + +type LogoutView { + revoked: Bool +} + +type Identity { + id: Text + user_id: model.principal.id? + tenant_id: platform.tenant.model.id? +} + +bootstrap( + tenant_key: Text, + tenant_name: Text, + email: Text, + display_name: Text, + password: Secret +) (result: BootstrapView) { + hash = blocking(dever.crypto.passwordHash(password)) + result = registerAccount(tenant_key, tenant_name, email, display_name, hash) +} + +transaction registerAccount( + tenant_key: Text, + tenant_name: Text, + email: Text, + display_name: Text, + hash: Text +) (result: BootstrapView) { + tenant = platform.tenant.provision(tenant_key, tenant_name) + principal = model.principal.create( + { + display_name = display_name + } + ) + stored = model.create( + { + principal_id = principal.id + email = email + password_hash = hash + } + ) + domain.validateEmail(stored.email) + grantOwner(principal.id, tenant.id) + result = BootstrapView { + tenant_id = tenant.id + user_id = principal.id + credential_id = stored.id + } +} + +login(credential_id: model.id, password: Secret, tenant: Text) (result: LoginView) { + dever.api.require(model.exists( + { + where = id == credential_id + } + ), dever.api.Error.Unauthorized) + stored = model.get(credential_id) + principal = model.principal.get(stored.principal_id) + selected = platform.tenant.active(tenant) + dever.api.require(stored.status == model.AccountStatus.Active, dever.api.Error.Unauthorized) + dever.api.require(principal.status == model.principal.PrincipalStatus.Active, dever.api.Error.Unauthorized) + dever.api.require(blocking(dever.crypto.passwordVerify(password, stored.password_hash)), dever.api.Error.Unauthorized) + authenticated = establishLogin(credential_id, selected.key, dever.site.key()) + dever.auth.issueCookie( + authenticated.subject, + uuid.toText(authenticated.session_id), + authenticated.tenant_key + ) + result = authenticated.result +} + +transaction establishLogin(credential_id: model.id, tenant: Text, site: Text) ( + result: LoginView, + subject: Text, + session_id: Uuid, + tenant_key: Text +) { + stored = model.get(credential_id) + principal = model.principal.get(stored.principal_id) + selected = platform.tenant.active(tenant) + dever.api.require(stored.status == model.AccountStatus.Active, dever.api.Error.Unauthorized) + dever.api.require(principal.status == model.principal.PrincipalStatus.Active, dever.api.Error.Unauthorized) + requireMembership(principal.id, selected.id) + session = open( + uuid.toText(principal.uuid), + principal.id, + selected.id, + selected.key, + site + ) + result = LoginView { + account = AccountView { + id = principal.id + uuid = principal.uuid + email = stored.email + display_name = principal.display_name + } + tenant_id = selected.id + site = site + } + subject = uuid.toText(principal.uuid) + session_id = session.uuid + tenant_key = selected.key +} + +logout() (result: LogoutView) { + revoke(uuid.parse(dever.auth.session())) + dever.auth.clearCookie() + result = LogoutView { + revoked = true + } +} + +verify(claims: dever.auth.Claims) (identity: Identity) { + session = authenticate(uuid.parse(claims.session), claims.site) + dever.api.require(session.subject == claims.subject, dever.api.Error.Unauthorized) + domain.requireTenant(session.tenant_key, claims.tenant) + selected = platform.tenant.active(session.tenant_key) + dever.api.require(selected.id == session.tenant_id, dever.api.Error.Unauthorized) + active(session.user_id) + requireMembership(session.user_id, session.tenant_id) + identity = Identity { + id = claims.subject + user_id = session.user_id + tenant_id = session.tenant_id + } +} + +active(user_id: model.principal.id) () { + principal = model.principal.get(user_id) + dever.api.require(principal.status == model.principal.PrincipalStatus.Active, dever.api.Error.Unauthorized) +} + +currentUser() (id: Int) { + id = requireUser(dever.auth.userId()) +} + +requireUser(value: Int) (id: Int) { + id = value +} + +requireUser(value: null) (id: Int) { + fail(dever.api.Error.Unauthorized) +} diff --git a/examples/cms/dever/module/user/account/app/membership.dever b/examples/cms/dever/module/user/account/app/membership.dever new file mode 100644 index 0000000..cbec17a --- /dev/null +++ b/examples/cms/dever/module/user/account/app/membership.dever @@ -0,0 +1,31 @@ +grantOwner( + account_id: model.principal.id, + selected_tenant_id: platform.tenant.model.id +) () { + stored = model.membership.create( + { + user_id = account_id + tenant_id = selected_tenant_id + role = model.membership.MembershipRole.Owner + } + ) + discarded = stored +} + +requireMembership( + account_id: model.principal.id, + selected_tenant_id: platform.tenant.model.id +) () { + stored = model.membership.first( + { + where = user_id == account_id and tenant_id == selected_tenant_id and status == model.membership.MembershipStatus.Active + } + ) + activeMembership(stored) +} + +activeMembership(stored: model.membership.Membership) () {} + +activeMembership(stored: null) () { + fail(dever.api.Error.Unauthorized) +} diff --git a/examples/cms/dever/module/user/account/app/session.dever b/examples/cms/dever/module/user/account/app/session.dever new file mode 100644 index 0000000..36f6fda --- /dev/null +++ b/examples/cms/dever/module/user/account/app/session.dever @@ -0,0 +1,94 @@ +type SessionView { + uuid: Uuid +} + +type AuthSession { + subject: Text + user_id: model.principal.id + tenant_id: platform.tenant.model.id + tenant_key: Text +} + +open( + subject: Text, + account_id: model.principal.id, + selected_tenant_id: platform.tenant.model.id, + selected_tenant_key: Text, + site_key: Text +) (session: SessionView) { + stored = model.session.create( + { + subject = subject + user_id = account_id + tenant_id = selected_tenant_id + tenant_key = selected_tenant_key + site = site_key + expires_at = dever.time.add(dever.time.now(), dever.time.duration(3600000)) + } + ) + session = SessionView { + uuid = stored.uuid + } +} + +authenticate(session_id: Uuid, site_key: Text) (result: AuthSession) { + stored = model.session.first( + { + where = uuid == session_id and site == site_key and revoked == false and expires_at > dever.time.now( + ) + } + ) + result = authSession(stored) +} + +authenticate(session_id: null, site_key: Text) (result: AuthSession) { + fail(dever.api.Error.Unauthorized) +} + +revoke(session_id: Uuid) () { + stored = model.session.first( + { + where = uuid == session_id and revoked == false + } + ) + revokeFound(stored) +} + +revoke(session_id: null) () { + fail(dever.api.Error.Unauthorized) +} + +authSession(stored: model.session.Session) (result: AuthSession) { + result = AuthSession { + subject = stored.subject + user_id = stored.user_id + tenant_id = stored.tenant_id + tenant_key = stored.tenant_key + } +} + +authSession(stored: null) (result: AuthSession) { + fail(dever.api.Error.Unauthorized) +} + +revokeFound(stored: model.session.Session) () { + changed = model.session.update( + stored.id, + { + revoked = true + } + ) + discarded = changed +} + +revokeFound(stored: null) () { + fail(dever.api.Error.Unauthorized) +} + +cleanup() () { + removed = model.session.delete( + { + where = revoked == true or expires_at <= dever.time.now() + } + ) +} diff --git a/examples/cms/dever/module/user/account/doc/api-admin-session.md b/examples/cms/dever/module/user/account/doc/api-admin-session.md new file mode 100644 index 0000000..19bb6d6 --- /dev/null +++ b/examples/cms/dever/module/user/account/doc/api-admin-session.md @@ -0,0 +1,9 @@ +# 后台会话接口 + +## user.account.api.admin.session.login + +校验凭据并建立后台站点会话,成功后设置认证 Cookie。 + +## user.account.api.admin.session.logout + +撤销当前会话并清除认证 Cookie。 diff --git a/examples/cms/dever/module/user/account/doc/api-front-session.md b/examples/cms/dever/module/user/account/doc/api-front-session.md new file mode 100644 index 0000000..6a33d51 --- /dev/null +++ b/examples/cms/dever/module/user/account/doc/api-front-session.md @@ -0,0 +1,9 @@ +# 前台会话接口 + +## user.account.api.front.session.login + +校验凭据并建立前台站点会话,成功后设置认证 Cookie。 + +## user.account.api.front.session.logout + +撤销当前会话并清除认证 Cookie。 diff --git a/examples/cms/dever/module/user/account/doc/api.md b/examples/cms/dever/module/user/account/doc/api.md new file mode 100644 index 0000000..19ea9aa --- /dev/null +++ b/examples/cms/dever/module/user/account/doc/api.md @@ -0,0 +1,5 @@ +# 账户引导命令 + +## user.account.api.bootstrap + +创建租户、用户、凭据和 Owner 成员关系,返回部署所需编号。 diff --git a/examples/cms/dever/module/user/account/doc/app.md b/examples/cms/dever/module/user/account/doc/app.md new file mode 100644 index 0000000..e83399f --- /dev/null +++ b/examples/cms/dever/module/user/account/doc/app.md @@ -0,0 +1,57 @@ +# 账户应用 + +## user.account.AccountView + +登录后公开的用户编号、UUID、邮箱和显示名,不含密码哈希。 + +## user.account.BootstrapView + +引导完成后返回租户、用户和凭据编号,供后续部署命令使用。 + +## user.account.LoginView + +登录成功后的公开账户、租户编号和站点。 + +## user.account.LogoutView + +退出操作是否已撤销会话。 + +## user.account.Identity + +Provider 返回的可信主体、用户编号和租户编号。 + +## user.account.bootstrap + +在线程池计算密码哈希,再创建租户、用户、凭据和 Owner 成员关系。核心站点 Owner 权限仍由部署命令建立。 + +## user.account.registerAccount + +在同一事务中创建租户、用户、邮箱凭据和 Owner 成员记录;邮箱验证失败会回滚。 + +## user.account.login + +校验凭据、用户和租户状态及密码,创建会话并设置当前站点 Cookie。 + +## user.account.establishLogin + +事务中重新检查账号状态和成员关系,创建站点会话,返回签发 Cookie 所需信息。 + +## user.account.logout + +撤销当前会话并清除认证 Cookie。 + +## user.account.verify + +校验会话、主体、租户、用户和成员关系,返回可信身份供核心鉴权。 + +## user.account.active + +要求用户主体已启用,否则返回未授权。 + +## user.account.currentUser + +读取当前认证用户编号,没有用户身份时返回未授权。 + +## user.account.requireUser + +取出非空用户编号,null 返回未授权。 diff --git a/examples/cms/dever/module/user/account/doc/domain.md b/examples/cms/dever/module/user/account/doc/domain.md new file mode 100644 index 0000000..e48062a --- /dev/null +++ b/examples/cms/dever/module/user/account/doc/domain.md @@ -0,0 +1,9 @@ +# 账户规则 + +## user.account.domain.validateEmail + +要求邮箱包含非首尾位置的 @,不验证邮件能否投递。 + +## user.account.domain.requireTenant + +要求认证声明中的租户键与会话一致,缺失或不同返回未授权。 diff --git a/examples/cms/dever/module/user/account/doc/membership-app.md b/examples/cms/dever/module/user/account/doc/membership-app.md new file mode 100644 index 0000000..5e24e5c --- /dev/null +++ b/examples/cms/dever/module/user/account/doc/membership-app.md @@ -0,0 +1,13 @@ +# 成员应用 + +## user.account.grantOwner + +创建租户 Owner 成员记录,不修改核心站点角色。 + +## user.account.requireMembership + +要求用户与租户之间存在启用的成员关系。 + +## user.account.activeMembership + +找到有效成员记录时通过,否则返回未授权。 diff --git a/examples/cms/dever/module/user/account/doc/membership-model.md b/examples/cms/dever/module/user/account/doc/membership-model.md new file mode 100644 index 0000000..b4888dc --- /dev/null +++ b/examples/cms/dever/module/user/account/doc/membership-model.md @@ -0,0 +1,13 @@ +# 租户成员模型 + +## user.account.model.membership.MembershipRole + +租户成员的 Owner、Editor 或 Reader 身份。 + +## user.account.model.membership.MembershipStatus + +成员关系的启用或禁用状态。 + +## user.account.model.membership.Membership + +平台库中的用户与租户关系,每对用户和租户只能有一条。 diff --git a/examples/cms/dever/module/user/account/doc/model-principal.md b/examples/cms/dever/module/user/account/doc/model-principal.md new file mode 100644 index 0000000..e9cc659 --- /dev/null +++ b/examples/cms/dever/module/user/account/doc/model-principal.md @@ -0,0 +1,9 @@ +# 用户主体模型 + +## user.account.model.principal.PrincipalStatus + +用户主体的启用或禁用状态。 + +## user.account.model.principal.Principal + +平台用户主体,保存自动生成的 UUID、显示名和状态。 diff --git a/examples/cms/dever/module/user/account/doc/model.md b/examples/cms/dever/module/user/account/doc/model.md new file mode 100644 index 0000000..2f1441a --- /dev/null +++ b/examples/cms/dever/module/user/account/doc/model.md @@ -0,0 +1,9 @@ +# 账户凭据模型 + +## user.account.model.AccountStatus + +登录凭据的启用或禁用状态。 + +## user.account.model.Account + +平台登录凭据,关联用户主体;邮箱规范化且唯一,密码哈希为私有原文。 diff --git a/examples/cms/dever/module/user/account/doc/session-app.md b/examples/cms/dever/module/user/account/doc/session-app.md new file mode 100644 index 0000000..fd7516a --- /dev/null +++ b/examples/cms/dever/module/user/account/doc/session-app.md @@ -0,0 +1,33 @@ +# 会话应用 + +## user.account.SessionView + +新会话的 UUID。 + +## user.account.AuthSession + +已认证会话的主体、用户和租户信息。 + +## user.account.open + +创建有效期一小时的站点会话。 + +## user.account.authenticate + +按会话 UUID 和站点查找未撤销、未过期的会话;无效时返回未授权。 + +## user.account.revoke + +按 UUID 查找并撤销尚未撤销的会话;无效编号或缺失记录返回未授权。 + +## user.account.authSession + +把已找到的会话转换为认证信息;空记录返回未授权。 + +## user.account.revokeFound + +标记已有会话为已撤销;空记录返回未授权。 + +## user.account.cleanup + +删除已撤销或已过期的会话。 diff --git a/examples/cms/dever/module/user/account/doc/session-job.md b/examples/cms/dever/module/user/account/doc/session-job.md new file mode 100644 index 0000000..8991e89 --- /dev/null +++ b/examples/cms/dever/module/user/account/doc/session-job.md @@ -0,0 +1,5 @@ +# 会话清理任务 + +## user.account.job.cleanup + +每小时清理失效会话,最多执行 3 次(含首次),单次期限 30 秒。 diff --git a/examples/cms/dever/module/user/account/doc/session-model.md b/examples/cms/dever/module/user/account/doc/session-model.md new file mode 100644 index 0000000..fb870f1 --- /dev/null +++ b/examples/cms/dever/module/user/account/doc/session-model.md @@ -0,0 +1,5 @@ +# 会话模型 + +## user.account.model.session.Session + +平台会话记录,包含主体、用户、租户、站点、到期时间和撤销标记。 diff --git a/examples/cms/dever/module/user/account/domain.dever b/examples/cms/dever/module/user/account/domain.dever new file mode 100644 index 0000000..b14eca2 --- /dev/null +++ b/examples/cms/dever/module/user/account/domain.dever @@ -0,0 +1,17 @@ +validateEmail(address: Text) () pure { + dever.api.require( + text.contains(address, "@") and not text.startsWith(address, "@") and not text.endsWith( + address, + "@" + ), + dever.api.Error.Invalid + ) +} + +requireTenant(expected: Text, actual: Text) () pure { + dever.api.require(expected == actual, dever.api.Error.Unauthorized) +} + +requireTenant(expected: Text, actual: null) () pure { + fail(dever.api.Error.Unauthorized) +} diff --git a/examples/cms/dever/module/user/account/job.dever b/examples/cms/dever/module/user/account/job.dever new file mode 100644 index 0000000..734c1b4 --- /dev/null +++ b/examples/cms/dever/module/user/account/job.dever @@ -0,0 +1,7 @@ +database default + +job cleanup() () retry(3) timeout(30000) { + app.cleanup() +} + +schedule cleanup = "0 * * * *" diff --git a/examples/cms/dever/module/user/account/link.md b/examples/cms/dever/module/user/account/link.md new file mode 100644 index 0000000..27f8b57 --- /dev/null +++ b/examples/cms/dever/module/user/account/link.md @@ -0,0 +1,1633 @@ + + + +# user.account + +按求值顺序展示调用、分支和任务调度,包含 API 与自动 REST 入口。点击节点打开源码;双框为调用,虚线为调度或错误路径。外部库和驱动不展开。 + +图不是某次运行的日志,分句和 Adapter 按输入或配置选择。修改源码后运行 `dever inspect . --links`。 + +## 入口 + +- API [user.account.api.admin.session.login](api/admin/session.dever#L1) → [user.account.login](app/account.dever#L70) +- API [user.account.api.admin.session.logout](api/admin/session.dever#L3) → [user.account.logout](app/account.dever#L125) +- CMD [user.account.api.bootstrap](api.dever#L1) → [user.account.bootstrap](app/account.dever#L30) +- API [user.account.api.front.session.login](api/front/session.dever#L1) → [user.account.login](app/account.dever#L70) +- API [user.account.api.front.session.logout](api/front/session.dever#L3) → [user.account.logout](app/account.dever#L125) +- Job [user.account.job.cleanup](job.dever#L3) + +## 外部调用方 + +| 能力 | 调用方 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [user.account.currentUser](app/account.dever#L153) | [news.article.create](../../news/article/app/editing.dever#L16) | 调用 | [../../news/article/app/editing.dever:28:19](../../news/article/app/editing.dever#L28,19) | +| [user.account.currentUser](app/account.dever#L153) | [news.article.list](../../news/article/app/editing.dever#L39) | 调用 | [../../news/article/app/editing.dever:40:12](../../news/article/app/editing.dever#L40,12) | +| [user.account.currentUser](app/account.dever#L153) | [news.article.detail](../../news/article/app/editing.dever#L51) | 调用 | [../../news/article/app/editing.dever:53:41](../../news/article/app/editing.dever#L53,41) | +| [user.account.currentUser](app/account.dever#L153) | [news.article.remove](../../news/article/app/editing.dever#L65) | 调用 | [../../news/article/app/editing.dever:67:41](../../news/article/app/editing.dever#L67,41) | +| [user.account.currentUser](app/account.dever#L153) | [news.article.edit](../../news/article/app/editing.dever#L77) | 调用 | [../../news/article/app/editing.dever:86:41](../../news/article/app/editing.dever#L86,41) | +| [user.account.currentUser](app/account.dever#L153) | [news.article.requireSchedulable](../../news/article/app/editing.dever#L107) | 调用 | [../../news/article/app/editing.dever:109:41](../../news/article/app/editing.dever#L109,41) | +| [user.account.currentUser](app/account.dever#L153) | [news.article.publish](../../news/article/app/publication.dever#L1) | 调用 | [../../news/article/app/publication.dever:3:41](../../news/article/app/publication.dever#L3,41) | +| [user.account.verify](app/account.dever#L133) | [content.category.api.admin.manage.create](../../content/category/api/admin/manage.dever#L1) | 身份验证 | [../../content/category/api/admin/manage.dever:1:15](../../content/category/api/admin/manage.dever#L1,15) | +| [user.account.verify](app/account.dever#L133) | [content.category.api.admin.manage.disable](../../content/category/api/admin/manage.dever#L3) | 身份验证 | [../../content/category/api/admin/manage.dever:3:18](../../content/category/api/admin/manage.dever#L3,18) | +| [user.account.verify](app/account.dever#L133) | [content.revision.api.admin.manage.create](../../content/revision/api/admin/manage.dever#L1) | 身份验证 | [../../content/revision/api/admin/manage.dever:1:15](../../content/revision/api/admin/manage.dever#L1,15) | +| [user.account.verify](app/account.dever#L133) | [media.asset.api.admin.manage.upload](../../media/asset/api/admin/manage.dever#L1) | 身份验证 | [../../media/asset/api/admin/manage.dever:1:20](../../media/asset/api/admin/manage.dever#L1,20) | +| [user.account.verify](app/account.dever#L133) | [news.article.api.admin.manage.list](../../news/article/api/admin/manage.dever#L1) | 身份验证 | [../../news/article/api/admin/manage.dever:1:12](../../news/article/api/admin/manage.dever#L1,12) | +| [user.account.verify](app/account.dever#L133) | [news.article.api.admin.manage.detail](../../news/article/api/admin/manage.dever#L3) | 身份验证 | [../../news/article/api/admin/manage.dever:3:14](../../news/article/api/admin/manage.dever#L3,14) | +| [user.account.verify](app/account.dever#L133) | [news.article.api.admin.manage.create](../../news/article/api/admin/manage.dever#L5) | 身份验证 | [../../news/article/api/admin/manage.dever:5:15](../../news/article/api/admin/manage.dever#L5,15) | +| [user.account.verify](app/account.dever#L133) | [news.article.api.admin.manage.remove](../../news/article/api/admin/manage.dever#L7) | 身份验证 | [../../news/article/api/admin/manage.dever:7:17](../../news/article/api/admin/manage.dever#L7,17) | +| [user.account.verify](app/account.dever#L133) | [news.article.api.admin.manage.publish](../../news/article/api/admin/manage.dever#L9) | 身份验证 | [../../news/article/api/admin/manage.dever:9:16](../../news/article/api/admin/manage.dever#L9,16) | +| [user.account.verify](app/account.dever#L133) | [news.article.api.front.browse.list](../../news/article/api/front/browse.dever#L1) | 身份验证 | [../../news/article/api/front/browse.dever:1:12](../../news/article/api/front/browse.dever#L1,12) | +| [user.account.verify](app/account.dever#L133) | [news.article.api.front.browse.detail](../../news/article/api/front/browse.dever#L3) | 身份验证 | [../../news/article/api/front/browse.dever:3:14](../../news/article/api/front/browse.dever#L3,14) | +| [user.account.verify](app/account.dever#L133) | [publishing.schedule.api.admin.manage.schedule](../../publishing/schedule/api/admin/manage.dever#L1) | 身份验证 | [../../publishing/schedule/api/admin/manage.dever:1:17](../../publishing/schedule/api/admin/manage.dever#L1,17) | +| [user.account.verify](app/account.dever#L133) | [user.authorization.api.admin.manage.permissions](../authorization/api/admin/manage.dever#L1) | 身份验证 | [../authorization/api/admin/manage.dever:1:19](../authorization/api/admin/manage.dever#L1,19) | +| [user.account.verify](app/account.dever#L133) | [user.authorization.api.admin.manage.save](../authorization/api/admin/manage.dever#L3) | 身份验证 | [../authorization/api/admin/manage.dever:3:13](../authorization/api/admin/manage.dever#L3,13) | +| [user.account.verify](app/account.dever#L133) | [user.authorization.api.admin.manage.grant](../authorization/api/admin/manage.dever#L5) | 身份验证 | [../authorization/api/admin/manage.dever:5:14](../authorization/api/admin/manage.dever#L5,14) | +| [user.account.verify](app/account.dever#L133) | [user.authorization.api.admin.manage.revoke](../authorization/api/admin/manage.dever#L7) | 身份验证 | [../authorization/api/admin/manage.dever:7:17](../authorization/api/admin/manage.dever#L7,17) | +| [user.account.verify](app/account.dever#L133) | [user.authorization.api.admin.manage.disable](../authorization/api/admin/manage.dever#L9) | 身份验证 | [../authorization/api/admin/manage.dever:9:18](../authorization/api/admin/manage.dever#L9,18) | +| [user.account.verify](app/account.dever#L133) | [user.authorization.api.front.manage.permissions](../authorization/api/front/manage.dever#L1) | 身份验证 | [../authorization/api/front/manage.dever:1:19](../authorization/api/front/manage.dever#L1,19) | +| [user.account.verify](app/account.dever#L133) | [user.authorization.api.front.manage.save](../authorization/api/front/manage.dever#L3) | 身份验证 | [../authorization/api/front/manage.dever:3:13](../authorization/api/front/manage.dever#L3,13) | +| [user.account.verify](app/account.dever#L133) | [user.authorization.api.front.manage.grant](../authorization/api/front/manage.dever#L5) | 身份验证 | [../authorization/api/front/manage.dever:5:14](../authorization/api/front/manage.dever#L5,14) | +| [user.account.verify](app/account.dever#L133) | [user.authorization.api.front.manage.revoke](../authorization/api/front/manage.dever#L7) | 身份验证 | [../authorization/api/front/manage.dever:7:17](../authorization/api/front/manage.dever#L7,17) | +| [user.account.verify](app/account.dever#L133) | [user.authorization.api.front.manage.disable](../authorization/api/front/manage.dever#L9) | 身份验证 | [../authorization/api/front/manage.dever:9:18](../authorization/api/front/manage.dever#L9,18) | + +## 能力与内部调用 + +### user.account.AccountView + +定义:[app/account.dever:1:1](app/account.dever#L1,1) + +没有已收录的下游调用。 + +### user.account.AuthSession + +定义:[app/session.dever:5:1](app/session.dever#L5,1) + +没有已收录的下游调用。 + +### user.account.BootstrapView + +定义:[app/account.dever:8:1](app/account.dever#L8,1) + +没有已收录的下游调用。 + +### user.account.Identity + +定义:[app/account.dever:24:1](app/account.dever#L24,1) + +没有已收录的下游调用。 + +### user.account.LoginView + +定义:[app/account.dever:14:1](app/account.dever#L14,1) + +没有已收录的下游调用。 + +### user.account.LogoutView + +定义:[app/account.dever:20:1](app/account.dever#L20,1) + +没有已收录的下游调用。 + +### user.account.SessionView + +定义:[app/session.dever:1:1](app/session.dever#L1,1) + +没有已收录的下游调用。 + +### user.account.active + +定义:[app/account.dever:148:1](app/account.dever#L148,1) + + + +```mermaid +flowchart TD + n0(["active"]) + click n0 href "app/account.dever#L148,1" "打开源码" + n1(["返回"]) + n3["user.account.model.principal.get"] + click n3 href "app/account.dever#L149,15" "打开源码" + n4["赋值 principal"] + click n4 href "app/account.dever#L149,15" "打开源码" + n5[["dever.api.require"]] + click n5 href "app/account.dever#L150,3" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n5 + n5 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| dever.api.require | [app/account.dever:150:3](app/account.dever#L150,3) | 依赖或生成的分派(本页不展开) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [user.account.model.principal.get](model/principal.dever#L6) | 领域内 | 内建操作 | [app/account.dever:149:15](app/account.dever#L149,15) | +| `dever.api.require` | 依赖 | 调用 | [app/account.dever:150:3](app/account.dever#L150,3) | + +### user.account.activeMembership + +定义:[app/membership.dever:27:1](app/membership.dever#L27,1)、[app/membership.dever:29:1](app/membership.dever#L29,1) + + + +```mermaid +flowchart TD + n0(["activeMembership"]) + click n0 href "app/membership.dever#L27,1" "打开源码" + n1(["返回"]) + n2(["传播错误"]) + n3{"按实参匹配分句(互斥)"} + n4["stored: model.membership.Membership"] + click n4 href "app/membership.dever#L27,1" "打开源码" + n5["stored: null"] + click n5 href "app/membership.dever#L29,1" "打开源码" + n6["fail dever.api.Error.Unauthorized"] + click n6 href "app/membership.dever#L30,8" "打开源码" + n0 --> n3 + n3 -->|"stored: model.membership.Membership"| n4 + n4 --> n1 + n3 -->|"stored: null"| n5 + n5 --> n6 + n6 --> n2 +``` + +没有已收录的下游调用。 + +### user.account.api.admin.session.login + +定义:[api/admin/session.dever:1:1](api/admin/session.dever#L1,1) + + + +```mermaid +flowchart TD + n0(["user.account.api.admin.session.login"]) + click n0 href "api/admin/session.dever#L1,26" "打开源码" + n1{"路由 POST /user/account/admin/session/login,topic #34;admin/session#34; 接受请求?"} + n2(["返回 HTTP 404"]) + n3["进入 API"] + n4["取得认证时间"] + n5(["返回 HTTP 500"]) + n6["按 topic #34;admin/session#34; 选择站点与 Provider"] + n7(["返回 HTTP 403"]) + n8{"读取显式 Authorization;public 忽略自动 Cookie"} + n9(["返回 HTTP 401"]) + n10["认证上下文就绪"] + n11["校验 Token 的签名、有效期及站点"] + n12(["返回 HTTP 401"]) + n13{"按 Provider.verify 选择验证函数"} + n14[["user.account.verify"]] + click n14 href "api/admin/session.dever#L1,26" "打开源码" + n15(["返回 HTTP 业务错误映射状态;其他故障 500"]) + n16["用验证结果建立身份"] + n17(["返回 HTTP 认证错误状态"]) + n18(["返回 HTTP 500"]) + n19["建立请求数据库上下文"] + n20["失败:保留 HTTP 错误状态"] + n21(["返回错误响应"]) + n22["读取 API 输入"] + n23["解码具体参数并检查字段合同"] + n24[["user.account.login"]] + click n24 href "api/admin/session.dever#L1,26" "打开源码" + n25["按输出类型检查可序列化值并编码响应"] + n26["发布暂存的认证 Header / Cookie"] + n27(["返回成功响应"]) + n0 --> n1 + n1 -->|"未匹配此入口:继续路由;均未匹配"| n2 + n1 --> n3 + n3 --> n4 + n4 -->|"失败"| n5 + n4 --> n6 + n6 -->|"无站点绑定"| n7 + n6 --> n8 + n8 -->|"凭据格式错误或冲突"| n9 + n8 -->|"没有显式凭据:匿名"| n10 + n8 -->|"有 Bearer"| n11 + n11 -->|"无 Provider 或 Token 无效"| n12 + n11 --> n13 + n13 -->|"user.account.verify"| n14 + n14 -->|"验证函数失败"| n15 + n14 --> n16 + n16 -->|"身份不合法"| n17 + n16 -->|"通过"| n10 + n13 -->|"无匹配验证函数"| n18 + n10 --> n19 + n20 --> n21 + n19 --> n22 + n22 -->|"失败:输入错误状态"| n20 + n22 --> n23 + n23 -->|"失败:400"| n20 + n23 --> n24 + n24 -->|"业务错误映射状态;其他故障:500"| n20 + n24 --> n25 + n25 -->|"编码失败:500"| n20 + n25 --> n26 + n26 --> n27 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.verify | [api/admin/session.dever:1:26](api/admin/session.dever#L1,26) | [user.account.verify](link.md#flow-aaaacb87d61f819c-1) | +| user.account.login | [api/admin/session.dever:1:26](api/admin/session.dever#L1,26) | [user.account.login](link.md#flow-606752223302c551-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [user.account.login](app/account.dever#L70) | 领域内 | API 绑定 | [api/admin/session.dever:1:26](api/admin/session.dever#L1,26) | +| [user.account.verify](app/account.dever#L133) | 领域内 | 身份验证 | [api/admin/session.dever:1:26](api/admin/session.dever#L1,26) | + +### user.account.api.admin.session.logout + +定义:[api/admin/session.dever:3:1](api/admin/session.dever#L3,1) + + + +```mermaid +flowchart TD + n0(["user.account.api.admin.session.logout"]) + click n0 href "api/admin/session.dever#L3,15" "打开源码" + n1{"路由 POST /user/account/admin/session/logout,topic #34;admin/session#34; 接受请求?"} + n2(["返回 HTTP 404"]) + n3["进入 API"] + n4["取得认证时间"] + n5(["返回 HTTP 500"]) + n6["按 topic #34;admin/session#34; 选择站点与 Provider"] + n7(["返回 HTTP 403"]) + n8{"读取 Bearer 或站点 Cookie;两者互斥"} + n9(["返回 HTTP 401"]) + n10["认证上下文就绪"] + n11(["返回 HTTP 401"]) + n12["校验 Token 的签名、有效期及站点"] + n13{"Cookie 且非 GET / HEAD / OPTIONS?"} + n14["检查站点 Origin"] + n15(["返回 HTTP 403"]) + n16(["返回 HTTP 401"]) + n17{"按 Provider.verify 选择验证函数"} + n18[["user.account.verify"]] + click n18 href "api/admin/session.dever#L3,15" "打开源码" + n19(["返回 HTTP 业务错误映射状态;其他故障 500"]) + n20["用验证结果建立身份"] + n21(["返回 HTTP 认证错误状态"]) + n22(["返回 HTTP 500"]) + n23["建立请求数据库上下文"] + n24{"有已验证租户?"} + n25["数据库上下文就绪"] + n26["按已验证租户选择数据库"] + n27(["返回 HTTP 503"]) + n28{"检查租户组件启用状态:"} + n29(["返回 HTTP 403"]) + n30(["返回 HTTP 503"]) + n31["进入写入口事务"] + n32["失败:保留 HTTP 错误状态"] + n33["回滚入口拥有的事务"] + n34(["返回 HTTP 500"]) + n35(["返回错误响应"]) + n36{"权限检查:user.account.{site}.logout"} + n37["权限通过"] + n38["读取 API 输入"] + n39["解码具体参数并检查字段合同"] + n40[["user.account.logout"]] + click n40 href "api/admin/session.dever#L3,15" "打开源码" + n41["按输出类型检查可序列化值并编码响应"] + n42["提交入口拥有的事务"] + n43(["返回 HTTP 500"]) + n44["发布暂存的认证 Header / Cookie"] + n45(["返回成功响应"]) + n0 --> n1 + n1 -->|"未匹配此入口:继续路由;均未匹配"| n2 + n1 --> n3 + n3 --> n4 + n4 -->|"失败"| n5 + n4 --> n6 + n6 -->|"无站点绑定"| n7 + n6 --> n8 + n8 -->|"凭据格式错误或冲突"| n9 + n8 -->|"无凭据"| n11 + n8 -->|"有凭据"| n13 + n13 -->|"否"| n12 + n13 -->|"是"| n14 + n14 -->|"不符合"| n15 + n14 -->|"通过"| n12 + n12 -->|"无 Provider 或 Token 无效"| n16 + n12 --> n17 + n17 -->|"user.account.verify"| n18 + n18 -->|"验证函数失败"| n19 + n18 --> n20 + n20 -->|"身份不合法"| n21 + n20 -->|"通过"| n10 + n17 -->|"无匹配验证函数"| n22 + n10 --> n23 + n23 --> n24 + n24 -->|"否"| n25 + n24 -->|"是"| n26 + n26 -->|"选择失败"| n27 + n26 --> n28 + n28 -->|"未启用"| n29 + n28 -->|"查询失败"| n30 + n28 -->|"允许"| n25 + n25 --> n31 + n32 --> n33 + n33 -->|"事务结束失败"| n34 + n33 --> n35 + n31 --> n36 + n36 -->|"拒绝:Access 状态;查询故障:500"| n32 + n36 -->|"允许"| n37 + n37 --> n38 + n38 -->|"失败:输入错误状态"| n32 + n38 --> n39 + n39 -->|"失败:400"| n32 + n39 --> n40 + n40 -->|"业务错误映射状态;其他故障:500"| n32 + n40 --> n41 + n41 -->|"编码失败:500"| n32 + n41 --> n42 + n42 -->|"提交失败"| n43 + n42 --> n44 + n44 --> n45 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.verify | [api/admin/session.dever:3:15](api/admin/session.dever#L3,15) | [user.account.verify](link.md#flow-aaaacb87d61f819c-1) | +| user.account.logout | [api/admin/session.dever:3:15](api/admin/session.dever#L3,15) | [user.account.logout](link.md#flow-060100ede2755748-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [user.account.logout](app/account.dever#L125) | 领域内 | API 绑定 | [api/admin/session.dever:3:15](api/admin/session.dever#L3,15) | +| [user.account.verify](app/account.dever#L133) | 领域内 | 身份验证 | [api/admin/session.dever:3:15](api/admin/session.dever#L3,15) | + +### user.account.api.bootstrap + +定义:[api.dever:1:1](api.dever#L1,1) + + + +```mermaid +flowchart TD + n0(["user.account.api.bootstrap"]) + click n0 href "api.dever#L1,22" "打开源码" + n1[["user.account.bootstrap"]] + click n1 href "api.dever#L1,22" "打开源码" + n2(["返回命令结果;未捕获错误终止命令"]) + n0 --> n1 + n1 --> n2 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.bootstrap | [api.dever:1:22](api.dever#L1,22) | [user.account.bootstrap](link.md#flow-71461691e97d1a48-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [user.account.bootstrap](app/account.dever#L30) | 领域内 | CMD 绑定 | [api.dever:1:22](api.dever#L1,22) | + +### user.account.api.front.session.login + +定义:[api/front/session.dever:1:1](api/front/session.dever#L1,1) + + + +```mermaid +flowchart TD + n0(["user.account.api.front.session.login"]) + click n0 href "api/front/session.dever#L1,26" "打开源码" + n1{"路由 POST /user/account/front/session/login,topic #34;front/session#34; 接受请求?"} + n2(["返回 HTTP 404"]) + n3["进入 API"] + n4["取得认证时间"] + n5(["返回 HTTP 500"]) + n6["按 topic #34;front/session#34; 选择站点与 Provider"] + n7(["返回 HTTP 403"]) + n8{"读取显式 Authorization;public 忽略自动 Cookie"} + n9(["返回 HTTP 401"]) + n10["认证上下文就绪"] + n11["校验 Token 的签名、有效期及站点"] + n12(["返回 HTTP 401"]) + n13{"按 Provider.verify 选择验证函数"} + n14[["user.account.verify"]] + click n14 href "api/front/session.dever#L1,26" "打开源码" + n15(["返回 HTTP 业务错误映射状态;其他故障 500"]) + n16["用验证结果建立身份"] + n17(["返回 HTTP 认证错误状态"]) + n18(["返回 HTTP 500"]) + n19["建立请求数据库上下文"] + n20["失败:保留 HTTP 错误状态"] + n21(["返回错误响应"]) + n22["读取 API 输入"] + n23["解码具体参数并检查字段合同"] + n24[["user.account.login"]] + click n24 href "api/front/session.dever#L1,26" "打开源码" + n25["按输出类型检查可序列化值并编码响应"] + n26["发布暂存的认证 Header / Cookie"] + n27(["返回成功响应"]) + n0 --> n1 + n1 -->|"未匹配此入口:继续路由;均未匹配"| n2 + n1 --> n3 + n3 --> n4 + n4 -->|"失败"| n5 + n4 --> n6 + n6 -->|"无站点绑定"| n7 + n6 --> n8 + n8 -->|"凭据格式错误或冲突"| n9 + n8 -->|"没有显式凭据:匿名"| n10 + n8 -->|"有 Bearer"| n11 + n11 -->|"无 Provider 或 Token 无效"| n12 + n11 --> n13 + n13 -->|"user.account.verify"| n14 + n14 -->|"验证函数失败"| n15 + n14 --> n16 + n16 -->|"身份不合法"| n17 + n16 -->|"通过"| n10 + n13 -->|"无匹配验证函数"| n18 + n10 --> n19 + n20 --> n21 + n19 --> n22 + n22 -->|"失败:输入错误状态"| n20 + n22 --> n23 + n23 -->|"失败:400"| n20 + n23 --> n24 + n24 -->|"业务错误映射状态;其他故障:500"| n20 + n24 --> n25 + n25 -->|"编码失败:500"| n20 + n25 --> n26 + n26 --> n27 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.verify | [api/front/session.dever:1:26](api/front/session.dever#L1,26) | [user.account.verify](link.md#flow-aaaacb87d61f819c-1) | +| user.account.login | [api/front/session.dever:1:26](api/front/session.dever#L1,26) | [user.account.login](link.md#flow-606752223302c551-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [user.account.login](app/account.dever#L70) | 领域内 | API 绑定 | [api/front/session.dever:1:26](api/front/session.dever#L1,26) | +| [user.account.verify](app/account.dever#L133) | 领域内 | 身份验证 | [api/front/session.dever:1:26](api/front/session.dever#L1,26) | + +### user.account.api.front.session.logout + +定义:[api/front/session.dever:3:1](api/front/session.dever#L3,1) + + + +```mermaid +flowchart TD + n0(["user.account.api.front.session.logout"]) + click n0 href "api/front/session.dever#L3,15" "打开源码" + n1{"路由 POST /user/account/front/session/logout,topic #34;front/session#34; 接受请求?"} + n2(["返回 HTTP 404"]) + n3["进入 API"] + n4["取得认证时间"] + n5(["返回 HTTP 500"]) + n6["按 topic #34;front/session#34; 选择站点与 Provider"] + n7(["返回 HTTP 403"]) + n8{"读取 Bearer 或站点 Cookie;两者互斥"} + n9(["返回 HTTP 401"]) + n10["认证上下文就绪"] + n11(["返回 HTTP 401"]) + n12["校验 Token 的签名、有效期及站点"] + n13{"Cookie 且非 GET / HEAD / OPTIONS?"} + n14["检查站点 Origin"] + n15(["返回 HTTP 403"]) + n16(["返回 HTTP 401"]) + n17{"按 Provider.verify 选择验证函数"} + n18[["user.account.verify"]] + click n18 href "api/front/session.dever#L3,15" "打开源码" + n19(["返回 HTTP 业务错误映射状态;其他故障 500"]) + n20["用验证结果建立身份"] + n21(["返回 HTTP 认证错误状态"]) + n22(["返回 HTTP 500"]) + n23["建立请求数据库上下文"] + n24{"有已验证租户?"} + n25["数据库上下文就绪"] + n26["按已验证租户选择数据库"] + n27(["返回 HTTP 503"]) + n28{"检查租户组件启用状态:"} + n29(["返回 HTTP 403"]) + n30(["返回 HTTP 503"]) + n31["进入写入口事务"] + n32["失败:保留 HTTP 错误状态"] + n33["回滚入口拥有的事务"] + n34(["返回 HTTP 500"]) + n35(["返回错误响应"]) + n36{"权限检查:user.account.{site}.logout"} + n37["权限通过"] + n38["读取 API 输入"] + n39["解码具体参数并检查字段合同"] + n40[["user.account.logout"]] + click n40 href "api/front/session.dever#L3,15" "打开源码" + n41["按输出类型检查可序列化值并编码响应"] + n42["提交入口拥有的事务"] + n43(["返回 HTTP 500"]) + n44["发布暂存的认证 Header / Cookie"] + n45(["返回成功响应"]) + n0 --> n1 + n1 -->|"未匹配此入口:继续路由;均未匹配"| n2 + n1 --> n3 + n3 --> n4 + n4 -->|"失败"| n5 + n4 --> n6 + n6 -->|"无站点绑定"| n7 + n6 --> n8 + n8 -->|"凭据格式错误或冲突"| n9 + n8 -->|"无凭据"| n11 + n8 -->|"有凭据"| n13 + n13 -->|"否"| n12 + n13 -->|"是"| n14 + n14 -->|"不符合"| n15 + n14 -->|"通过"| n12 + n12 -->|"无 Provider 或 Token 无效"| n16 + n12 --> n17 + n17 -->|"user.account.verify"| n18 + n18 -->|"验证函数失败"| n19 + n18 --> n20 + n20 -->|"身份不合法"| n21 + n20 -->|"通过"| n10 + n17 -->|"无匹配验证函数"| n22 + n10 --> n23 + n23 --> n24 + n24 -->|"否"| n25 + n24 -->|"是"| n26 + n26 -->|"选择失败"| n27 + n26 --> n28 + n28 -->|"未启用"| n29 + n28 -->|"查询失败"| n30 + n28 -->|"允许"| n25 + n25 --> n31 + n32 --> n33 + n33 -->|"事务结束失败"| n34 + n33 --> n35 + n31 --> n36 + n36 -->|"拒绝:Access 状态;查询故障:500"| n32 + n36 -->|"允许"| n37 + n37 --> n38 + n38 -->|"失败:输入错误状态"| n32 + n38 --> n39 + n39 -->|"失败:400"| n32 + n39 --> n40 + n40 -->|"业务错误映射状态;其他故障:500"| n32 + n40 --> n41 + n41 -->|"编码失败:500"| n32 + n41 --> n42 + n42 -->|"提交失败"| n43 + n42 --> n44 + n44 --> n45 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.verify | [api/front/session.dever:3:15](api/front/session.dever#L3,15) | [user.account.verify](link.md#flow-aaaacb87d61f819c-1) | +| user.account.logout | [api/front/session.dever:3:15](api/front/session.dever#L3,15) | [user.account.logout](link.md#flow-060100ede2755748-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [user.account.logout](app/account.dever#L125) | 领域内 | API 绑定 | [api/front/session.dever:3:15](api/front/session.dever#L3,15) | +| [user.account.verify](app/account.dever#L133) | 领域内 | 身份验证 | [api/front/session.dever:3:15](api/front/session.dever#L3,15) | + +### user.account.authSession + +定义:[app/session.dever:61:1](app/session.dever#L61,1)、[app/session.dever:70:1](app/session.dever#L70,1) + + + +```mermaid +flowchart TD + n0(["authSession"]) + click n0 href "app/session.dever#L61,1" "打开源码" + n1(["返回"]) + n2(["传播错误"]) + n3{"按实参匹配分句(互斥)"} + n4["stored: model.session.Session"] + click n4 href "app/session.dever#L61,1" "打开源码" + n5["赋值 result"] + click n5 href "app/session.dever#L62,12" "打开源码" + n6["stored: null"] + click n6 href "app/session.dever#L70,1" "打开源码" + n7["fail dever.api.Error.Unauthorized"] + click n7 href "app/session.dever#L71,8" "打开源码" + n0 --> n3 + n3 -->|"stored: model.session.Session"| n4 + n4 --> n5 + n5 --> n1 + n3 -->|"stored: null"| n6 + n6 --> n7 + n7 --> n2 +``` + +没有已收录的下游调用。 + +### user.account.authenticate + +定义:[app/session.dever:34:1](app/session.dever#L34,1)、[app/session.dever:44:1](app/session.dever#L44,1) + + + +```mermaid +flowchart TD + n0(["authenticate"]) + click n0 href "app/session.dever#L34,1" "打开源码" + n1(["返回"]) + n2(["传播错误"]) + n3{"按实参匹配分句(互斥)"} + n4["session_id: Uuid"] + click n4 href "app/session.dever#L34,1" "打开源码" + n5[["dever.time.now"]] + click n5 href "app/session.dever#L37,93" "打开源码" + n6["user.account.model.session.first"] + click n6 href "app/session.dever#L35,12" "打开源码" + n7["赋值 stored"] + click n7 href "app/session.dever#L35,12" "打开源码" + n8[["user.account.authSession"]] + click n8 href "app/session.dever#L41,12" "打开源码" + n9["赋值 result"] + click n9 href "app/session.dever#L41,12" "打开源码" + n10["session_id: null"] + click n10 href "app/session.dever#L44,1" "打开源码" + n11["fail dever.api.Error.Unauthorized"] + click n11 href "app/session.dever#L45,8" "打开源码" + n0 --> n3 + n3 -->|"session_id: Uuid"| n4 + n4 --> n5 + n5 --> n6 + n6 --> n7 + n7 --> n8 + n8 --> n9 + n9 --> n1 + n3 -->|"session_id: null"| n10 + n10 --> n11 + n11 --> n2 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| dever.time.now | [app/session.dever:37:93](app/session.dever#L37,93) | 依赖或生成的分派(本页不展开) | +| user.account.authSession | [app/session.dever:41:12](app/session.dever#L41,12) | [user.account.authSession](link.md#flow-906878ee60b70782-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [user.account.model.session.first](model/session.dever#L1) | 领域内 | 内建操作 | [app/session.dever:35:12](app/session.dever#L35,12) | +| `dever.time.now` | 依赖 | 调用 | [app/session.dever:37:93](app/session.dever#L37,93) | +| [user.account.authSession](app/session.dever#L61) | 领域内 | 调用 | [app/session.dever:41:12](app/session.dever#L41,12) | + +### user.account.bootstrap + +定义:[app/account.dever:30:1](app/account.dever#L30,1) + + + +```mermaid +flowchart TD + n0(["bootstrap"]) + click n0 href "app/account.dever#L30,1" "打开源码" + n1(["返回"]) + n3[["阻塞线程:dever.crypto.passwordHash"]] + click n3 href "app/account.dever#L37,10" "打开源码" + n4["等待任务完成并取得结果"] + click n4 href "app/account.dever#L37,10" "打开源码" + n5["赋值 hash"] + click n5 href "app/account.dever#L37,10" "打开源码" + n6[["user.account.registerAccount"]] + click n6 href "app/account.dever#L38,12" "打开源码" + n7["赋值 result"] + click n7 href "app/account.dever#L38,12" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n5 + n5 --> n6 + n6 --> n7 + n7 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| 阻塞线程:dever.crypto.passwordHash | [app/account.dever:37:10](app/account.dever#L37,10) | 依赖或生成的分派(本页不展开) | +| user.account.registerAccount | [app/account.dever:38:12](app/account.dever#L38,12) | [user.account.registerAccount](link.md#flow-64c5dd0c194cf29d-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| `dever.crypto.passwordHash` | 依赖 | 阻塞任务 | [app/account.dever:37:19](app/account.dever#L37,19) | +| [user.account.registerAccount](app/account.dever#L41) | 领域内 | 调用 | [app/account.dever:38:12](app/account.dever#L38,12) | + +### user.account.cleanup + +定义:[app/session.dever:88:1](app/session.dever#L88,1) + + + +```mermaid +flowchart TD + n0(["cleanup"]) + click n0 href "app/session.dever#L88,1" "打开源码" + n1(["返回"]) + n3[["dever.time.now"]] + click n3 href "app/session.dever#L91,48" "打开源码" + n4["user.account.model.session.delete"] + click n4 href "app/session.dever#L89,13" "打开源码" + n5["赋值 removed"] + click n5 href "app/session.dever#L89,13" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n5 + n5 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| dever.time.now | [app/session.dever:91:48](app/session.dever#L91,48) | 依赖或生成的分派(本页不展开) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [user.account.model.session.delete](model/session.dever#L1) | 领域内 | 内建操作 | [app/session.dever:89:13](app/session.dever#L89,13) | +| `dever.time.now` | 依赖 | 调用 | [app/session.dever:91:48](app/session.dever#L91,48) | + +### user.account.currentUser + +定义:[app/account.dever:153:1](app/account.dever#L153,1) + + + +```mermaid +flowchart TD + n0(["currentUser"]) + click n0 href "app/account.dever#L153,1" "打开源码" + n1(["返回"]) + n3[["dever.auth.userId"]] + click n3 href "app/account.dever#L154,20" "打开源码" + n4[["user.account.requireUser"]] + click n4 href "app/account.dever#L154,8" "打开源码" + n5["赋值 id"] + click n5 href "app/account.dever#L154,8" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n5 + n5 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| dever.auth.userId | [app/account.dever:154:20](app/account.dever#L154,20) | 依赖或生成的分派(本页不展开) | +| user.account.requireUser | [app/account.dever:154:8](app/account.dever#L154,8) | [user.account.requireUser](link.md#flow-10e69f1dcd8d6bed-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [user.account.requireUser](app/account.dever#L157) | 领域内 | 调用 | [app/account.dever:154:8](app/account.dever#L154,8) | +| `dever.auth.userId` | 依赖 | 调用 | [app/account.dever:154:20](app/account.dever#L154,20) | + +### user.account.domain.requireTenant + +定义:[domain.dever:11:1](domain.dever#L11,1)、[domain.dever:15:1](domain.dever#L15,1) + + + +```mermaid +flowchart TD + n0(["requireTenant"]) + click n0 href "domain.dever#L11,1" "打开源码" + n1(["返回"]) + n2(["传播错误"]) + n3{"按实参匹配分句(互斥)"} + n4["actual: Text"] + click n4 href "domain.dever#L11,1" "打开源码" + n5[["dever.api.require"]] + click n5 href "domain.dever#L12,3" "打开源码" + n6["actual: null"] + click n6 href "domain.dever#L15,1" "打开源码" + n7["fail dever.api.Error.Unauthorized"] + click n7 href "domain.dever#L16,8" "打开源码" + n0 --> n3 + n3 -->|"actual: Text"| n4 + n4 --> n5 + n5 --> n1 + n3 -->|"actual: null"| n6 + n6 --> n7 + n7 --> n2 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| dever.api.require | [domain.dever:12:3](domain.dever#L12,3) | 依赖或生成的分派(本页不展开) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| `dever.api.require` | 依赖 | 调用 | [domain.dever:12:3](domain.dever#L12,3) | + +### user.account.domain.validateEmail + +定义:[domain.dever:1:1](domain.dever#L1,1) + + + +```mermaid +flowchart TD + n0(["validateEmail"]) + click n0 href "domain.dever#L1,1" "打开源码" + n1(["返回"]) + n3[["text.contains"]] + click n3 href "domain.dever#L3,5" "打开源码" + n4{"text.contains(address, #34;@#34;)"} + click n4 href "domain.dever#L3,5" "打开源码" + n5["求右侧值"] + click n5 href "domain.dever#L3,37" "打开源码" + n6["短路表达式结果"] + click n6 href "domain.dever#L3,5" "打开源码" + n7[["text.startsWith"]] + click n7 href "domain.dever#L3,41" "打开源码" + n8{"text.contains(address, #34;@#34;) and not text.startsWith(address, #34;@#34;)"} + click n8 href "domain.dever#L3,5" "打开源码" + n9["求右侧值"] + click n9 href "domain.dever#L3,75" "打开源码" + n10["短路表达式结果"] + click n10 href "domain.dever#L3,5" "打开源码" + n11[["text.endsWith"]] + click n11 href "domain.dever#L3,79" "打开源码" + n12[["dever.api.require"]] + click n12 href "domain.dever#L2,3" "打开源码" + n0 --> n3 + n3 --> n4 + n4 -->|"true"| n5 + n4 -->|"false:跳过右侧"| n6 + n5 --> n7 + n7 --> n6 + n6 --> n8 + n8 -->|"true"| n9 + n8 -->|"false:跳过右侧"| n10 + n9 --> n11 + n11 --> n10 + n10 --> n12 + n12 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| text.contains | [domain.dever:3:5](domain.dever#L3,5) | 依赖或生成的分派(本页不展开) | +| text.startsWith | [domain.dever:3:41](domain.dever#L3,41) | 依赖或生成的分派(本页不展开) | +| text.endsWith | [domain.dever:3:79](domain.dever#L3,79) | 依赖或生成的分派(本页不展开) | +| dever.api.require | [domain.dever:2:3](domain.dever#L2,3) | 依赖或生成的分派(本页不展开) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| `dever.api.require` | 依赖 | 调用 | [domain.dever:2:3](domain.dever#L2,3) | +| `text.contains` | 依赖 | 调用 | [domain.dever:3:5](domain.dever#L3,5) | +| `text.startsWith` | 依赖 | 调用 | [domain.dever:3:41](domain.dever#L3,41) | +| `text.endsWith` | 依赖 | 调用 | [domain.dever:3:79](domain.dever#L3,79) | + +### user.account.establishLogin + +定义:[app/account.dever:91:1](app/account.dever#L91,1) + + + +```mermaid +flowchart TD + n0(["establishLogin"]) + click n0 href "app/account.dever#L91,1" "打开源码" + n1["开始事务"] + n2(["提交事务,返回"]) + n3(["回滚事务,传播错误"]) + n4["user.account.model.get"] + click n4 href "app/account.dever#L97,12" "打开源码" + n5["赋值 stored"] + click n5 href "app/account.dever#L97,12" "打开源码" + n6["user.account.model.principal.get"] + click n6 href "app/account.dever#L98,15" "打开源码" + n7["赋值 principal"] + click n7 href "app/account.dever#L98,15" "打开源码" + n8[["platform.tenant.active"]] + click n8 href "app/account.dever#L99,14" "打开源码" + n9["赋值 selected"] + click n9 href "app/account.dever#L99,14" "打开源码" + n10[["dever.api.require"]] + click n10 href "app/account.dever#L100,3" "打开源码" + n11[["dever.api.require"]] + click n11 href "app/account.dever#L101,3" "打开源码" + n12[["user.account.requireMembership"]] + click n12 href "app/account.dever#L102,3" "打开源码" + n13[["uuid.toText"]] + click n13 href "app/account.dever#L104,5" "打开源码" + n14[["user.account.open"]] + click n14 href "app/account.dever#L103,13" "打开源码" + n15["赋值 session"] + click n15 href "app/account.dever#L103,13" "打开源码" + n16["赋值 result"] + click n16 href "app/account.dever#L110,12" "打开源码" + n17[["uuid.toText"]] + click n17 href "app/account.dever#L120,13" "打开源码" + n18["赋值 subject"] + click n18 href "app/account.dever#L120,13" "打开源码" + n19["赋值 session_id"] + click n19 href "app/account.dever#L121,16" "打开源码" + n20["赋值 tenant_key"] + click n20 href "app/account.dever#L122,16" "打开源码" + n0 --> n1 + n1 -.->|"事务体任一未捕获错误"| n3 + n1 --> n4 + n4 --> n5 + n5 --> n6 + n6 --> n7 + n7 --> n8 + n8 --> n9 + n9 --> n10 + n10 --> n11 + n11 --> n12 + n12 --> n13 + n13 --> n14 + n14 --> n15 + n15 --> n16 + n16 --> n17 + n17 --> n18 + n18 --> n19 + n19 --> n20 + n20 --> n2 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| platform.tenant.active | [app/account.dever:99:14](app/account.dever#L99,14) | [platform.tenant.active](../../platform/tenant/link.md#flow-720cd3e6ee01541e-1) | +| dever.api.require | [app/account.dever:100:3](app/account.dever#L100,3) | 依赖或生成的分派(本页不展开) | +| dever.api.require | [app/account.dever:101:3](app/account.dever#L101,3) | 依赖或生成的分派(本页不展开) | +| user.account.requireMembership | [app/account.dever:102:3](app/account.dever#L102,3) | [user.account.requireMembership](link.md#flow-a43ccc948a7a02c1-1) | +| uuid.toText | [app/account.dever:104:5](app/account.dever#L104,5) | 依赖或生成的分派(本页不展开) | +| user.account.open | [app/account.dever:103:13](app/account.dever#L103,13) | [user.account.open](link.md#flow-990c1cbc2a294199-1) | +| uuid.toText | [app/account.dever:120:13](app/account.dever#L120,13) | 依赖或生成的分派(本页不展开) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [user.account.model.get](model.dever#L6) | 领域内 | 内建操作 | [app/account.dever:97:12](app/account.dever#L97,12) | +| [user.account.model.principal.get](model/principal.dever#L6) | 领域内 | 内建操作 | [app/account.dever:98:15](app/account.dever#L98,15) | +| [platform.tenant.active](../../platform/tenant/app.dever#L21) | 跨领域 [platform.tenant](../../platform/tenant/link.md) | 调用 | [app/account.dever:99:14](app/account.dever#L99,14) | +| `dever.api.require` | 依赖 | 调用 | [app/account.dever:100:3](app/account.dever#L100,3) | +| `dever.api.require` | 依赖 | 调用 | [app/account.dever:101:3](app/account.dever#L101,3) | +| [user.account.requireMembership](app/membership.dever#L15) | 领域内 | 调用 | [app/account.dever:102:3](app/account.dever#L102,3) | +| [user.account.open](app/session.dever#L12) | 领域内 | 调用 | [app/account.dever:103:13](app/account.dever#L103,13) | +| `uuid.toText` | 依赖 | 调用 | [app/account.dever:104:5](app/account.dever#L104,5) | +| `uuid.toText` | 依赖 | 调用 | [app/account.dever:120:13](app/account.dever#L120,13) | + +### user.account.grantOwner + +定义:[app/membership.dever:1:1](app/membership.dever#L1,1) + + + +```mermaid +flowchart TD + n0(["grantOwner"]) + click n0 href "app/membership.dever#L1,1" "打开源码" + n1(["返回"]) + n3["user.account.model.membership.create"] + click n3 href "app/membership.dever#L5,12" "打开源码" + n4["赋值 stored"] + click n4 href "app/membership.dever#L5,12" "打开源码" + n5["赋值 discarded"] + click n5 href "app/membership.dever#L12,15" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n5 + n5 --> n1 +``` + +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [user.account.model.membership.create](model/membership.dever#L12) | 领域内 | 内建操作 | [app/membership.dever:5:12](app/membership.dever#L5,12) | + +### user.account.job.cleanup + +定义:[job.dever:3:1](job.dever#L3,1) + + + +```mermaid +flowchart TD + n0(["cleanup"]) + click n0 href "job.dever#L3,1" "打开源码" + n1(["返回"]) + n3[["user.account.cleanup"]] + click n3 href "job.dever#L4,3" "打开源码" + n0 --> n3 + n3 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.cleanup | [job.dever:4:3](job.dever#L4,3) | [user.account.cleanup](link.md#flow-fef25e4ceb636b6d-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [user.account.cleanup](app/session.dever#L88) | 领域内 | 调用 | [job.dever:4:3](job.dever#L4,3) | + +### user.account.login + +定义:[app/account.dever:70:1](app/account.dever#L70,1) + + + +```mermaid +flowchart TD + n0(["login"]) + click n0 href "app/account.dever#L70,1" "打开源码" + n1(["返回"]) + n3["user.account.model.exists"] + click n3 href "app/account.dever#L71,21" "打开源码" + n4[["dever.api.require"]] + click n4 href "app/account.dever#L71,3" "打开源码" + n5["user.account.model.get"] + click n5 href "app/account.dever#L76,12" "打开源码" + n6["赋值 stored"] + click n6 href "app/account.dever#L76,12" "打开源码" + n7["user.account.model.principal.get"] + click n7 href "app/account.dever#L77,15" "打开源码" + n8["赋值 principal"] + click n8 href "app/account.dever#L77,15" "打开源码" + n9[["platform.tenant.active"]] + click n9 href "app/account.dever#L78,14" "打开源码" + n10["赋值 selected"] + click n10 href "app/account.dever#L78,14" "打开源码" + n11[["dever.api.require"]] + click n11 href "app/account.dever#L79,3" "打开源码" + n12[["dever.api.require"]] + click n12 href "app/account.dever#L80,3" "打开源码" + n13[["阻塞线程:dever.crypto.passwordVerify"]] + click n13 href "app/account.dever#L81,21" "打开源码" + n14["等待任务完成并取得结果"] + click n14 href "app/account.dever#L81,21" "打开源码" + n15[["dever.api.require"]] + click n15 href "app/account.dever#L81,3" "打开源码" + n16[["dever.site.key"]] + click n16 href "app/account.dever#L82,63" "打开源码" + n17[["user.account.establishLogin"]] + click n17 href "app/account.dever#L82,19" "打开源码" + n18["赋值 authenticated"] + click n18 href "app/account.dever#L82,19" "打开源码" + n19[["uuid.toText"]] + click n19 href "app/account.dever#L85,5" "打开源码" + n20[["dever.auth.issueCookie"]] + click n20 href "app/account.dever#L83,3" "打开源码" + n21["赋值 result"] + click n21 href "app/account.dever#L88,12" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n5 + n5 --> n6 + n6 --> n7 + n7 --> n8 + n8 --> n9 + n9 --> n10 + n10 --> n11 + n11 --> n12 + n12 --> n13 + n13 --> n14 + n14 --> n15 + n15 --> n16 + n16 --> n17 + n17 --> n18 + n18 --> n19 + n19 --> n20 + n20 --> n21 + n21 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| dever.api.require | [app/account.dever:71:3](app/account.dever#L71,3) | 依赖或生成的分派(本页不展开) | +| platform.tenant.active | [app/account.dever:78:14](app/account.dever#L78,14) | [platform.tenant.active](../../platform/tenant/link.md#flow-720cd3e6ee01541e-1) | +| dever.api.require | [app/account.dever:79:3](app/account.dever#L79,3) | 依赖或生成的分派(本页不展开) | +| dever.api.require | [app/account.dever:80:3](app/account.dever#L80,3) | 依赖或生成的分派(本页不展开) | +| 阻塞线程:dever.crypto.passwordVerify | [app/account.dever:81:21](app/account.dever#L81,21) | 依赖或生成的分派(本页不展开) | +| dever.api.require | [app/account.dever:81:3](app/account.dever#L81,3) | 依赖或生成的分派(本页不展开) | +| dever.site.key | [app/account.dever:82:63](app/account.dever#L82,63) | 依赖或生成的分派(本页不展开) | +| user.account.establishLogin | [app/account.dever:82:19](app/account.dever#L82,19) | [user.account.establishLogin](link.md#flow-cb017292969712cb-1) | +| uuid.toText | [app/account.dever:85:5](app/account.dever#L85,5) | 依赖或生成的分派(本页不展开) | +| dever.auth.issueCookie | [app/account.dever:83:3](app/account.dever#L83,3) | 依赖或生成的分派(本页不展开) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| `dever.api.require` | 依赖 | 调用 | [app/account.dever:71:3](app/account.dever#L71,3) | +| [user.account.model.exists](model.dever#L6) | 领域内 | 内建操作 | [app/account.dever:71:21](app/account.dever#L71,21) | +| [user.account.model.get](model.dever#L6) | 领域内 | 内建操作 | [app/account.dever:76:12](app/account.dever#L76,12) | +| [user.account.model.principal.get](model/principal.dever#L6) | 领域内 | 内建操作 | [app/account.dever:77:15](app/account.dever#L77,15) | +| [platform.tenant.active](../../platform/tenant/app.dever#L21) | 跨领域 [platform.tenant](../../platform/tenant/link.md) | 调用 | [app/account.dever:78:14](app/account.dever#L78,14) | +| `dever.api.require` | 依赖 | 调用 | [app/account.dever:79:3](app/account.dever#L79,3) | +| `dever.api.require` | 依赖 | 调用 | [app/account.dever:80:3](app/account.dever#L80,3) | +| `dever.api.require` | 依赖 | 调用 | [app/account.dever:81:3](app/account.dever#L81,3) | +| `dever.crypto.passwordVerify` | 依赖 | 阻塞任务 | [app/account.dever:81:30](app/account.dever#L81,30) | +| [user.account.establishLogin](app/account.dever#L91) | 领域内 | 调用 | [app/account.dever:82:19](app/account.dever#L82,19) | +| `dever.site.key` | 依赖 | 调用 | [app/account.dever:82:63](app/account.dever#L82,63) | +| `dever.auth.issueCookie` | 依赖 | 调用 | [app/account.dever:83:3](app/account.dever#L83,3) | +| `uuid.toText` | 依赖 | 调用 | [app/account.dever:85:5](app/account.dever#L85,5) | + +### user.account.logout + +定义:[app/account.dever:125:1](app/account.dever#L125,1) + + + +```mermaid +flowchart TD + n0(["logout"]) + click n0 href "app/account.dever#L125,1" "打开源码" + n1(["返回"]) + n3[["dever.auth.session"]] + click n3 href "app/account.dever#L126,21" "打开源码" + n4[["uuid.parse"]] + click n4 href "app/account.dever#L126,10" "打开源码" + n5[["user.account.revoke"]] + click n5 href "app/account.dever#L126,3" "打开源码" + n6[["dever.auth.clearCookie"]] + click n6 href "app/account.dever#L127,3" "打开源码" + n7["赋值 result"] + click n7 href "app/account.dever#L128,12" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n5 + n5 --> n6 + n6 --> n7 + n7 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| dever.auth.session | [app/account.dever:126:21](app/account.dever#L126,21) | 依赖或生成的分派(本页不展开) | +| uuid.parse | [app/account.dever:126:10](app/account.dever#L126,10) | 依赖或生成的分派(本页不展开) | +| user.account.revoke | [app/account.dever:126:3](app/account.dever#L126,3) | [user.account.revoke](link.md#flow-637ad4bf48f7ea3b-1) | +| dever.auth.clearCookie | [app/account.dever:127:3](app/account.dever#L127,3) | 依赖或生成的分派(本页不展开) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [user.account.revoke](app/session.dever#L48) | 领域内 | 调用 | [app/account.dever:126:3](app/account.dever#L126,3) | +| `uuid.parse` | 依赖 | 调用 | [app/account.dever:126:10](app/account.dever#L126,10) | +| `dever.auth.session` | 依赖 | 调用 | [app/account.dever:126:21](app/account.dever#L126,21) | +| `dever.auth.clearCookie` | 依赖 | 调用 | [app/account.dever:127:3](app/account.dever#L127,3) | + +### user.account.model.Account + +定义:[model.dever:6:1](model.dever#L6,1) + +没有已收录的下游调用。 + +### user.account.model.AccountStatus + +定义:[model.dever:1:1](model.dever#L1,1) + +没有已收录的下游调用。 + +### user.account.model.membership.Membership + +定义:[model/membership.dever:12:1](model/membership.dever#L12,1) + +没有已收录的下游调用。 + +### user.account.model.membership.MembershipRole + +定义:[model/membership.dever:1:1](model/membership.dever#L1,1) + +没有已收录的下游调用。 + +### user.account.model.membership.MembershipStatus + +定义:[model/membership.dever:7:1](model/membership.dever#L7,1) + +没有已收录的下游调用。 + +### user.account.model.principal.Principal + +定义:[model/principal.dever:6:1](model/principal.dever#L6,1) + +没有已收录的下游调用。 + +### user.account.model.principal.PrincipalStatus + +定义:[model/principal.dever:1:1](model/principal.dever#L1,1) + +没有已收录的下游调用。 + +### user.account.model.session.Session + +定义:[model/session.dever:1:1](model/session.dever#L1,1) + +没有已收录的下游调用。 + +### user.account.open + +定义:[app/session.dever:12:1](app/session.dever#L12,1) + + + +```mermaid +flowchart TD + n0(["open"]) + click n0 href "app/session.dever#L12,1" "打开源码" + n1(["返回"]) + n3[["dever.time.now"]] + click n3 href "app/session.dever#L26,35" "打开源码" + n4[["dever.time.duration"]] + click n4 href "app/session.dever#L26,53" "打开源码" + n5[["dever.time.add"]] + click n5 href "app/session.dever#L26,20" "打开源码" + n6["user.account.model.session.create"] + click n6 href "app/session.dever#L19,12" "打开源码" + n7["赋值 stored"] + click n7 href "app/session.dever#L19,12" "打开源码" + n8["赋值 session"] + click n8 href "app/session.dever#L29,13" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n5 + n5 --> n6 + n6 --> n7 + n7 --> n8 + n8 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| dever.time.now | [app/session.dever:26:35](app/session.dever#L26,35) | 依赖或生成的分派(本页不展开) | +| dever.time.duration | [app/session.dever:26:53](app/session.dever#L26,53) | 依赖或生成的分派(本页不展开) | +| dever.time.add | [app/session.dever:26:20](app/session.dever#L26,20) | 依赖或生成的分派(本页不展开) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [user.account.model.session.create](model/session.dever#L1) | 领域内 | 内建操作 | [app/session.dever:19:12](app/session.dever#L19,12) | +| `dever.time.add` | 依赖 | 调用 | [app/session.dever:26:20](app/session.dever#L26,20) | +| `dever.time.now` | 依赖 | 调用 | [app/session.dever:26:35](app/session.dever#L26,35) | +| `dever.time.duration` | 依赖 | 调用 | [app/session.dever:26:53](app/session.dever#L26,53) | + +### user.account.registerAccount + +定义:[app/account.dever:41:1](app/account.dever#L41,1) + + + +```mermaid +flowchart TD + n0(["registerAccount"]) + click n0 href "app/account.dever#L41,1" "打开源码" + n1["开始事务"] + n2(["提交事务,返回"]) + n3(["回滚事务,传播错误"]) + n4[["platform.tenant.provision"]] + click n4 href "app/account.dever#L48,12" "打开源码" + n5["赋值 tenant"] + click n5 href "app/account.dever#L48,12" "打开源码" + n6["user.account.model.principal.create"] + click n6 href "app/account.dever#L49,15" "打开源码" + n7["赋值 principal"] + click n7 href "app/account.dever#L49,15" "打开源码" + n8["user.account.model.create"] + click n8 href "app/account.dever#L54,12" "打开源码" + n9["赋值 stored"] + click n9 href "app/account.dever#L54,12" "打开源码" + n10[["user.account.domain.validateEmail"]] + click n10 href "app/account.dever#L61,3" "打开源码" + n11[["user.account.grantOwner"]] + click n11 href "app/account.dever#L62,3" "打开源码" + n12["赋值 result"] + click n12 href "app/account.dever#L63,12" "打开源码" + n0 --> n1 + n1 -.->|"事务体任一未捕获错误"| n3 + n1 --> n4 + n4 --> n5 + n5 --> n6 + n6 --> n7 + n7 --> n8 + n8 --> n9 + n9 --> n10 + n10 --> n11 + n11 --> n12 + n12 --> n2 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| platform.tenant.provision | [app/account.dever:48:12](app/account.dever#L48,12) | [platform.tenant.provision](../../platform/tenant/link.md#flow-cc4bbca9579ab41a-1) | +| user.account.domain.validateEmail | [app/account.dever:61:3](app/account.dever#L61,3) | [user.account.domain.validateEmail](link.md#flow-f510af5d5274cd13-1) | +| user.account.grantOwner | [app/account.dever:62:3](app/account.dever#L62,3) | [user.account.grantOwner](link.md#flow-731d294fccf1a4fa-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [platform.tenant.provision](../../platform/tenant/app.dever#L7) | 跨领域 [platform.tenant](../../platform/tenant/link.md) | 调用 | [app/account.dever:48:12](app/account.dever#L48,12) | +| [user.account.model.principal.create](model/principal.dever#L6) | 领域内 | 内建操作 | [app/account.dever:49:15](app/account.dever#L49,15) | +| [user.account.model.create](model.dever#L6) | 领域内 | 内建操作 | [app/account.dever:54:12](app/account.dever#L54,12) | +| [user.account.domain.validateEmail](domain.dever#L1) | 领域内 | 调用 | [app/account.dever:61:3](app/account.dever#L61,3) | +| [user.account.grantOwner](app/membership.dever#L1) | 领域内 | 调用 | [app/account.dever:62:3](app/account.dever#L62,3) | + +### user.account.requireMembership + +定义:[app/membership.dever:15:1](app/membership.dever#L15,1) + + + +```mermaid +flowchart TD + n0(["requireMembership"]) + click n0 href "app/membership.dever#L15,1" "打开源码" + n1(["返回"]) + n3["user.account.model.membership.first"] + click n3 href "app/membership.dever#L19,12" "打开源码" + n4["赋值 stored"] + click n4 href "app/membership.dever#L19,12" "打开源码" + n5[["user.account.activeMembership"]] + click n5 href "app/membership.dever#L24,3" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n5 + n5 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.activeMembership | [app/membership.dever:24:3](app/membership.dever#L24,3) | [user.account.activeMembership](link.md#flow-952083c29a92f0b8-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [user.account.model.membership.first](model/membership.dever#L12) | 领域内 | 内建操作 | [app/membership.dever:19:12](app/membership.dever#L19,12) | +| [user.account.activeMembership](app/membership.dever#L27) | 领域内 | 调用 | [app/membership.dever:24:3](app/membership.dever#L24,3) | + +### user.account.requireUser + +定义:[app/account.dever:157:1](app/account.dever#L157,1)、[app/account.dever:161:1](app/account.dever#L161,1) + + + +```mermaid +flowchart TD + n0(["requireUser"]) + click n0 href "app/account.dever#L157,1" "打开源码" + n1(["返回"]) + n2(["传播错误"]) + n3{"按实参匹配分句(互斥)"} + n4["value: Int"] + click n4 href "app/account.dever#L157,1" "打开源码" + n5["赋值 id"] + click n5 href "app/account.dever#L158,8" "打开源码" + n6["value: null"] + click n6 href "app/account.dever#L161,1" "打开源码" + n7["fail dever.api.Error.Unauthorized"] + click n7 href "app/account.dever#L162,8" "打开源码" + n0 --> n3 + n3 -->|"value: Int"| n4 + n4 --> n5 + n5 --> n1 + n3 -->|"value: null"| n6 + n6 --> n7 + n7 --> n2 +``` + +没有已收录的下游调用。 + +### user.account.revoke + +定义:[app/session.dever:48:1](app/session.dever#L48,1)、[app/session.dever:57:1](app/session.dever#L57,1) + + + +```mermaid +flowchart TD + n0(["revoke"]) + click n0 href "app/session.dever#L48,1" "打开源码" + n1(["返回"]) + n2(["传播错误"]) + n3{"按实参匹配分句(互斥)"} + n4["session_id: Uuid"] + click n4 href "app/session.dever#L48,1" "打开源码" + n5["user.account.model.session.first"] + click n5 href "app/session.dever#L49,12" "打开源码" + n6["赋值 stored"] + click n6 href "app/session.dever#L49,12" "打开源码" + n7[["user.account.revokeFound"]] + click n7 href "app/session.dever#L54,3" "打开源码" + n8["session_id: null"] + click n8 href "app/session.dever#L57,1" "打开源码" + n9["fail dever.api.Error.Unauthorized"] + click n9 href "app/session.dever#L58,8" "打开源码" + n0 --> n3 + n3 -->|"session_id: Uuid"| n4 + n4 --> n5 + n5 --> n6 + n6 --> n7 + n7 --> n1 + n3 -->|"session_id: null"| n8 + n8 --> n9 + n9 --> n2 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.revokeFound | [app/session.dever:54:3](app/session.dever#L54,3) | [user.account.revokeFound](link.md#flow-53001d1f5441a7a3-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [user.account.model.session.first](model/session.dever#L1) | 领域内 | 内建操作 | [app/session.dever:49:12](app/session.dever#L49,12) | +| [user.account.revokeFound](app/session.dever#L74) | 领域内 | 调用 | [app/session.dever:54:3](app/session.dever#L54,3) | + +### user.account.revokeFound + +定义:[app/session.dever:74:1](app/session.dever#L74,1)、[app/session.dever:84:1](app/session.dever#L84,1) + + + +```mermaid +flowchart TD + n0(["revokeFound"]) + click n0 href "app/session.dever#L74,1" "打开源码" + n1(["返回"]) + n2(["传播错误"]) + n3{"按实参匹配分句(互斥)"} + n4["stored: model.session.Session"] + click n4 href "app/session.dever#L74,1" "打开源码" + n5["user.account.model.session.update"] + click n5 href "app/session.dever#L75,13" "打开源码" + n6["赋值 changed"] + click n6 href "app/session.dever#L75,13" "打开源码" + n7["赋值 discarded"] + click n7 href "app/session.dever#L81,15" "打开源码" + n8["stored: null"] + click n8 href "app/session.dever#L84,1" "打开源码" + n9["fail dever.api.Error.Unauthorized"] + click n9 href "app/session.dever#L85,8" "打开源码" + n0 --> n3 + n3 -->|"stored: model.session.Session"| n4 + n4 --> n5 + n5 --> n6 + n6 --> n7 + n7 --> n1 + n3 -->|"stored: null"| n8 + n8 --> n9 + n9 --> n2 +``` + +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [user.account.model.session.update](model/session.dever#L1) | 领域内 | 内建操作 | [app/session.dever:75:13](app/session.dever#L75,13) | + +### user.account.verify + +定义:[app/account.dever:133:1](app/account.dever#L133,1) + + + +```mermaid +flowchart TD + n0(["verify"]) + click n0 href "app/account.dever#L133,1" "打开源码" + n1(["返回"]) + n3[["uuid.parse"]] + click n3 href "app/account.dever#L134,26" "打开源码" + n4[["user.account.authenticate"]] + click n4 href "app/account.dever#L134,13" "打开源码" + n5["赋值 session"] + click n5 href "app/account.dever#L134,13" "打开源码" + n6[["dever.api.require"]] + click n6 href "app/account.dever#L135,3" "打开源码" + n7[["user.account.domain.requireTenant"]] + click n7 href "app/account.dever#L136,3" "打开源码" + n8[["platform.tenant.active"]] + click n8 href "app/account.dever#L137,14" "打开源码" + n9["赋值 selected"] + click n9 href "app/account.dever#L137,14" "打开源码" + n10[["dever.api.require"]] + click n10 href "app/account.dever#L138,3" "打开源码" + n11[["user.account.active"]] + click n11 href "app/account.dever#L139,3" "打开源码" + n12[["user.account.requireMembership"]] + click n12 href "app/account.dever#L140,3" "打开源码" + n13["赋值 identity"] + click n13 href "app/account.dever#L141,14" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n5 + n5 --> n6 + n6 --> n7 + n7 --> n8 + n8 --> n9 + n9 --> n10 + n10 --> n11 + n11 --> n12 + n12 --> n13 + n13 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| uuid.parse | [app/account.dever:134:26](app/account.dever#L134,26) | 依赖或生成的分派(本页不展开) | +| user.account.authenticate | [app/account.dever:134:13](app/account.dever#L134,13) | [user.account.authenticate](link.md#flow-0ee082c8e99a1e2c-1) | +| dever.api.require | [app/account.dever:135:3](app/account.dever#L135,3) | 依赖或生成的分派(本页不展开) | +| user.account.domain.requireTenant | [app/account.dever:136:3](app/account.dever#L136,3) | [user.account.domain.requireTenant](link.md#flow-860f37a05810a61d-1) | +| platform.tenant.active | [app/account.dever:137:14](app/account.dever#L137,14) | [platform.tenant.active](../../platform/tenant/link.md#flow-720cd3e6ee01541e-1) | +| dever.api.require | [app/account.dever:138:3](app/account.dever#L138,3) | 依赖或生成的分派(本页不展开) | +| user.account.active | [app/account.dever:139:3](app/account.dever#L139,3) | [user.account.active](link.md#flow-a01b0ff801f6e737-1) | +| user.account.requireMembership | [app/account.dever:140:3](app/account.dever#L140,3) | [user.account.requireMembership](link.md#flow-a43ccc948a7a02c1-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [user.account.authenticate](app/session.dever#L34) | 领域内 | 调用 | [app/account.dever:134:13](app/account.dever#L134,13) | +| `uuid.parse` | 依赖 | 调用 | [app/account.dever:134:26](app/account.dever#L134,26) | +| `dever.api.require` | 依赖 | 调用 | [app/account.dever:135:3](app/account.dever#L135,3) | +| [user.account.domain.requireTenant](domain.dever#L11) | 领域内 | 调用 | [app/account.dever:136:3](app/account.dever#L136,3) | +| [platform.tenant.active](../../platform/tenant/app.dever#L21) | 跨领域 [platform.tenant](../../platform/tenant/link.md) | 调用 | [app/account.dever:137:14](app/account.dever#L137,14) | +| `dever.api.require` | 依赖 | 调用 | [app/account.dever:138:3](app/account.dever#L138,3) | +| [user.account.active](app/account.dever#L148) | 领域内 | 调用 | [app/account.dever:139:3](app/account.dever#L139,3) | +| [user.account.requireMembership](app/membership.dever#L15) | 领域内 | 调用 | [app/account.dever:140:3](app/account.dever#L140,3) | diff --git a/examples/cms/dever/module/user/account/model.dever b/examples/cms/dever/module/user/account/model.dever new file mode 100644 index 0000000..30c32f0 --- /dev/null +++ b/examples/cms/dever/module/user/account/model.dever @@ -0,0 +1,11 @@ +type AccountStatus { + Active = "启用" + Disabled = "禁用" +} + +global type Account { + principal_id: user.account.model.principal.id unique + email: Text(254) lower unique + private password_hash: Text(255) raw + status: AccountStatus default AccountStatus.Active index +} diff --git a/examples/cms/dever/module/user/account/model/membership.dever b/examples/cms/dever/module/user/account/model/membership.dever new file mode 100644 index 0000000..eb386fd --- /dev/null +++ b/examples/cms/dever/module/user/account/model/membership.dever @@ -0,0 +1,19 @@ +type MembershipRole { + Owner = "所有者" + Editor = "编辑" + Reader = "读者" +} + +type MembershipStatus { + Active = "启用" + Disabled = "禁用" +} + +global type Membership { + user_id: model.principal.id + tenant_id: platform.tenant.model.id + role: MembershipRole index + status: MembershipStatus default MembershipStatus.Active index +} + +unique(user_id, tenant_id) diff --git a/examples/cms/dever/module/user/account/model/principal.dever b/examples/cms/dever/module/user/account/model/principal.dever new file mode 100644 index 0000000..b662b44 --- /dev/null +++ b/examples/cms/dever/module/user/account/model/principal.dever @@ -0,0 +1,10 @@ +type PrincipalStatus { + Active = "启用" + Disabled = "禁用" +} + +global type Principal { + uuid: Uuid generated unique + display_name: Text(1, 64) + status: PrincipalStatus default PrincipalStatus.Active index +} diff --git a/examples/cms/dever/module/user/account/model/session.dever b/examples/cms/dever/module/user/account/model/session.dever new file mode 100644 index 0000000..f6f5edc --- /dev/null +++ b/examples/cms/dever/module/user/account/model/session.dever @@ -0,0 +1,12 @@ +global type Session { + uuid: Uuid generated unique + subject: Text(1, 128) + user_id: model.principal.id + tenant_id: platform.tenant.model.id + tenant_key: Text(1, 64) + site: Text(1, 32) + expires_at: DateTime index + revoked: Bool default false index +} + +index(user_id, tenant_id, site) diff --git a/examples/cms/dever/module/user/authorization/README.md b/examples/cms/dever/module/user/authorization/README.md new file mode 100644 index 0000000..5bfc13a --- /dev/null +++ b/examples/cms/dever/module/user/authorization/README.md @@ -0,0 +1,8 @@ +# 角色授权 + +通过核心权限接口管理当前站点的普通角色和用户授权。 + +- [角色管理接口](doc/api-admin-manage.md) +- [角色管理接口](doc/api-front-manage.md) +- [授权管理应用](doc/app.md) +- [调用流程](link.md) diff --git a/examples/cms/dever/module/user/authorization/api/admin/manage.dever b/examples/cms/dever/module/user/authorization/api/admin/manage.dever new file mode 100644 index 0000000..e918e84 --- /dev/null +++ b/examples/cms/dever/module/user/authorization/api/admin/manage.dever @@ -0,0 +1,9 @@ +get permissions = app.permissions + +post save = app.save + +post grant = app.grant + +delete revoke = app.revoke + +delete disable = app.disable diff --git a/examples/cms/dever/module/user/authorization/api/front/manage.dever b/examples/cms/dever/module/user/authorization/api/front/manage.dever new file mode 100644 index 0000000..e918e84 --- /dev/null +++ b/examples/cms/dever/module/user/authorization/api/front/manage.dever @@ -0,0 +1,9 @@ +get permissions = app.permissions + +post save = app.save + +post grant = app.grant + +delete revoke = app.revoke + +delete disable = app.disable diff --git a/examples/cms/dever/module/user/authorization/app.dever b/examples/cms/dever/module/user/authorization/app.dever new file mode 100644 index 0000000..5531adb --- /dev/null +++ b/examples/cms/dever/module/user/authorization/app.dever @@ -0,0 +1,25 @@ +permissions() (values: List) { + values = dever.auth.permissions() +} + +save(id: Text, name: Text, all_permissions: Bool, permission_keys: List) ( + ok: Bool +) { + dever.auth.saveRole(id, name, all_permissions, permission_keys) + ok = true +} + +grant(user_id: Int, role_id: Text) (ok: Bool) { + dever.auth.grantRole(user_id, role_id) + ok = true +} + +revoke(user_id: Int, role_id: Text) (ok: Bool) { + dever.auth.revokeRole(user_id, role_id) + ok = true +} + +disable(role_id: Text) (ok: Bool) { + dever.auth.disableRole(role_id) + ok = true +} diff --git a/examples/cms/dever/module/user/authorization/doc/api-admin-manage.md b/examples/cms/dever/module/user/authorization/doc/api-admin-manage.md new file mode 100644 index 0000000..628ff97 --- /dev/null +++ b/examples/cms/dever/module/user/authorization/doc/api-admin-manage.md @@ -0,0 +1,21 @@ +# 角色管理接口 + +## user.authorization.api.admin.manage.permissions + +读取本站点可管理的权限。 + +## user.authorization.api.admin.manage.save + +保存角色及权限键。 + +## user.authorization.api.admin.manage.grant + +向用户授予普通角色。 + +## user.authorization.api.admin.manage.revoke + +撤销用户的普通角色。 + +## user.authorization.api.admin.manage.disable + +禁用普通角色,保留授权关系。 diff --git a/examples/cms/dever/module/user/authorization/doc/api-front-manage.md b/examples/cms/dever/module/user/authorization/doc/api-front-manage.md new file mode 100644 index 0000000..ac90b1b --- /dev/null +++ b/examples/cms/dever/module/user/authorization/doc/api-front-manage.md @@ -0,0 +1,21 @@ +# 角色管理接口 + +## user.authorization.api.front.manage.permissions + +读取本站点可管理的权限。 + +## user.authorization.api.front.manage.save + +保存角色及权限键。 + +## user.authorization.api.front.manage.grant + +向用户授予普通角色。 + +## user.authorization.api.front.manage.revoke + +撤销用户的普通角色。 + +## user.authorization.api.front.manage.disable + +禁用普通角色,保留授权关系。 diff --git a/examples/cms/dever/module/user/authorization/doc/app.md b/examples/cms/dever/module/user/authorization/doc/app.md new file mode 100644 index 0000000..3f9765c --- /dev/null +++ b/examples/cms/dever/module/user/authorization/doc/app.md @@ -0,0 +1,21 @@ +# 授权管理应用 + +## user.authorization.permissions + +读取当前站点可管理的权限。 + +## user.authorization.save + +保存当前站点的角色名称、全权限标记及权限键。 + +## user.authorization.grant + +把普通角色授予指定用户。 + +## user.authorization.revoke + +撤销指定用户的普通角色。 + +## user.authorization.disable + +禁用普通角色,保留已有授权关系。 diff --git a/examples/cms/dever/module/user/authorization/link.md b/examples/cms/dever/module/user/authorization/link.md new file mode 100644 index 0000000..9df5d4b --- /dev/null +++ b/examples/cms/dever/module/user/authorization/link.md @@ -0,0 +1,1422 @@ + + + +# user.authorization + +按求值顺序展示调用、分支和任务调度,包含 API 与自动 REST 入口。点击节点打开源码;双框为调用,虚线为调度或错误路径。外部库和驱动不展开。 + +图不是某次运行的日志,分句和 Adapter 按输入或配置选择。修改源码后运行 `dever inspect . --links`。 + +## 入口 + +- API [user.authorization.api.admin.manage.disable](api/admin/manage.dever#L9) → [user.authorization.disable](app.dever#L22) +- API [user.authorization.api.admin.manage.grant](api/admin/manage.dever#L5) → [user.authorization.grant](app.dever#L12) +- API [user.authorization.api.admin.manage.permissions](api/admin/manage.dever#L1) → [user.authorization.permissions](app.dever#L1) +- API [user.authorization.api.admin.manage.revoke](api/admin/manage.dever#L7) → [user.authorization.revoke](app.dever#L17) +- API [user.authorization.api.admin.manage.save](api/admin/manage.dever#L3) → [user.authorization.save](app.dever#L5) +- API [user.authorization.api.front.manage.disable](api/front/manage.dever#L9) → [user.authorization.disable](app.dever#L22) +- API [user.authorization.api.front.manage.grant](api/front/manage.dever#L5) → [user.authorization.grant](app.dever#L12) +- API [user.authorization.api.front.manage.permissions](api/front/manage.dever#L1) → [user.authorization.permissions](app.dever#L1) +- API [user.authorization.api.front.manage.revoke](api/front/manage.dever#L7) → [user.authorization.revoke](app.dever#L17) +- API [user.authorization.api.front.manage.save](api/front/manage.dever#L3) → [user.authorization.save](app.dever#L5) + +## 外部调用方 + +当前加载的源码中没有已收录的外部调用。 + +## 能力与内部调用 + +### user.authorization.api.admin.manage.disable + +定义:[api/admin/manage.dever:9:1](api/admin/manage.dever#L9,1) + + + +```mermaid +flowchart TD + n0(["user.authorization.api.admin.manage.disable"]) + click n0 href "api/admin/manage.dever#L9,18" "打开源码" + n1{"路由 DELETE /user/authorization/admin/manage/disable,topic #34;admin/manage#34; 接受请求?"} + n2(["返回 HTTP 404"]) + n3["进入 API"] + n4["取得认证时间"] + n5(["返回 HTTP 500"]) + n6["按 topic #34;admin/manage#34; 选择站点与 Provider"] + n7(["返回 HTTP 403"]) + n8{"读取 Bearer 或站点 Cookie;两者互斥"} + n9(["返回 HTTP 401"]) + n10["认证上下文就绪"] + n11(["返回 HTTP 401"]) + n12["校验 Token 的签名、有效期及站点"] + n13{"Cookie 且非 GET / HEAD / OPTIONS?"} + n14["检查站点 Origin"] + n15(["返回 HTTP 403"]) + n16(["返回 HTTP 401"]) + n17{"按 Provider.verify 选择验证函数"} + n18[["user.account.verify"]] + click n18 href "api/admin/manage.dever#L9,18" "打开源码" + n19(["返回 HTTP 业务错误映射状态;其他故障 500"]) + n20["用验证结果建立身份"] + n21(["返回 HTTP 认证错误状态"]) + n22(["返回 HTTP 500"]) + n23["建立请求数据库上下文"] + n24{"有已验证租户?"} + n25["数据库上下文就绪"] + n26["按已验证租户选择数据库"] + n27(["返回 HTTP 503"]) + n28{"检查租户组件启用状态:"} + n29(["返回 HTTP 403"]) + n30(["返回 HTTP 503"]) + n31["进入写入口事务"] + n32["失败:保留 HTTP 错误状态"] + n33["回滚入口拥有的事务"] + n34(["返回 HTTP 500"]) + n35(["返回错误响应"]) + n36{"权限检查:user.authorization.{site}.disable"} + n37["权限通过"] + n38["读取 API 输入"] + n39["解码具体参数并检查字段合同"] + n40[["user.authorization.disable"]] + click n40 href "api/admin/manage.dever#L9,18" "打开源码" + n41["按输出类型检查可序列化值并编码响应"] + n42["提交入口拥有的事务"] + n43(["返回 HTTP 500"]) + n44["发布暂存的认证 Header / Cookie"] + n45(["返回成功响应"]) + n0 --> n1 + n1 -->|"未匹配此入口:继续路由;均未匹配"| n2 + n1 --> n3 + n3 --> n4 + n4 -->|"失败"| n5 + n4 --> n6 + n6 -->|"无站点绑定"| n7 + n6 --> n8 + n8 -->|"凭据格式错误或冲突"| n9 + n8 -->|"无凭据"| n11 + n8 -->|"有凭据"| n13 + n13 -->|"否"| n12 + n13 -->|"是"| n14 + n14 -->|"不符合"| n15 + n14 -->|"通过"| n12 + n12 -->|"无 Provider 或 Token 无效"| n16 + n12 --> n17 + n17 -->|"user.account.verify"| n18 + n18 -->|"验证函数失败"| n19 + n18 --> n20 + n20 -->|"身份不合法"| n21 + n20 -->|"通过"| n10 + n17 -->|"无匹配验证函数"| n22 + n10 --> n23 + n23 --> n24 + n24 -->|"否"| n25 + n24 -->|"是"| n26 + n26 -->|"选择失败"| n27 + n26 --> n28 + n28 -->|"未启用"| n29 + n28 -->|"查询失败"| n30 + n28 -->|"允许"| n25 + n25 --> n31 + n32 --> n33 + n33 -->|"事务结束失败"| n34 + n33 --> n35 + n31 --> n36 + n36 -->|"拒绝:Access 状态;查询故障:500"| n32 + n36 -->|"允许"| n37 + n37 --> n38 + n38 -->|"失败:输入错误状态"| n32 + n38 --> n39 + n39 -->|"失败:400"| n32 + n39 --> n40 + n40 -->|"业务错误映射状态;其他故障:500"| n32 + n40 --> n41 + n41 -->|"编码失败:500"| n32 + n41 --> n42 + n42 -->|"提交失败"| n43 + n42 --> n44 + n44 --> n45 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.verify | [api/admin/manage.dever:9:18](api/admin/manage.dever#L9,18) | [user.account.verify](../account/link.md#flow-aaaacb87d61f819c-1) | +| user.authorization.disable | [api/admin/manage.dever:9:18](api/admin/manage.dever#L9,18) | [user.authorization.disable](link.md#flow-57c59248be965dfd-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [user.account.verify](../account/app/account.dever#L133) | 跨领域 [user.account](../account/link.md) | 身份验证 | [api/admin/manage.dever:9:18](api/admin/manage.dever#L9,18) | +| [user.authorization.disable](app.dever#L22) | 领域内 | API 绑定 | [api/admin/manage.dever:9:18](api/admin/manage.dever#L9,18) | + +### user.authorization.api.admin.manage.grant + +定义:[api/admin/manage.dever:5:1](api/admin/manage.dever#L5,1) + + + +```mermaid +flowchart TD + n0(["user.authorization.api.admin.manage.grant"]) + click n0 href "api/admin/manage.dever#L5,14" "打开源码" + n1{"路由 POST /user/authorization/admin/manage/grant,topic #34;admin/manage#34; 接受请求?"} + n2(["返回 HTTP 404"]) + n3["进入 API"] + n4["取得认证时间"] + n5(["返回 HTTP 500"]) + n6["按 topic #34;admin/manage#34; 选择站点与 Provider"] + n7(["返回 HTTP 403"]) + n8{"读取 Bearer 或站点 Cookie;两者互斥"} + n9(["返回 HTTP 401"]) + n10["认证上下文就绪"] + n11(["返回 HTTP 401"]) + n12["校验 Token 的签名、有效期及站点"] + n13{"Cookie 且非 GET / HEAD / OPTIONS?"} + n14["检查站点 Origin"] + n15(["返回 HTTP 403"]) + n16(["返回 HTTP 401"]) + n17{"按 Provider.verify 选择验证函数"} + n18[["user.account.verify"]] + click n18 href "api/admin/manage.dever#L5,14" "打开源码" + n19(["返回 HTTP 业务错误映射状态;其他故障 500"]) + n20["用验证结果建立身份"] + n21(["返回 HTTP 认证错误状态"]) + n22(["返回 HTTP 500"]) + n23["建立请求数据库上下文"] + n24{"有已验证租户?"} + n25["数据库上下文就绪"] + n26["按已验证租户选择数据库"] + n27(["返回 HTTP 503"]) + n28{"检查租户组件启用状态:"} + n29(["返回 HTTP 403"]) + n30(["返回 HTTP 503"]) + n31["进入写入口事务"] + n32["失败:保留 HTTP 错误状态"] + n33["回滚入口拥有的事务"] + n34(["返回 HTTP 500"]) + n35(["返回错误响应"]) + n36{"权限检查:user.authorization.{site}.grant"} + n37["权限通过"] + n38["读取 API 输入"] + n39["解码具体参数并检查字段合同"] + n40[["user.authorization.grant"]] + click n40 href "api/admin/manage.dever#L5,14" "打开源码" + n41["按输出类型检查可序列化值并编码响应"] + n42["提交入口拥有的事务"] + n43(["返回 HTTP 500"]) + n44["发布暂存的认证 Header / Cookie"] + n45(["返回成功响应"]) + n0 --> n1 + n1 -->|"未匹配此入口:继续路由;均未匹配"| n2 + n1 --> n3 + n3 --> n4 + n4 -->|"失败"| n5 + n4 --> n6 + n6 -->|"无站点绑定"| n7 + n6 --> n8 + n8 -->|"凭据格式错误或冲突"| n9 + n8 -->|"无凭据"| n11 + n8 -->|"有凭据"| n13 + n13 -->|"否"| n12 + n13 -->|"是"| n14 + n14 -->|"不符合"| n15 + n14 -->|"通过"| n12 + n12 -->|"无 Provider 或 Token 无效"| n16 + n12 --> n17 + n17 -->|"user.account.verify"| n18 + n18 -->|"验证函数失败"| n19 + n18 --> n20 + n20 -->|"身份不合法"| n21 + n20 -->|"通过"| n10 + n17 -->|"无匹配验证函数"| n22 + n10 --> n23 + n23 --> n24 + n24 -->|"否"| n25 + n24 -->|"是"| n26 + n26 -->|"选择失败"| n27 + n26 --> n28 + n28 -->|"未启用"| n29 + n28 -->|"查询失败"| n30 + n28 -->|"允许"| n25 + n25 --> n31 + n32 --> n33 + n33 -->|"事务结束失败"| n34 + n33 --> n35 + n31 --> n36 + n36 -->|"拒绝:Access 状态;查询故障:500"| n32 + n36 -->|"允许"| n37 + n37 --> n38 + n38 -->|"失败:输入错误状态"| n32 + n38 --> n39 + n39 -->|"失败:400"| n32 + n39 --> n40 + n40 -->|"业务错误映射状态;其他故障:500"| n32 + n40 --> n41 + n41 -->|"编码失败:500"| n32 + n41 --> n42 + n42 -->|"提交失败"| n43 + n42 --> n44 + n44 --> n45 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.verify | [api/admin/manage.dever:5:14](api/admin/manage.dever#L5,14) | [user.account.verify](../account/link.md#flow-aaaacb87d61f819c-1) | +| user.authorization.grant | [api/admin/manage.dever:5:14](api/admin/manage.dever#L5,14) | [user.authorization.grant](link.md#flow-9d35689c5d6d754f-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [user.account.verify](../account/app/account.dever#L133) | 跨领域 [user.account](../account/link.md) | 身份验证 | [api/admin/manage.dever:5:14](api/admin/manage.dever#L5,14) | +| [user.authorization.grant](app.dever#L12) | 领域内 | API 绑定 | [api/admin/manage.dever:5:14](api/admin/manage.dever#L5,14) | + +### user.authorization.api.admin.manage.permissions + +定义:[api/admin/manage.dever:1:1](api/admin/manage.dever#L1,1) + + + +```mermaid +flowchart TD + n0(["user.authorization.api.admin.manage.permissions"]) + click n0 href "api/admin/manage.dever#L1,19" "打开源码" + n1{"路由 GET /user/authorization/admin/manage/permissions,topic #34;admin/manage#34; 接受请求?"} + n2(["返回 HTTP 404"]) + n3["进入 API"] + n4["取得认证时间"] + n5(["返回 HTTP 500"]) + n6["按 topic #34;admin/manage#34; 选择站点与 Provider"] + n7(["返回 HTTP 403"]) + n8{"读取 Bearer 或站点 Cookie;两者互斥"} + n9(["返回 HTTP 401"]) + n10["认证上下文就绪"] + n11(["返回 HTTP 401"]) + n12["校验 Token 的签名、有效期及站点"] + n13{"Cookie 且非 GET / HEAD / OPTIONS?"} + n14["检查站点 Origin"] + n15(["返回 HTTP 403"]) + n16(["返回 HTTP 401"]) + n17{"按 Provider.verify 选择验证函数"} + n18[["user.account.verify"]] + click n18 href "api/admin/manage.dever#L1,19" "打开源码" + n19(["返回 HTTP 业务错误映射状态;其他故障 500"]) + n20["用验证结果建立身份"] + n21(["返回 HTTP 认证错误状态"]) + n22(["返回 HTTP 500"]) + n23["建立请求数据库上下文"] + n24{"有已验证租户?"} + n25["数据库上下文就绪"] + n26["按已验证租户选择数据库"] + n27(["返回 HTTP 503"]) + n28{"检查租户组件启用状态:"} + n29(["返回 HTTP 403"]) + n30(["返回 HTTP 503"]) + n31["失败:保留 HTTP 错误状态"] + n32(["返回错误响应"]) + n33{"权限检查:user.authorization.{site}.permissions"} + n34["权限通过"] + n35["读取 API 输入"] + n36["解码具体参数并检查字段合同"] + n37[["user.authorization.permissions"]] + click n37 href "api/admin/manage.dever#L1,19" "打开源码" + n38["按输出类型检查可序列化值并编码响应"] + n39["发布暂存的认证 Header / Cookie"] + n40(["返回成功响应"]) + n0 --> n1 + n1 -->|"未匹配此入口:继续路由;均未匹配"| n2 + n1 --> n3 + n3 --> n4 + n4 -->|"失败"| n5 + n4 --> n6 + n6 -->|"无站点绑定"| n7 + n6 --> n8 + n8 -->|"凭据格式错误或冲突"| n9 + n8 -->|"无凭据"| n11 + n8 -->|"有凭据"| n13 + n13 -->|"否"| n12 + n13 -->|"是"| n14 + n14 -->|"不符合"| n15 + n14 -->|"通过"| n12 + n12 -->|"无 Provider 或 Token 无效"| n16 + n12 --> n17 + n17 -->|"user.account.verify"| n18 + n18 -->|"验证函数失败"| n19 + n18 --> n20 + n20 -->|"身份不合法"| n21 + n20 -->|"通过"| n10 + n17 -->|"无匹配验证函数"| n22 + n10 --> n23 + n23 --> n24 + n24 -->|"否"| n25 + n24 -->|"是"| n26 + n26 -->|"选择失败"| n27 + n26 --> n28 + n28 -->|"未启用"| n29 + n28 -->|"查询失败"| n30 + n28 -->|"允许"| n25 + n31 --> n32 + n25 --> n33 + n33 -->|"拒绝:Access 状态;查询故障:500"| n31 + n33 -->|"允许"| n34 + n34 --> n35 + n35 -->|"失败:输入错误状态"| n31 + n35 --> n36 + n36 -->|"失败:400"| n31 + n36 --> n37 + n37 -->|"业务错误映射状态;其他故障:500"| n31 + n37 --> n38 + n38 -->|"编码失败:500"| n31 + n38 --> n39 + n39 --> n40 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.verify | [api/admin/manage.dever:1:19](api/admin/manage.dever#L1,19) | [user.account.verify](../account/link.md#flow-aaaacb87d61f819c-1) | +| user.authorization.permissions | [api/admin/manage.dever:1:19](api/admin/manage.dever#L1,19) | [user.authorization.permissions](link.md#flow-a894f8a3aac1a477-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [user.account.verify](../account/app/account.dever#L133) | 跨领域 [user.account](../account/link.md) | 身份验证 | [api/admin/manage.dever:1:19](api/admin/manage.dever#L1,19) | +| [user.authorization.permissions](app.dever#L1) | 领域内 | API 绑定 | [api/admin/manage.dever:1:19](api/admin/manage.dever#L1,19) | + +### user.authorization.api.admin.manage.revoke + +定义:[api/admin/manage.dever:7:1](api/admin/manage.dever#L7,1) + + + +```mermaid +flowchart TD + n0(["user.authorization.api.admin.manage.revoke"]) + click n0 href "api/admin/manage.dever#L7,17" "打开源码" + n1{"路由 DELETE /user/authorization/admin/manage/revoke,topic #34;admin/manage#34; 接受请求?"} + n2(["返回 HTTP 404"]) + n3["进入 API"] + n4["取得认证时间"] + n5(["返回 HTTP 500"]) + n6["按 topic #34;admin/manage#34; 选择站点与 Provider"] + n7(["返回 HTTP 403"]) + n8{"读取 Bearer 或站点 Cookie;两者互斥"} + n9(["返回 HTTP 401"]) + n10["认证上下文就绪"] + n11(["返回 HTTP 401"]) + n12["校验 Token 的签名、有效期及站点"] + n13{"Cookie 且非 GET / HEAD / OPTIONS?"} + n14["检查站点 Origin"] + n15(["返回 HTTP 403"]) + n16(["返回 HTTP 401"]) + n17{"按 Provider.verify 选择验证函数"} + n18[["user.account.verify"]] + click n18 href "api/admin/manage.dever#L7,17" "打开源码" + n19(["返回 HTTP 业务错误映射状态;其他故障 500"]) + n20["用验证结果建立身份"] + n21(["返回 HTTP 认证错误状态"]) + n22(["返回 HTTP 500"]) + n23["建立请求数据库上下文"] + n24{"有已验证租户?"} + n25["数据库上下文就绪"] + n26["按已验证租户选择数据库"] + n27(["返回 HTTP 503"]) + n28{"检查租户组件启用状态:"} + n29(["返回 HTTP 403"]) + n30(["返回 HTTP 503"]) + n31["进入写入口事务"] + n32["失败:保留 HTTP 错误状态"] + n33["回滚入口拥有的事务"] + n34(["返回 HTTP 500"]) + n35(["返回错误响应"]) + n36{"权限检查:user.authorization.{site}.revoke"} + n37["权限通过"] + n38["读取 API 输入"] + n39["解码具体参数并检查字段合同"] + n40[["user.authorization.revoke"]] + click n40 href "api/admin/manage.dever#L7,17" "打开源码" + n41["按输出类型检查可序列化值并编码响应"] + n42["提交入口拥有的事务"] + n43(["返回 HTTP 500"]) + n44["发布暂存的认证 Header / Cookie"] + n45(["返回成功响应"]) + n0 --> n1 + n1 -->|"未匹配此入口:继续路由;均未匹配"| n2 + n1 --> n3 + n3 --> n4 + n4 -->|"失败"| n5 + n4 --> n6 + n6 -->|"无站点绑定"| n7 + n6 --> n8 + n8 -->|"凭据格式错误或冲突"| n9 + n8 -->|"无凭据"| n11 + n8 -->|"有凭据"| n13 + n13 -->|"否"| n12 + n13 -->|"是"| n14 + n14 -->|"不符合"| n15 + n14 -->|"通过"| n12 + n12 -->|"无 Provider 或 Token 无效"| n16 + n12 --> n17 + n17 -->|"user.account.verify"| n18 + n18 -->|"验证函数失败"| n19 + n18 --> n20 + n20 -->|"身份不合法"| n21 + n20 -->|"通过"| n10 + n17 -->|"无匹配验证函数"| n22 + n10 --> n23 + n23 --> n24 + n24 -->|"否"| n25 + n24 -->|"是"| n26 + n26 -->|"选择失败"| n27 + n26 --> n28 + n28 -->|"未启用"| n29 + n28 -->|"查询失败"| n30 + n28 -->|"允许"| n25 + n25 --> n31 + n32 --> n33 + n33 -->|"事务结束失败"| n34 + n33 --> n35 + n31 --> n36 + n36 -->|"拒绝:Access 状态;查询故障:500"| n32 + n36 -->|"允许"| n37 + n37 --> n38 + n38 -->|"失败:输入错误状态"| n32 + n38 --> n39 + n39 -->|"失败:400"| n32 + n39 --> n40 + n40 -->|"业务错误映射状态;其他故障:500"| n32 + n40 --> n41 + n41 -->|"编码失败:500"| n32 + n41 --> n42 + n42 -->|"提交失败"| n43 + n42 --> n44 + n44 --> n45 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.verify | [api/admin/manage.dever:7:17](api/admin/manage.dever#L7,17) | [user.account.verify](../account/link.md#flow-aaaacb87d61f819c-1) | +| user.authorization.revoke | [api/admin/manage.dever:7:17](api/admin/manage.dever#L7,17) | [user.authorization.revoke](link.md#flow-474dbe753fc89dfa-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [user.account.verify](../account/app/account.dever#L133) | 跨领域 [user.account](../account/link.md) | 身份验证 | [api/admin/manage.dever:7:17](api/admin/manage.dever#L7,17) | +| [user.authorization.revoke](app.dever#L17) | 领域内 | API 绑定 | [api/admin/manage.dever:7:17](api/admin/manage.dever#L7,17) | + +### user.authorization.api.admin.manage.save + +定义:[api/admin/manage.dever:3:1](api/admin/manage.dever#L3,1) + + + +```mermaid +flowchart TD + n0(["user.authorization.api.admin.manage.save"]) + click n0 href "api/admin/manage.dever#L3,13" "打开源码" + n1{"路由 POST /user/authorization/admin/manage/save,topic #34;admin/manage#34; 接受请求?"} + n2(["返回 HTTP 404"]) + n3["进入 API"] + n4["取得认证时间"] + n5(["返回 HTTP 500"]) + n6["按 topic #34;admin/manage#34; 选择站点与 Provider"] + n7(["返回 HTTP 403"]) + n8{"读取 Bearer 或站点 Cookie;两者互斥"} + n9(["返回 HTTP 401"]) + n10["认证上下文就绪"] + n11(["返回 HTTP 401"]) + n12["校验 Token 的签名、有效期及站点"] + n13{"Cookie 且非 GET / HEAD / OPTIONS?"} + n14["检查站点 Origin"] + n15(["返回 HTTP 403"]) + n16(["返回 HTTP 401"]) + n17{"按 Provider.verify 选择验证函数"} + n18[["user.account.verify"]] + click n18 href "api/admin/manage.dever#L3,13" "打开源码" + n19(["返回 HTTP 业务错误映射状态;其他故障 500"]) + n20["用验证结果建立身份"] + n21(["返回 HTTP 认证错误状态"]) + n22(["返回 HTTP 500"]) + n23["建立请求数据库上下文"] + n24{"有已验证租户?"} + n25["数据库上下文就绪"] + n26["按已验证租户选择数据库"] + n27(["返回 HTTP 503"]) + n28{"检查租户组件启用状态:"} + n29(["返回 HTTP 403"]) + n30(["返回 HTTP 503"]) + n31["进入写入口事务"] + n32["失败:保留 HTTP 错误状态"] + n33["回滚入口拥有的事务"] + n34(["返回 HTTP 500"]) + n35(["返回错误响应"]) + n36{"权限检查:user.authorization.{site}.save"} + n37["权限通过"] + n38["读取 API 输入"] + n39["解码具体参数并检查字段合同"] + n40[["user.authorization.save"]] + click n40 href "api/admin/manage.dever#L3,13" "打开源码" + n41["按输出类型检查可序列化值并编码响应"] + n42["提交入口拥有的事务"] + n43(["返回 HTTP 500"]) + n44["发布暂存的认证 Header / Cookie"] + n45(["返回成功响应"]) + n0 --> n1 + n1 -->|"未匹配此入口:继续路由;均未匹配"| n2 + n1 --> n3 + n3 --> n4 + n4 -->|"失败"| n5 + n4 --> n6 + n6 -->|"无站点绑定"| n7 + n6 --> n8 + n8 -->|"凭据格式错误或冲突"| n9 + n8 -->|"无凭据"| n11 + n8 -->|"有凭据"| n13 + n13 -->|"否"| n12 + n13 -->|"是"| n14 + n14 -->|"不符合"| n15 + n14 -->|"通过"| n12 + n12 -->|"无 Provider 或 Token 无效"| n16 + n12 --> n17 + n17 -->|"user.account.verify"| n18 + n18 -->|"验证函数失败"| n19 + n18 --> n20 + n20 -->|"身份不合法"| n21 + n20 -->|"通过"| n10 + n17 -->|"无匹配验证函数"| n22 + n10 --> n23 + n23 --> n24 + n24 -->|"否"| n25 + n24 -->|"是"| n26 + n26 -->|"选择失败"| n27 + n26 --> n28 + n28 -->|"未启用"| n29 + n28 -->|"查询失败"| n30 + n28 -->|"允许"| n25 + n25 --> n31 + n32 --> n33 + n33 -->|"事务结束失败"| n34 + n33 --> n35 + n31 --> n36 + n36 -->|"拒绝:Access 状态;查询故障:500"| n32 + n36 -->|"允许"| n37 + n37 --> n38 + n38 -->|"失败:输入错误状态"| n32 + n38 --> n39 + n39 -->|"失败:400"| n32 + n39 --> n40 + n40 -->|"业务错误映射状态;其他故障:500"| n32 + n40 --> n41 + n41 -->|"编码失败:500"| n32 + n41 --> n42 + n42 -->|"提交失败"| n43 + n42 --> n44 + n44 --> n45 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.verify | [api/admin/manage.dever:3:13](api/admin/manage.dever#L3,13) | [user.account.verify](../account/link.md#flow-aaaacb87d61f819c-1) | +| user.authorization.save | [api/admin/manage.dever:3:13](api/admin/manage.dever#L3,13) | [user.authorization.save](link.md#flow-2d4338c207d80210-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [user.account.verify](../account/app/account.dever#L133) | 跨领域 [user.account](../account/link.md) | 身份验证 | [api/admin/manage.dever:3:13](api/admin/manage.dever#L3,13) | +| [user.authorization.save](app.dever#L5) | 领域内 | API 绑定 | [api/admin/manage.dever:3:13](api/admin/manage.dever#L3,13) | + +### user.authorization.api.front.manage.disable + +定义:[api/front/manage.dever:9:1](api/front/manage.dever#L9,1) + + + +```mermaid +flowchart TD + n0(["user.authorization.api.front.manage.disable"]) + click n0 href "api/front/manage.dever#L9,18" "打开源码" + n1{"路由 DELETE /user/authorization/front/manage/disable,topic #34;front/manage#34; 接受请求?"} + n2(["返回 HTTP 404"]) + n3["进入 API"] + n4["取得认证时间"] + n5(["返回 HTTP 500"]) + n6["按 topic #34;front/manage#34; 选择站点与 Provider"] + n7(["返回 HTTP 403"]) + n8{"读取 Bearer 或站点 Cookie;两者互斥"} + n9(["返回 HTTP 401"]) + n10["认证上下文就绪"] + n11(["返回 HTTP 401"]) + n12["校验 Token 的签名、有效期及站点"] + n13{"Cookie 且非 GET / HEAD / OPTIONS?"} + n14["检查站点 Origin"] + n15(["返回 HTTP 403"]) + n16(["返回 HTTP 401"]) + n17{"按 Provider.verify 选择验证函数"} + n18[["user.account.verify"]] + click n18 href "api/front/manage.dever#L9,18" "打开源码" + n19(["返回 HTTP 业务错误映射状态;其他故障 500"]) + n20["用验证结果建立身份"] + n21(["返回 HTTP 认证错误状态"]) + n22(["返回 HTTP 500"]) + n23["建立请求数据库上下文"] + n24{"有已验证租户?"} + n25["数据库上下文就绪"] + n26["按已验证租户选择数据库"] + n27(["返回 HTTP 503"]) + n28{"检查租户组件启用状态:"} + n29(["返回 HTTP 403"]) + n30(["返回 HTTP 503"]) + n31["进入写入口事务"] + n32["失败:保留 HTTP 错误状态"] + n33["回滚入口拥有的事务"] + n34(["返回 HTTP 500"]) + n35(["返回错误响应"]) + n36{"权限检查:user.authorization.{site}.disable"} + n37["权限通过"] + n38["读取 API 输入"] + n39["解码具体参数并检查字段合同"] + n40[["user.authorization.disable"]] + click n40 href "api/front/manage.dever#L9,18" "打开源码" + n41["按输出类型检查可序列化值并编码响应"] + n42["提交入口拥有的事务"] + n43(["返回 HTTP 500"]) + n44["发布暂存的认证 Header / Cookie"] + n45(["返回成功响应"]) + n0 --> n1 + n1 -->|"未匹配此入口:继续路由;均未匹配"| n2 + n1 --> n3 + n3 --> n4 + n4 -->|"失败"| n5 + n4 --> n6 + n6 -->|"无站点绑定"| n7 + n6 --> n8 + n8 -->|"凭据格式错误或冲突"| n9 + n8 -->|"无凭据"| n11 + n8 -->|"有凭据"| n13 + n13 -->|"否"| n12 + n13 -->|"是"| n14 + n14 -->|"不符合"| n15 + n14 -->|"通过"| n12 + n12 -->|"无 Provider 或 Token 无效"| n16 + n12 --> n17 + n17 -->|"user.account.verify"| n18 + n18 -->|"验证函数失败"| n19 + n18 --> n20 + n20 -->|"身份不合法"| n21 + n20 -->|"通过"| n10 + n17 -->|"无匹配验证函数"| n22 + n10 --> n23 + n23 --> n24 + n24 -->|"否"| n25 + n24 -->|"是"| n26 + n26 -->|"选择失败"| n27 + n26 --> n28 + n28 -->|"未启用"| n29 + n28 -->|"查询失败"| n30 + n28 -->|"允许"| n25 + n25 --> n31 + n32 --> n33 + n33 -->|"事务结束失败"| n34 + n33 --> n35 + n31 --> n36 + n36 -->|"拒绝:Access 状态;查询故障:500"| n32 + n36 -->|"允许"| n37 + n37 --> n38 + n38 -->|"失败:输入错误状态"| n32 + n38 --> n39 + n39 -->|"失败:400"| n32 + n39 --> n40 + n40 -->|"业务错误映射状态;其他故障:500"| n32 + n40 --> n41 + n41 -->|"编码失败:500"| n32 + n41 --> n42 + n42 -->|"提交失败"| n43 + n42 --> n44 + n44 --> n45 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.verify | [api/front/manage.dever:9:18](api/front/manage.dever#L9,18) | [user.account.verify](../account/link.md#flow-aaaacb87d61f819c-1) | +| user.authorization.disable | [api/front/manage.dever:9:18](api/front/manage.dever#L9,18) | [user.authorization.disable](link.md#flow-57c59248be965dfd-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [user.account.verify](../account/app/account.dever#L133) | 跨领域 [user.account](../account/link.md) | 身份验证 | [api/front/manage.dever:9:18](api/front/manage.dever#L9,18) | +| [user.authorization.disable](app.dever#L22) | 领域内 | API 绑定 | [api/front/manage.dever:9:18](api/front/manage.dever#L9,18) | + +### user.authorization.api.front.manage.grant + +定义:[api/front/manage.dever:5:1](api/front/manage.dever#L5,1) + + + +```mermaid +flowchart TD + n0(["user.authorization.api.front.manage.grant"]) + click n0 href "api/front/manage.dever#L5,14" "打开源码" + n1{"路由 POST /user/authorization/front/manage/grant,topic #34;front/manage#34; 接受请求?"} + n2(["返回 HTTP 404"]) + n3["进入 API"] + n4["取得认证时间"] + n5(["返回 HTTP 500"]) + n6["按 topic #34;front/manage#34; 选择站点与 Provider"] + n7(["返回 HTTP 403"]) + n8{"读取 Bearer 或站点 Cookie;两者互斥"} + n9(["返回 HTTP 401"]) + n10["认证上下文就绪"] + n11(["返回 HTTP 401"]) + n12["校验 Token 的签名、有效期及站点"] + n13{"Cookie 且非 GET / HEAD / OPTIONS?"} + n14["检查站点 Origin"] + n15(["返回 HTTP 403"]) + n16(["返回 HTTP 401"]) + n17{"按 Provider.verify 选择验证函数"} + n18[["user.account.verify"]] + click n18 href "api/front/manage.dever#L5,14" "打开源码" + n19(["返回 HTTP 业务错误映射状态;其他故障 500"]) + n20["用验证结果建立身份"] + n21(["返回 HTTP 认证错误状态"]) + n22(["返回 HTTP 500"]) + n23["建立请求数据库上下文"] + n24{"有已验证租户?"} + n25["数据库上下文就绪"] + n26["按已验证租户选择数据库"] + n27(["返回 HTTP 503"]) + n28{"检查租户组件启用状态:"} + n29(["返回 HTTP 403"]) + n30(["返回 HTTP 503"]) + n31["进入写入口事务"] + n32["失败:保留 HTTP 错误状态"] + n33["回滚入口拥有的事务"] + n34(["返回 HTTP 500"]) + n35(["返回错误响应"]) + n36{"权限检查:user.authorization.{site}.grant"} + n37["权限通过"] + n38["读取 API 输入"] + n39["解码具体参数并检查字段合同"] + n40[["user.authorization.grant"]] + click n40 href "api/front/manage.dever#L5,14" "打开源码" + n41["按输出类型检查可序列化值并编码响应"] + n42["提交入口拥有的事务"] + n43(["返回 HTTP 500"]) + n44["发布暂存的认证 Header / Cookie"] + n45(["返回成功响应"]) + n0 --> n1 + n1 -->|"未匹配此入口:继续路由;均未匹配"| n2 + n1 --> n3 + n3 --> n4 + n4 -->|"失败"| n5 + n4 --> n6 + n6 -->|"无站点绑定"| n7 + n6 --> n8 + n8 -->|"凭据格式错误或冲突"| n9 + n8 -->|"无凭据"| n11 + n8 -->|"有凭据"| n13 + n13 -->|"否"| n12 + n13 -->|"是"| n14 + n14 -->|"不符合"| n15 + n14 -->|"通过"| n12 + n12 -->|"无 Provider 或 Token 无效"| n16 + n12 --> n17 + n17 -->|"user.account.verify"| n18 + n18 -->|"验证函数失败"| n19 + n18 --> n20 + n20 -->|"身份不合法"| n21 + n20 -->|"通过"| n10 + n17 -->|"无匹配验证函数"| n22 + n10 --> n23 + n23 --> n24 + n24 -->|"否"| n25 + n24 -->|"是"| n26 + n26 -->|"选择失败"| n27 + n26 --> n28 + n28 -->|"未启用"| n29 + n28 -->|"查询失败"| n30 + n28 -->|"允许"| n25 + n25 --> n31 + n32 --> n33 + n33 -->|"事务结束失败"| n34 + n33 --> n35 + n31 --> n36 + n36 -->|"拒绝:Access 状态;查询故障:500"| n32 + n36 -->|"允许"| n37 + n37 --> n38 + n38 -->|"失败:输入错误状态"| n32 + n38 --> n39 + n39 -->|"失败:400"| n32 + n39 --> n40 + n40 -->|"业务错误映射状态;其他故障:500"| n32 + n40 --> n41 + n41 -->|"编码失败:500"| n32 + n41 --> n42 + n42 -->|"提交失败"| n43 + n42 --> n44 + n44 --> n45 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.verify | [api/front/manage.dever:5:14](api/front/manage.dever#L5,14) | [user.account.verify](../account/link.md#flow-aaaacb87d61f819c-1) | +| user.authorization.grant | [api/front/manage.dever:5:14](api/front/manage.dever#L5,14) | [user.authorization.grant](link.md#flow-9d35689c5d6d754f-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [user.account.verify](../account/app/account.dever#L133) | 跨领域 [user.account](../account/link.md) | 身份验证 | [api/front/manage.dever:5:14](api/front/manage.dever#L5,14) | +| [user.authorization.grant](app.dever#L12) | 领域内 | API 绑定 | [api/front/manage.dever:5:14](api/front/manage.dever#L5,14) | + +### user.authorization.api.front.manage.permissions + +定义:[api/front/manage.dever:1:1](api/front/manage.dever#L1,1) + + + +```mermaid +flowchart TD + n0(["user.authorization.api.front.manage.permissions"]) + click n0 href "api/front/manage.dever#L1,19" "打开源码" + n1{"路由 GET /user/authorization/front/manage/permissions,topic #34;front/manage#34; 接受请求?"} + n2(["返回 HTTP 404"]) + n3["进入 API"] + n4["取得认证时间"] + n5(["返回 HTTP 500"]) + n6["按 topic #34;front/manage#34; 选择站点与 Provider"] + n7(["返回 HTTP 403"]) + n8{"读取 Bearer 或站点 Cookie;两者互斥"} + n9(["返回 HTTP 401"]) + n10["认证上下文就绪"] + n11(["返回 HTTP 401"]) + n12["校验 Token 的签名、有效期及站点"] + n13{"Cookie 且非 GET / HEAD / OPTIONS?"} + n14["检查站点 Origin"] + n15(["返回 HTTP 403"]) + n16(["返回 HTTP 401"]) + n17{"按 Provider.verify 选择验证函数"} + n18[["user.account.verify"]] + click n18 href "api/front/manage.dever#L1,19" "打开源码" + n19(["返回 HTTP 业务错误映射状态;其他故障 500"]) + n20["用验证结果建立身份"] + n21(["返回 HTTP 认证错误状态"]) + n22(["返回 HTTP 500"]) + n23["建立请求数据库上下文"] + n24{"有已验证租户?"} + n25["数据库上下文就绪"] + n26["按已验证租户选择数据库"] + n27(["返回 HTTP 503"]) + n28{"检查租户组件启用状态:"} + n29(["返回 HTTP 403"]) + n30(["返回 HTTP 503"]) + n31["失败:保留 HTTP 错误状态"] + n32(["返回错误响应"]) + n33{"权限检查:user.authorization.{site}.permissions"} + n34["权限通过"] + n35["读取 API 输入"] + n36["解码具体参数并检查字段合同"] + n37[["user.authorization.permissions"]] + click n37 href "api/front/manage.dever#L1,19" "打开源码" + n38["按输出类型检查可序列化值并编码响应"] + n39["发布暂存的认证 Header / Cookie"] + n40(["返回成功响应"]) + n0 --> n1 + n1 -->|"未匹配此入口:继续路由;均未匹配"| n2 + n1 --> n3 + n3 --> n4 + n4 -->|"失败"| n5 + n4 --> n6 + n6 -->|"无站点绑定"| n7 + n6 --> n8 + n8 -->|"凭据格式错误或冲突"| n9 + n8 -->|"无凭据"| n11 + n8 -->|"有凭据"| n13 + n13 -->|"否"| n12 + n13 -->|"是"| n14 + n14 -->|"不符合"| n15 + n14 -->|"通过"| n12 + n12 -->|"无 Provider 或 Token 无效"| n16 + n12 --> n17 + n17 -->|"user.account.verify"| n18 + n18 -->|"验证函数失败"| n19 + n18 --> n20 + n20 -->|"身份不合法"| n21 + n20 -->|"通过"| n10 + n17 -->|"无匹配验证函数"| n22 + n10 --> n23 + n23 --> n24 + n24 -->|"否"| n25 + n24 -->|"是"| n26 + n26 -->|"选择失败"| n27 + n26 --> n28 + n28 -->|"未启用"| n29 + n28 -->|"查询失败"| n30 + n28 -->|"允许"| n25 + n31 --> n32 + n25 --> n33 + n33 -->|"拒绝:Access 状态;查询故障:500"| n31 + n33 -->|"允许"| n34 + n34 --> n35 + n35 -->|"失败:输入错误状态"| n31 + n35 --> n36 + n36 -->|"失败:400"| n31 + n36 --> n37 + n37 -->|"业务错误映射状态;其他故障:500"| n31 + n37 --> n38 + n38 -->|"编码失败:500"| n31 + n38 --> n39 + n39 --> n40 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.verify | [api/front/manage.dever:1:19](api/front/manage.dever#L1,19) | [user.account.verify](../account/link.md#flow-aaaacb87d61f819c-1) | +| user.authorization.permissions | [api/front/manage.dever:1:19](api/front/manage.dever#L1,19) | [user.authorization.permissions](link.md#flow-a894f8a3aac1a477-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [user.account.verify](../account/app/account.dever#L133) | 跨领域 [user.account](../account/link.md) | 身份验证 | [api/front/manage.dever:1:19](api/front/manage.dever#L1,19) | +| [user.authorization.permissions](app.dever#L1) | 领域内 | API 绑定 | [api/front/manage.dever:1:19](api/front/manage.dever#L1,19) | + +### user.authorization.api.front.manage.revoke + +定义:[api/front/manage.dever:7:1](api/front/manage.dever#L7,1) + + + +```mermaid +flowchart TD + n0(["user.authorization.api.front.manage.revoke"]) + click n0 href "api/front/manage.dever#L7,17" "打开源码" + n1{"路由 DELETE /user/authorization/front/manage/revoke,topic #34;front/manage#34; 接受请求?"} + n2(["返回 HTTP 404"]) + n3["进入 API"] + n4["取得认证时间"] + n5(["返回 HTTP 500"]) + n6["按 topic #34;front/manage#34; 选择站点与 Provider"] + n7(["返回 HTTP 403"]) + n8{"读取 Bearer 或站点 Cookie;两者互斥"} + n9(["返回 HTTP 401"]) + n10["认证上下文就绪"] + n11(["返回 HTTP 401"]) + n12["校验 Token 的签名、有效期及站点"] + n13{"Cookie 且非 GET / HEAD / OPTIONS?"} + n14["检查站点 Origin"] + n15(["返回 HTTP 403"]) + n16(["返回 HTTP 401"]) + n17{"按 Provider.verify 选择验证函数"} + n18[["user.account.verify"]] + click n18 href "api/front/manage.dever#L7,17" "打开源码" + n19(["返回 HTTP 业务错误映射状态;其他故障 500"]) + n20["用验证结果建立身份"] + n21(["返回 HTTP 认证错误状态"]) + n22(["返回 HTTP 500"]) + n23["建立请求数据库上下文"] + n24{"有已验证租户?"} + n25["数据库上下文就绪"] + n26["按已验证租户选择数据库"] + n27(["返回 HTTP 503"]) + n28{"检查租户组件启用状态:"} + n29(["返回 HTTP 403"]) + n30(["返回 HTTP 503"]) + n31["进入写入口事务"] + n32["失败:保留 HTTP 错误状态"] + n33["回滚入口拥有的事务"] + n34(["返回 HTTP 500"]) + n35(["返回错误响应"]) + n36{"权限检查:user.authorization.{site}.revoke"} + n37["权限通过"] + n38["读取 API 输入"] + n39["解码具体参数并检查字段合同"] + n40[["user.authorization.revoke"]] + click n40 href "api/front/manage.dever#L7,17" "打开源码" + n41["按输出类型检查可序列化值并编码响应"] + n42["提交入口拥有的事务"] + n43(["返回 HTTP 500"]) + n44["发布暂存的认证 Header / Cookie"] + n45(["返回成功响应"]) + n0 --> n1 + n1 -->|"未匹配此入口:继续路由;均未匹配"| n2 + n1 --> n3 + n3 --> n4 + n4 -->|"失败"| n5 + n4 --> n6 + n6 -->|"无站点绑定"| n7 + n6 --> n8 + n8 -->|"凭据格式错误或冲突"| n9 + n8 -->|"无凭据"| n11 + n8 -->|"有凭据"| n13 + n13 -->|"否"| n12 + n13 -->|"是"| n14 + n14 -->|"不符合"| n15 + n14 -->|"通过"| n12 + n12 -->|"无 Provider 或 Token 无效"| n16 + n12 --> n17 + n17 -->|"user.account.verify"| n18 + n18 -->|"验证函数失败"| n19 + n18 --> n20 + n20 -->|"身份不合法"| n21 + n20 -->|"通过"| n10 + n17 -->|"无匹配验证函数"| n22 + n10 --> n23 + n23 --> n24 + n24 -->|"否"| n25 + n24 -->|"是"| n26 + n26 -->|"选择失败"| n27 + n26 --> n28 + n28 -->|"未启用"| n29 + n28 -->|"查询失败"| n30 + n28 -->|"允许"| n25 + n25 --> n31 + n32 --> n33 + n33 -->|"事务结束失败"| n34 + n33 --> n35 + n31 --> n36 + n36 -->|"拒绝:Access 状态;查询故障:500"| n32 + n36 -->|"允许"| n37 + n37 --> n38 + n38 -->|"失败:输入错误状态"| n32 + n38 --> n39 + n39 -->|"失败:400"| n32 + n39 --> n40 + n40 -->|"业务错误映射状态;其他故障:500"| n32 + n40 --> n41 + n41 -->|"编码失败:500"| n32 + n41 --> n42 + n42 -->|"提交失败"| n43 + n42 --> n44 + n44 --> n45 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.verify | [api/front/manage.dever:7:17](api/front/manage.dever#L7,17) | [user.account.verify](../account/link.md#flow-aaaacb87d61f819c-1) | +| user.authorization.revoke | [api/front/manage.dever:7:17](api/front/manage.dever#L7,17) | [user.authorization.revoke](link.md#flow-474dbe753fc89dfa-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [user.account.verify](../account/app/account.dever#L133) | 跨领域 [user.account](../account/link.md) | 身份验证 | [api/front/manage.dever:7:17](api/front/manage.dever#L7,17) | +| [user.authorization.revoke](app.dever#L17) | 领域内 | API 绑定 | [api/front/manage.dever:7:17](api/front/manage.dever#L7,17) | + +### user.authorization.api.front.manage.save + +定义:[api/front/manage.dever:3:1](api/front/manage.dever#L3,1) + + + +```mermaid +flowchart TD + n0(["user.authorization.api.front.manage.save"]) + click n0 href "api/front/manage.dever#L3,13" "打开源码" + n1{"路由 POST /user/authorization/front/manage/save,topic #34;front/manage#34; 接受请求?"} + n2(["返回 HTTP 404"]) + n3["进入 API"] + n4["取得认证时间"] + n5(["返回 HTTP 500"]) + n6["按 topic #34;front/manage#34; 选择站点与 Provider"] + n7(["返回 HTTP 403"]) + n8{"读取 Bearer 或站点 Cookie;两者互斥"} + n9(["返回 HTTP 401"]) + n10["认证上下文就绪"] + n11(["返回 HTTP 401"]) + n12["校验 Token 的签名、有效期及站点"] + n13{"Cookie 且非 GET / HEAD / OPTIONS?"} + n14["检查站点 Origin"] + n15(["返回 HTTP 403"]) + n16(["返回 HTTP 401"]) + n17{"按 Provider.verify 选择验证函数"} + n18[["user.account.verify"]] + click n18 href "api/front/manage.dever#L3,13" "打开源码" + n19(["返回 HTTP 业务错误映射状态;其他故障 500"]) + n20["用验证结果建立身份"] + n21(["返回 HTTP 认证错误状态"]) + n22(["返回 HTTP 500"]) + n23["建立请求数据库上下文"] + n24{"有已验证租户?"} + n25["数据库上下文就绪"] + n26["按已验证租户选择数据库"] + n27(["返回 HTTP 503"]) + n28{"检查租户组件启用状态:"} + n29(["返回 HTTP 403"]) + n30(["返回 HTTP 503"]) + n31["进入写入口事务"] + n32["失败:保留 HTTP 错误状态"] + n33["回滚入口拥有的事务"] + n34(["返回 HTTP 500"]) + n35(["返回错误响应"]) + n36{"权限检查:user.authorization.{site}.save"} + n37["权限通过"] + n38["读取 API 输入"] + n39["解码具体参数并检查字段合同"] + n40[["user.authorization.save"]] + click n40 href "api/front/manage.dever#L3,13" "打开源码" + n41["按输出类型检查可序列化值并编码响应"] + n42["提交入口拥有的事务"] + n43(["返回 HTTP 500"]) + n44["发布暂存的认证 Header / Cookie"] + n45(["返回成功响应"]) + n0 --> n1 + n1 -->|"未匹配此入口:继续路由;均未匹配"| n2 + n1 --> n3 + n3 --> n4 + n4 -->|"失败"| n5 + n4 --> n6 + n6 -->|"无站点绑定"| n7 + n6 --> n8 + n8 -->|"凭据格式错误或冲突"| n9 + n8 -->|"无凭据"| n11 + n8 -->|"有凭据"| n13 + n13 -->|"否"| n12 + n13 -->|"是"| n14 + n14 -->|"不符合"| n15 + n14 -->|"通过"| n12 + n12 -->|"无 Provider 或 Token 无效"| n16 + n12 --> n17 + n17 -->|"user.account.verify"| n18 + n18 -->|"验证函数失败"| n19 + n18 --> n20 + n20 -->|"身份不合法"| n21 + n20 -->|"通过"| n10 + n17 -->|"无匹配验证函数"| n22 + n10 --> n23 + n23 --> n24 + n24 -->|"否"| n25 + n24 -->|"是"| n26 + n26 -->|"选择失败"| n27 + n26 --> n28 + n28 -->|"未启用"| n29 + n28 -->|"查询失败"| n30 + n28 -->|"允许"| n25 + n25 --> n31 + n32 --> n33 + n33 -->|"事务结束失败"| n34 + n33 --> n35 + n31 --> n36 + n36 -->|"拒绝:Access 状态;查询故障:500"| n32 + n36 -->|"允许"| n37 + n37 --> n38 + n38 -->|"失败:输入错误状态"| n32 + n38 --> n39 + n39 -->|"失败:400"| n32 + n39 --> n40 + n40 -->|"业务错误映射状态;其他故障:500"| n32 + n40 --> n41 + n41 -->|"编码失败:500"| n32 + n41 --> n42 + n42 -->|"提交失败"| n43 + n42 --> n44 + n44 --> n45 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| user.account.verify | [api/front/manage.dever:3:13](api/front/manage.dever#L3,13) | [user.account.verify](../account/link.md#flow-aaaacb87d61f819c-1) | +| user.authorization.save | [api/front/manage.dever:3:13](api/front/manage.dever#L3,13) | [user.authorization.save](link.md#flow-2d4338c207d80210-1) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| [user.account.verify](../account/app/account.dever#L133) | 跨领域 [user.account](../account/link.md) | 身份验证 | [api/front/manage.dever:3:13](api/front/manage.dever#L3,13) | +| [user.authorization.save](app.dever#L5) | 领域内 | API 绑定 | [api/front/manage.dever:3:13](api/front/manage.dever#L3,13) | + +### user.authorization.disable + +定义:[app.dever:22:1](app.dever#L22,1) + + + +```mermaid +flowchart TD + n0(["disable"]) + click n0 href "app.dever#L22,1" "打开源码" + n1(["返回"]) + n3[["dever.auth.disableRole"]] + click n3 href "app.dever#L23,3" "打开源码" + n4["赋值 ok"] + click n4 href "app.dever#L24,8" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| dever.auth.disableRole | [app.dever:23:3](app.dever#L23,3) | 依赖或生成的分派(本页不展开) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| `dever.auth.disableRole` | 依赖 | 调用 | [app.dever:23:3](app.dever#L23,3) | + +### user.authorization.grant + +定义:[app.dever:12:1](app.dever#L12,1) + + + +```mermaid +flowchart TD + n0(["grant"]) + click n0 href "app.dever#L12,1" "打开源码" + n1(["返回"]) + n3[["dever.auth.grantRole"]] + click n3 href "app.dever#L13,3" "打开源码" + n4["赋值 ok"] + click n4 href "app.dever#L14,8" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| dever.auth.grantRole | [app.dever:13:3](app.dever#L13,3) | 依赖或生成的分派(本页不展开) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| `dever.auth.grantRole` | 依赖 | 调用 | [app.dever:13:3](app.dever#L13,3) | + +### user.authorization.permissions + +定义:[app.dever:1:1](app.dever#L1,1) + + + +```mermaid +flowchart TD + n0(["permissions"]) + click n0 href "app.dever#L1,1" "打开源码" + n1(["返回"]) + n3[["dever.auth.permissions"]] + click n3 href "app.dever#L2,12" "打开源码" + n4["赋值 values"] + click n4 href "app.dever#L2,12" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| dever.auth.permissions | [app.dever:2:12](app.dever#L2,12) | 依赖或生成的分派(本页不展开) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| `dever.auth.permissions` | 依赖 | 调用 | [app.dever:2:12](app.dever#L2,12) | + +### user.authorization.revoke + +定义:[app.dever:17:1](app.dever#L17,1) + + + +```mermaid +flowchart TD + n0(["revoke"]) + click n0 href "app.dever#L17,1" "打开源码" + n1(["返回"]) + n3[["dever.auth.revokeRole"]] + click n3 href "app.dever#L18,3" "打开源码" + n4["赋值 ok"] + click n4 href "app.dever#L19,8" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| dever.auth.revokeRole | [app.dever:18:3](app.dever#L18,3) | 依赖或生成的分派(本页不展开) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| `dever.auth.revokeRole` | 依赖 | 调用 | [app.dever:18:3](app.dever#L18,3) | + +### user.authorization.save + +定义:[app.dever:5:1](app.dever#L5,1) + + + +```mermaid +flowchart TD + n0(["save"]) + click n0 href "app.dever#L5,1" "打开源码" + n1(["返回"]) + n3[["dever.auth.saveRole"]] + click n3 href "app.dever#L8,3" "打开源码" + n4["赋值 ok"] + click n4 href "app.dever#L9,8" "打开源码" + n0 --> n3 + n3 --> n4 + n4 --> n1 +``` + +
+源码与子流程 + +| 节点 | 源码 | 子流程 | +| --- | --- | --- | +| dever.auth.saveRole | [app.dever:8:3](app.dever#L8,3) | 依赖或生成的分派(本页不展开) | + +
+ +| 目标 | 归属 | 方式 | 调用位置 | +| --- | --- | --- | --- | +| `dever.auth.saveRole` | 依赖 | 调用 | [app.dever:8:3](app.dever#L8,3) | diff --git a/examples/cms/dever/test/content/category/lifecycle.dever b/examples/cms/dever/test/content/category/lifecycle.dever new file mode 100644 index 0000000..8bc64aa --- /dev/null +++ b/examples/cms/dever/test/content/category/lifecycle.dever @@ -0,0 +1,13 @@ +lifecycle() () { + category = app.create(" Dever News ") + assertEq(category.name, "Dever News") + assertEq(category.slug, "dever news") + assertEq(category.status, "active") + app.requireActive(category.id) + app.requireActive(null) + assert(app.disable(category.id)) + independent = app.create("Announcements") + assert(independent.id != category.id) + assertEq(independent.slug, "announcements") + app.requireActive(independent.id) +} diff --git a/examples/cms/dever/test/news/article/publish.dever b/examples/cms/dever/test/news/article/publish.dever new file mode 100644 index 0000000..975718a --- /dev/null +++ b/examples/cms/dever/test/news/article/publish.dever @@ -0,0 +1,5 @@ +publish() () { + assertEq(domain.pageSize(1), 1) + assertEq(domain.pageSize(100), 100) + domain.requireUpdated(1) +} diff --git a/examples/cms/dever/test/user/account/bootstrap.dever b/examples/cms/dever/test/user/account/bootstrap.dever new file mode 100644 index 0000000..902ebb8 --- /dev/null +++ b/examples/cms/dever/test/user/account/bootstrap.dever @@ -0,0 +1,16 @@ +bootstrap() () { + created = user.account.bootstrap( + "tenant-one", + "Tenant One", + " owner@example.com ", + " Owner ", + secret("correct horse battery staple") + ) + app.requireMembership(created.user_id, created.tenant_id) + user.account.active(created.user_id) + tenant = platform.tenant.active("tenant-one") + assertEq(tenant.id, created.tenant_id) + assertEq(tenant.key, "tenant-one") + unrelated = platform.tenant.provision("tenant-two", "Tenant Two") + assert(unrelated.id != created.tenant_id) +} diff --git a/examples/cms/dever/test/user/account/lifecycle.dever b/examples/cms/dever/test/user/account/lifecycle.dever new file mode 100644 index 0000000..e2e5076 --- /dev/null +++ b/examples/cms/dever/test/user/account/lifecycle.dever @@ -0,0 +1,37 @@ +lifecycle() () { + account = app.bootstrap( + "sessions", + "Sessions", + "session@example.com", + "Session user", + secret("correct horse battery staple") + ) + session = app.open( + "session-subject", + account.user_id, + account.tenant_id, + "sessions", + "admin" + ) + authenticated = app.authenticate(session.uuid, "admin") + assertEq(authenticated.subject, "session-subject") + assertEq(authenticated.user_id, account.user_id) + assertEq(authenticated.tenant_id, account.tenant_id) + assertEq(authenticated.tenant_key, "sessions") + app.revoke(session.uuid) + dever.test.advanceClock(3600000) + app.cleanup() + replacement = app.open( + "new-subject", + account.user_id, + account.tenant_id, + "sessions", + "front" + ) + assert(replacement.uuid != session.uuid) + current = app.authenticate(replacement.uuid, "front") + assertEq(current.subject, "new-subject") + assertEq(current.user_id, account.user_id) + assertEq(current.tenant_id, account.tenant_id) + assertEq(current.tenant_key, "sessions") +} diff --git a/examples/cms/dever/test/user/account/validation.dever b/examples/cms/dever/test/user/account/validation.dever new file mode 100644 index 0000000..81d948d --- /dev/null +++ b/examples/cms/dever/test/user/account/validation.dever @@ -0,0 +1,4 @@ +validation() () { + domain.validateEmail("owner@example.com") + domain.validateEmail("editor@example.com") +} diff --git a/install b/install new file mode 100755 index 0000000..1aadcbc --- /dev/null +++ b/install @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +# 先安装 Dever 本体,再后台准备私有编译环境;--wait 用于同步安装。 +set -euo pipefail + +version=${DEVER_VERSION:-@DEVER_VERSION@} +install_root=${DEVER_INSTALL_DIR:-"$HOME/.dever"} +release_url= +modify_path=true +wait_for_environment=false +while (( $# )); do + case "$1" in + --version) version=${2:?--version requires a value}; shift 2 ;; + --prefix) install_root=${2:?--prefix requires a value}; shift 2 ;; + --release-url) release_url=${2:?--release-url requires a value}; shift 2 ;; + --no-modify-path) modify_path=false; shift ;; + --wait) wait_for_environment=true; shift ;; + --help|-h) + echo 'Usage: bash install [--version VERSION] [--prefix DIRECTORY] [--release-url URL] [--no-modify-path] [--wait]' + exit 0 ;; + *) echo "Unknown option: $1" >&2; exit 1 ;; + esac +done +if [[ ! $version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo 'Specify --version, or use the installer attached to a Dever release.' >&2 + exit 1 +fi +if [[ $(uname -s) != Linux || $(uname -m) != x86_64 ]]; then + echo 'This release supports Linux x86_64 only.' >&2 + exit 1 +fi +for tool in curl tar xz sha256sum flock getconf nohup; do + command -v "$tool" >/dev/null || { echo "Required installer tool: $tool" >&2; exit 1; } +done +glibc=$(getconf GNU_LIBC_VERSION) +glibc=${glibc#glibc } +if [[ $(printf '%s\n' 2.41 "$glibc" | sort -V | head -n 1) != 2.41 ]]; then + echo 'This release requires glibc 2.41 or newer; Debian 13 is validated.' >&2 + exit 1 +fi +mkdir -p "$install_root" +install_root=$(cd "$install_root" && pwd -P) +exec 9>"$install_root/.install.lock" +flock -n 9 || { echo 'Another Dever installation is running.' >&2; exit 1; } +mkdir -p "$install_root/bin" "$install_root/versions" "$install_root/toolchains" "$install_root/cache/cargo" +stage=$(mktemp -d "$install_root/.install.XXXXXX") +trap 'rm -rf -- "$stage"' EXIT +release_url=${release_url:-"https://git.dever.cc/dever/core/releases/download/v$version"} +release_url=${release_url%/} +triple=x86_64-unknown-linux-gnu + +download() { + local url=$1 file=$2 + curl --fail --location --show-error --progress-bar --retry 2 "$url" --output "$file" +} + +verified_download() { + local url=$1 file=$2 + download "$url" "$file" + download "$url.sha256" "$file.sha256" + (cd "$(dirname "$file")" && sha256sum --check "$(basename "$file").sha256") +} + +destination="$install_root/versions/$version" +if [[ -d $destination ]]; then + package=$destination + echo "Reusing Dever $version." +else + echo "Downloading Dever $version." + archive="dever-$version-linux-x86_64.tar.xz" + verified_download "$release_url/$archive" "$stage/$archive" + tar -xJf "$stage/$archive" -C "$stage" + package="$stage/dever" +fi +read -r rust_version native_id < "$package/toolchain/online" +if [[ ! $rust_version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ || ! $native_id =~ ^[a-f0-9]{64}$ ]]; then + echo 'Invalid toolchain metadata in the downloaded release.' >&2 + exit 1 +fi +if [[ $package != "$destination" ]]; then + # 暂存包与正式版本目录深度一致,使用相对链接共享工具和依赖缓存。 + ln -s "../../../toolchains/rust-$rust_version-$triple" "$package/toolchain/rust" + ln -s "../../../toolchains/native-$native_id/native" "$package/toolchain/native" + ln -s "../../../toolchains/native-$native_id/lib" "$package/toolchain/lib" + ln -s ../../../cache/cargo "$package/toolchain/cache" +fi +"$package/bin/dever" skill path >/dev/null +printf '%s\n%s\n' "$release_url" "${RUSTUP_DIST_SERVER:-https://static.rust-lang.org}" > "$package/toolchain/sources.new" +mv "$package/toolchain/sources.new" "$package/toolchain/sources" +if [[ $package != "$destination" ]]; then + mv "$package" "$destination" +fi +# 本体验证成功即可使用源码工具;编译命令等待后台环境准备完成。 +ln -s "../versions/$version/bin/dever" "$stage/command" +mv -Tf "$stage/command" "$install_root/bin/dever" +printf 'export PATH=%q/bin:$PATH\n' "$install_root" > "$install_root/env" +if $modify_path; then + shell_name=${SHELL:-/bin/sh} + case "${shell_name##*/}" in + zsh) profile="$HOME/.zshrc" ;; + bash) profile="$HOME/.bashrc" ;; + *) profile="$HOME/.profile" ;; + esac + printf -v path_line '. %q' "$install_root/env" + if [[ ! -f $profile ]] || ! grep -Fqx "$path_line" "$profile"; then + printf '\n%s\n' "$path_line" >> "$profile" + fi +fi +"$install_root/bin/dever" --version +printf 'Dever installed. Open a new terminal, or run: . %q\n' "$install_root/env" +echo 'Start a project: dever new hello' +flock -u 9 +if $wait_for_environment; then + bash "$destination/toolchain/prepare" +elif [[ ! -f $destination/toolchain/ready ]]; then + nohup bash "$destination/toolchain/prepare" /dev/null 2>&1 9>&- & + echo 'The build environment is preparing in the background. You can create, edit and check projects now.' + echo 'run/test/build will wait automatically if preparation is still in progress.' +else + echo 'Dever build environment is ready.' +fi diff --git a/lib/README.md b/lib/README.md new file mode 100644 index 0000000..a070186 --- /dev/null +++ b/lib/README.md @@ -0,0 +1,42 @@ +# 标准库 + +提供文本、数值、网络、数据库和并发能力,直接按包名调用,无需 import。签名从源码读取,主题说明解释用途和特殊规则。 + +- [十进制](doc/decimal.md) +- [Cookie](doc/dever-api-cookie.md) +- [请求信息](doc/dever-api-request.md) +- [API 错误](doc/dever-api.md) +- [业务权限](doc/dever-auth-capability.md) +- [角色与权限](doc/dever-auth-roles.md) +- [当前身份](doc/dever-auth-session.md) +- [身份声明](doc/dever-auth-values.md) +- [字节](doc/dever-bytes.md) +- [密码与摘要](doc/dever-crypto.md) +- [数据库错误](doc/dever-database.md) +- [HTTP 调用](doc/dever-http-call.md) +- [流式响应](doc/dever-http-live.md) +- [HTTP 连接池](doc/dever-http-pool.md) +- [流式请求](doc/dever-http-stream.md) +- [HTTP 数据结构](doc/dever-http-value.md) +- [文本标识](doc/dever-id.md) +- [文件与标准输出](doc/dever-io.md) +- [JSON 文档](doc/dever-json-value.md) +- [JSON](doc/dever-json.md) +- [TCP 监听](doc/dever-net-listener.md) +- [TCP 连接](doc/dever-net-socket.md) +- [进程与命令](doc/dever-process.md) +- [当前站点](doc/dever-site.md) +- [SSE](doc/dever-sse.md) +- [上传存储](doc/dever-storage.md) +- [异步等待](doc/dever-task.md) +- [日历结果](doc/dever-time-calendar.md) +- [时钟](doc/dever-time-clock.md) +- [日期与时长](doc/dever-time-operations.md) +- [同步等待](doc/dever-time.md) +- [TLS](doc/dever-tls.md) +- [WebSocket](doc/dever-websocket.md) +- [浮点数](doc/float.md) +- [整数](doc/int.md) +- [数学](doc/math.md) +- [文本](doc/text.md) +- [UUID](doc/uuid.md) diff --git a/lib/decimal.dever b/lib/decimal.dever new file mode 100644 index 0000000..f8a2cb8 --- /dev/null +++ b/lib/decimal.dever @@ -0,0 +1,15 @@ +public round(value: Decimal, places: Int) (result: Decimal) { + result = dever.system.decimalRound(value, places) +} + +public parse(value: Text) (result: Decimal?) { + result = dever.system.decimalParse(value) +} + +public toText(value: Decimal) (result: Text) { + result = dever.system.decimalToText(value) +} + +public fromInt(value: Int) (result: Decimal) { + result = dever.system.decimalFromInt(value) +} diff --git a/lib/dever/api.dever b/lib/dever/api.dever new file mode 100644 index 0000000..7e0eaaf --- /dev/null +++ b/lib/dever/api.dever @@ -0,0 +1,14 @@ +public type Error { + error Invalid + error Unauthorized + error Forbidden + error NotFound + error Conflict + error TooManyRequests +} + +public require(condition: true, error: Error) () pure recover("条件成立时不触发指定失败") {} + +public require(condition: false, error: Error) () pure recover("条件成立时不触发指定失败") { + fail(error) +} diff --git a/lib/dever/api/cookie.dever b/lib/dever/api/cookie.dever new file mode 100644 index 0000000..f4d2fee --- /dev/null +++ b/lib/dever/api/cookie.dever @@ -0,0 +1,26 @@ +public type SameSite { + Strict + Lax + None +} + +public type CookieOptions { + path: Text + domain: Text? + same_site: SameSite + secure: Bool + http_only: Bool + max_age: Int? +} + +public secureCookieOptions() (options: CookieOptions) pure { + options = CookieOptions { path = "/", domain = null, same_site = SameSite.Lax, secure = true, http_only = true, max_age = null } +} + +public cookie(name: Text) (value: Text?) { value = dever.system.apiCookie(name) } + +public secretCookie(name: Text) (value: Secret?) { value = dever.system.apiSecretCookie(name) } + +public setCookie(name: Text, value: Text, options: CookieOptions) () { dever.system.apiSetCookie(name, value, options) } + +public setSecretCookie(name: Text, value: Secret, options: CookieOptions) () { dever.system.apiSetSecretCookie(name, value, options) } diff --git a/lib/dever/api/request.dever b/lib/dever/api/request.dever new file mode 100644 index 0000000..74c401c --- /dev/null +++ b/lib/dever/api/request.dever @@ -0,0 +1,11 @@ +public requestId() (value: Text) { value = dever.system.apiRequestId() } + +public method() (value: Text) { value = dever.system.apiMethod() } + +public path() (value: Text) { value = dever.system.apiPath() } + +public clientAddress() (value: Text) { value = dever.system.apiClientAddress() } + +public header(name: Text) (value: Text?) { value = dever.system.apiHeader(name) } + +public setHeader(name: Text, value: Text) () { dever.system.apiSetHeader(name, value) } diff --git a/lib/dever/auth/capability.dever b/lib/dever/auth/capability.dever new file mode 100644 index 0000000..73982de --- /dev/null +++ b/lib/dever/auth/capability.dever @@ -0,0 +1,11 @@ +public hasProtectedRoles(user_id: Int) (value: Bool) { + value = dever.system.authHasProtectedRoles(user_id) +} + +public registerCapability(id: Uuid) (key: Text) { + key = dever.system.authRegisterCapability(id) +} + +public removeCapability(id: Uuid) () { + dever.system.authRemoveCapability(id) +} diff --git a/lib/dever/auth/roles.dever b/lib/dever/auth/roles.dever new file mode 100644 index 0000000..b21b911 --- /dev/null +++ b/lib/dever/auth/roles.dever @@ -0,0 +1,43 @@ +public type Permission { + key: Text + component: Text + domain: Text + site: Text + action: Text + method: Text + kind: Text +} + +public type Role { + id: Text + site: Text + name: Text + all_permissions: Bool + enabled: Bool + protected: Bool + permission_keys: List +} + +public permissions() (values: List) { values = dever.system.authPermissions() } + +public currentPermissions() (values: List) { values = dever.system.authCurrentPermissions() } + +public roles() (values: List) { values = dever.system.authRoles() } + +public role(id: Text) (value: Role?) { value = first(dever.system.authRole(id)) } + +public userRoles(user_id: Int) (values: List) { values = dever.system.authUserRoles(user_id) } + +public isOwner(user_id: Int) (value: Bool) { value = reduce(owner, userRoles(user_id), false) } + +public saveRole(id: Text, name: Text, all_permissions: Bool, permission_keys: List) () { dever.system.authSaveRole(id, name, all_permissions, permission_keys) } + +public grantRole(user_id: Int, role_id: Text) () { dever.system.authGrantRole(user_id, role_id) } + +public revokeRole(user_id: Int, role_id: Text) () { dever.system.authRevokeRole(user_id, role_id) } + +public disableRole(role_id: Text) () { dever.system.authDisableRole(role_id) } + +public deleteRole(role_id: Text) () { dever.system.authDeleteRole(role_id) } + +owner(role: Role, found: Bool) (value: Bool) pure { value = found or role.protected } diff --git a/lib/dever/auth/session.dever b/lib/dever/auth/session.dever new file mode 100644 index 0000000..c5e5741 --- /dev/null +++ b/lib/dever/auth/session.dever @@ -0,0 +1,13 @@ +public id() (value: Text) { value = dever.system.authId() } + +public session() (value: Text) { value = dever.system.authSession() } + +public userId() (value: Int?) { value = dever.system.authUserId() } + +public tenantId() (value: Int?) { value = dever.system.authTenantId() } + +public issue(subject: Text, session: Text, tenant: Text?) (value: Secret) { value = dever.system.authIssue(subject, session, tenant) } + +public issueCookie(subject: Text, session: Text, tenant: Text?) () { dever.system.authIssueCookie(subject, session, tenant) } + +public clearCookie() () { dever.system.authClearCookie() } diff --git a/lib/dever/auth/values.dever b/lib/dever/auth/values.dever new file mode 100644 index 0000000..9cf946e --- /dev/null +++ b/lib/dever/auth/values.dever @@ -0,0 +1,6 @@ +public type Claims { + subject: Text + session: Text + tenant: Text? + site: Text +} diff --git a/lib/dever/bytes.dever b/lib/dever/bytes.dever new file mode 100644 index 0000000..e43f634 --- /dev/null +++ b/lib/dever/bytes.dever @@ -0,0 +1,51 @@ +public type DecodeResult { + Decoded(text: Text) + error Failed(message: Text) +} + +public type BufferResult { + Ready(bytes: Bytes) + error Failed(message: Text) +} + +public fromText(text: Text) (bytes: Bytes) { + bytes = dever.system.bytesFromText(text) +} + +public toText(bytes: Bytes) (text: Text) { + text = decoded(dever.system.bytesToText(bytes)) +} + +decoded(result: DecodeResult.Decoded(value)) (text: Text) { + text = value +} +decoded(result: DecodeResult.Failed(message)) (text: Text) { + fail(DecodeResult.Failed(message)) +} + +public fromInts(values: List) (bytes: Bytes) { + bytes = ready(dever.system.bytesFromInts(values)) +} + +public length(bytes: Bytes) (length: Int) { + length = dever.system.bytesLength(bytes) +} + +public at(bytes: Bytes, index: Int) (value: Int?) { + value = dever.system.bytesAt(bytes, index) +} + +public slice(bytes: Bytes, start: Int, end: Int) (result: Bytes) { + result = ready(dever.system.bytesSlice(bytes, start, end)) +} + +ready(result: BufferResult.Ready(bytes)) (value: Bytes) { + value = bytes +} +ready(result: BufferResult.Failed(message)) (value: Bytes) { + fail(BufferResult.Failed(message)) +} + +public concat(left: Bytes, right: Bytes) (bytes: Bytes) { + bytes = dever.system.bytesConcat(left, right) +} diff --git a/lib/dever/crypto.dever b/lib/dever/crypto.dever new file mode 100644 index 0000000..a184752 --- /dev/null +++ b/lib/dever/crypto.dever @@ -0,0 +1,29 @@ +public type TokenResult { Ready(value: Secret) + error Failed(message: Text) } + +public type HashResult { Ready(value: Text) + error Failed(message: Text) } + +public type VerifyResult { Ready(value: Bool) + error Failed(message: Text) } + +public token(bytes: Int) (value: Secret) { value = tokenValue(dever.system.cryptoToken(bytes)) } + +public passwordHash(password: Secret) (value: Text) { value = hashValue(dever.system.cryptoPasswordHash(password)) } + +public passwordVerify(password: Secret, hash: Text) (value: Bool) { value = verifyValue(dever.system.cryptoPasswordVerify(password, hash)) } + +public sha256(bytes: Bytes) (value: Bytes) pure { value = dever.system.cryptoSha256(bytes) } + +public hmacSha256(key: Secret, bytes: Bytes) (value: Bytes) pure { value = dever.system.cryptoHmacSha256(key, bytes) } + +public constantTimeEq(left: Bytes, right: Bytes) (equal: Bool) pure { equal = dever.system.cryptoConstantTimeEq(left, right) } + +tokenValue(result: TokenResult.Ready(value)) (secret: Secret) { secret = value } +tokenValue(result: TokenResult.Failed(message)) (secret: Secret) { fail(TokenResult.Failed(message)) } + +hashValue(result: HashResult.Ready(value)) (hash: Text) { hash = value } +hashValue(result: HashResult.Failed(message)) (hash: Text) { fail(HashResult.Failed(message)) } + +verifyValue(result: VerifyResult.Ready(value)) (valid: Bool) { valid = value } +verifyValue(result: VerifyResult.Failed(message)) (valid: Bool) { fail(VerifyResult.Failed(message)) } diff --git a/lib/dever/database.dever b/lib/dever/database.dever new file mode 100644 index 0000000..9e5574e --- /dev/null +++ b/lib/dever/database.dever @@ -0,0 +1,12 @@ +public type Error { + error Pool(message: Text) + error PoolExhausted(message: Text) + error Connection(message: Text) + error Timeout(message: Text) + error Cancelled(message: Text) + error Database(message: Text) + error Constraint(message: Text) + error NotFound(message: Text) + error InvalidData(message: Text) + error Migration(message: Text) +} diff --git a/lib/dever/http/call.dever b/lib/dever/http/call.dever new file mode 100644 index 0000000..31fedac --- /dev/null +++ b/lib/dever/http/call.dever @@ -0,0 +1,29 @@ +public header(name: Text, value: Text) (result: Header) { +result = Header { name = text.lower(name), value = dever.bytes.fromText(value) } +} + +public headerValue(headers: List
, name: Text) (value: Bytes?) { +value = foundHeader(find(namedHeader, headers, text.lower(name))) +} + +namedHeader(header: Header, name: Text) (matches: Bool) { +matches = text.lower(header.name) == name +} + +foundHeader(header: null) (value: Bytes?) { value = null } +foundHeader(header: Header) (value: Bytes?) { value = header.value } + +public serve(route: handler(request: Request) (response: Response), listener: dever.system.Listener, limits: Limits) () { +dever.system.httpServe(route, listener, limits) +} + +public send(address: Text, port: Int, request: Request, limits: Limits) (response: Response) { +response = readResponse(dever.system.httpSend(address, port, request, limits)) +} + +public serveTls(route: handler(request: Request) (response: Response), listener: dever.system.Listener, limits: Limits, tls: dever.system.ServerTls) () { + dever.system.httpServeTls(route, listener, limits, tls) +} + +readResponse(result: ResponseResult.Read(response)) (value: Response) { value = response } +readResponse(result: ResponseResult.Failed(message)) (value: Response) { fail(ResponseResult.Failed(message)) } diff --git a/lib/dever/http/live.dever b/lib/dever/http/live.dever new file mode 100644 index 0000000..b7ed935 --- /dev/null +++ b/lib/dever/http/live.dever @@ -0,0 +1,39 @@ +public type LiveLimits { + chunk_bytes: Int + idle_ms: Int + heartbeat_ms: Int +} + +public defaultLiveLimits() (result: LiveLimits) { + result = LiveLimits { chunk_bytes = 65536, idle_ms = 60000, heartbeat_ms = 15000 } +} + +public serveLive(route: handler(request: Request, reply: dever.system.HttpReply) (), listener: dever.system.Listener, limits: Limits, live: LiveLimits) () { + dever.system.httpServeLive(route, listener, limits, live) +} + +public serveLiveTls(route: handler(request: Request, reply: dever.system.HttpReply) (), listener: dever.system.Listener, limits: Limits, live: LiveLimits, tls: dever.system.ServerTls) () { + dever.system.httpServeLiveTls(route, listener, limits, live, tls) +} + +public respond(reply: dever.system.HttpReply, response: Response) () { + wroteLive(dever.system.httpRespond(reply, response)) +} + +public start(reply: dever.system.HttpReply, status: Int, headers: List
) () { + wroteLive(dever.system.httpStart(reply, status, headers)) +} + +public write(reply: dever.system.HttpReply, bytes: Bytes) () { + wroteLive(dever.system.httpWrite(reply, bytes)) +} + +public finish(reply: dever.system.HttpReply) () { + closedLive(dever.system.httpFinish(reply)) +} + +wroteLive(result: dever.io.WriteResult.Done) () {} +wroteLive(result: dever.io.WriteResult.Failed(message)) () { fail(dever.io.WriteResult.Failed(message)) } + +closedLive(result: dever.io.CloseResult.Closed) () {} +closedLive(result: dever.io.CloseResult.Failed(message)) () { fail(dever.io.CloseResult.Failed(message)) } diff --git a/lib/dever/http/pool.dever b/lib/dever/http/pool.dever new file mode 100644 index 0000000..ce0b72b --- /dev/null +++ b/lib/dever/http/pool.dever @@ -0,0 +1,32 @@ +public type PoolLimits { + idle_ms: Int + chunk_bytes: Int + read_ms: Int +} + +public type ClientResult { + Ready(client: dever.system.HttpClient) + error Failed(message: Text) +} + +public defaultPoolLimits() (limits: PoolLimits) { + limits = PoolLimits { idle_ms = 10000, chunk_bytes = 65536, read_ms = 60000 } +} + +public client(origin: Text, tls: dever.system.ClientTls, limits: Limits, pool: PoolLimits) (client: dever.system.HttpClient) { + client = readClient(dever.system.httpClient(origin, tls, limits, pool)) +} + +public request(client: dever.system.HttpClient, request: Request) (response: Response) { + response = readResponse(dever.system.httpRequest(client, request)) +} + +public closeClient(client: dever.system.HttpClient) () { + closedClient(dever.system.httpCloseClient(client)) +} + +readClient(result: ClientResult.Ready(value)) (client: dever.system.HttpClient) { client = value } +readClient(result: ClientResult.Failed(message)) (client: dever.system.HttpClient) { fail(ClientResult.Failed(message)) } + +closedClient(result: dever.io.CloseResult.Closed) () {} +closedClient(result: dever.io.CloseResult.Failed(message)) () { fail(dever.io.CloseResult.Failed(message)) } diff --git a/lib/dever/http/stream.dever b/lib/dever/http/stream.dever new file mode 100644 index 0000000..a0b879c --- /dev/null +++ b/lib/dever/http/stream.dever @@ -0,0 +1,28 @@ +public type StreamRequest { + method: Text + target: Text + headers: List
+ body: AsyncStream +} + +public type StreamResponse { + status: Int + headers: List
+ body: AsyncStream +} + +public type StreamResponseResult { + Read(response: StreamResponse) + error Failed(message: Text) +} + +public open(client: dever.system.HttpClient, request: Request) (response: StreamResponse) { + response = readStreamResponse(dever.system.httpOpen(client, request)) +} + +public openStream(client: dever.system.HttpClient, request: StreamRequest) (response: StreamResponse) { + response = readStreamResponse(dever.system.httpOpenStream(client, request)) +} + +readStreamResponse(result: StreamResponseResult.Read(value)) (response: StreamResponse) { response = value } +readStreamResponse(result: StreamResponseResult.Failed(message)) (response: StreamResponse) { fail(StreamResponseResult.Failed(message)) } diff --git a/lib/dever/http/value.dever b/lib/dever/http/value.dever new file mode 100644 index 0000000..40d501b --- /dev/null +++ b/lib/dever/http/value.dever @@ -0,0 +1,44 @@ +public type Header { +name: Text +value: Bytes +} + +public type Request { +method: Text +target: Text +headers: List
+body: Bytes +} + +public type Response { +status: Int +headers: List
+body: Bytes +} + +public type Limits { +header_bytes: Int +body_bytes: Int +timeout_ms: Int +connections: Int +http2: Http2Limits? +} + +public type Http2Limits { +streams: Int +stream_window_bytes: Int +connection_window_bytes: Int +} + +public type ResponseResult { + Read(response: Response) + error Failed(message: Text) +} + +public defaultLimits() (result: Limits) { +result = Limits { header_bytes = 8192, body_bytes = 1048576, timeout_ms = 5000, connections = 32, http2 = null } +} + +public defaultHttp2Limits() (result: Http2Limits) { +result = Http2Limits { streams = 16, stream_window_bytes = 65535, connection_window_bytes = 262144 } +} diff --git a/lib/dever/id.dever b/lib/dever/id.dever new file mode 100644 index 0000000..576d77c --- /dev/null +++ b/lib/dever/id.dever @@ -0,0 +1,7 @@ +public fromText(text: Text) (id: Id) { + id = dever.system.idFromText(text) +} + +public toText(id: Id) (text: Text) { + text = dever.system.idToText(id) +} diff --git a/lib/dever/io.dever b/lib/dever/io.dever new file mode 100644 index 0000000..d1a07bd --- /dev/null +++ b/lib/dever/io.dever @@ -0,0 +1,102 @@ +public type OpenResult { + Opened(file: dever.system.File) + error Failed(message: Text) +} + +public type ReadState { + Read(bytes: Bytes) + End +} + +public type ReadResult { + Done(state: ReadState) + error Failed(message: Text) +} + +type SystemReadResult { + Read(bytes: Bytes) + End + error Failed(message: Text) +} + +public type WriteResult { + Done + error Failed(message: Text) +} + +public type CloseResult { + Closed + error Failed(message: Text) +} + +public type ReadEvent { + Chunk(bytes: Bytes) + error Failed(message: Text) +} + +public type ReadStreamResult { + Streaming(stream: Stream) + error Failed(message: Text) +} + +public println(text: Text) () { + dever.system.stdoutWrite(text + "\n") +} + +public open(path: Text) (file: dever.system.File) { + file = opened(dever.system.fileOpen(path)) +} + +public create(path: Text) (file: dever.system.File) { + file = opened(dever.system.fileCreate(path)) +} + +opened(result: OpenResult.Opened(file)) (value: dever.system.File) { + value = file +} +opened(result: OpenResult.Failed(message)) (value: dever.system.File) { + fail(OpenResult.Failed(message)) +} + +public read(file: dever.system.File, limit: Int) (state: ReadState) { + state = readState(dever.system.fileRead(file, limit)) +} + +readState(result: SystemReadResult.Read(bytes)) (state: ReadState) { + state = ReadState.Read(bytes) +} +readState(result: SystemReadResult.End) (state: ReadState) { + state = ReadState.End +} +readState(result: SystemReadResult.Failed(message)) (state: ReadState) { + fail(ReadResult.Failed(message)) +} + +public write(file: dever.system.File, bytes: Bytes) () { + wrote(dever.system.fileWrite(file, bytes)) +} + +wrote(result: WriteResult.Done) () {} +wrote(result: WriteResult.Failed(message)) () { + fail(WriteResult.Failed(message)) +} + +public close(file: dever.system.File) () { + closed(dever.system.fileClose(file)) +} + +closed(result: CloseResult.Closed) () {} +closed(result: CloseResult.Failed(message)) () { + fail(CloseResult.Failed(message)) +} + +public chunks(file: dever.system.File, limit: Int) (stream: Stream) { + stream = readStream(dever.system.fileChunks(file, limit)) +} + +readStream(result: ReadStreamResult.Streaming(stream)) (value: Stream) { + value = stream +} +readStream(result: ReadStreamResult.Failed(message)) (value: Stream) { + fail(ReadStreamResult.Failed(message)) +} diff --git a/lib/dever/json.dever b/lib/dever/json.dever new file mode 100644 index 0000000..adaaf61 --- /dev/null +++ b/lib/dever/json.dever @@ -0,0 +1,97 @@ +public type JsonResult { + Parsed(value: Json) + error Failed(message: Text) +} + +public type TextResult { + Read(value: Text) + error Failed(message: Text) +} + +public type IntResult { + Read(value: Int) + error Failed(message: Text) +} + +public fromText(source: Text) (value: Json) { + value = parsed(dever.system.jsonFromText(source)) +} + +public toText(value: Json) (source: Text) { + source = dever.system.jsonToText(value) +} + +public setProperty(value: Json, name: Text, child: Json) (result: Json) { + result = parsed(dever.system.jsonSetProperty(value, name, child)) +} + +public propertyValue(value: Json, name: Text) (result: Json?) { + result = propertyResult(dever.system.jsonProperty(value, name)) +} + +public asText(value: Json) (source: Text) { + source = textValue(dever.system.jsonAsText(value)) +} + +public asInteger(value: Json) (number: Int) { + number = intValue(dever.system.jsonAsInteger(value)) +} + +public encode(value: JsonResult) (result: Json) { + result = parsed(value) +} + +parsed(result: JsonResult.Parsed(value)) (json: Json) { json = value } +parsed(result: JsonResult.Failed(message)) (json: Json) { fail(JsonResult.Failed(message)) } + +public parse(source: Text) (document: dever.json.value.Document) { + document = parsedDocument(dever.system.jsonParse(source)) +} + +public stringify(document: dever.json.value.Document) (text: Text) { + text = written(dever.system.jsonStringify(document)) +} + +public node(document: dever.json.value.Document, id: Int) (value: dever.json.value.Value?) { + value = dever.system.jsonNode(document, id) +} + +public property(document: dever.json.value.Document, id: Int, name: Text) (value: Int?) { + value = dever.system.jsonDocumentProperty(document, id, name) +} + +public element(document: dever.json.value.Document, id: Int, index: Int) (value: Int?) { + value = dever.system.jsonElement(document, id, index) +} + +parsedDocument(result: dever.json.value.ParseResult.Parsed(document)) (value: dever.json.value.Document) { value = document } +parsedDocument(result: dever.json.value.ParseResult.Failed(position, message)) (value: dever.json.value.Document) { fail(dever.json.value.ParseResult.Failed(position, message)) } + +written(result: dever.json.value.WriteResult.Written(text)) (value: Text) { value = text } +written(result: dever.json.value.WriteResult.Failed(node, message)) (value: Text) { fail(dever.json.value.WriteResult.Failed(node, message)) } + +textValue(result: TextResult.Read(value)) (text: Text) { text = value } +textValue(result: TextResult.Failed(message)) (text: Text) { fail(TextResult.Failed(message)) } + +intValue(result: IntResult.Read(value)) (number: Int) { number = value } +intValue(result: IntResult.Failed(message)) (number: Int) { fail(IntResult.Failed(message)) } + +public type PropertyResult { + Found(value: Json?) + error Failed(message: Text) +} + +propertyResult(result: PropertyResult.Found(child)) (value: Json?) { value = child } +propertyResult(result: PropertyResult.Failed(message)) (value: Json?) { fail(PropertyResult.Failed(message)) } + +public object(fields: Map) (value: Json) { value = parsed(dever.system.jsonObject(fields)) } + +public array(values: List) (value: Json) { value = dever.json.encode(values) } + +public string(source: Text) (value: Json) { value = dever.json.encode(source) } + +public integer(source: Int) (value: Json) { value = dever.json.encode(source) } + +public boolean(source: Bool) (value: Json) { value = dever.json.encode(source) } + +public nullValue() (value: Json) { value = fromText("null") } diff --git a/lib/dever/json/value.dever b/lib/dever/json/value.dever new file mode 100644 index 0000000..a3775f0 --- /dev/null +++ b/lib/dever/json/value.dever @@ -0,0 +1,23 @@ +public type Value { + Null + Boolean(value: Bool) + Number(raw: Text) + String(text: Text) + Array(children: List) + Object(members: Map) +} + +public type Document { + root: Int + nodes: List +} + +public type ParseResult { + Parsed(document: Document) + error Failed(position: Int, message: Text) +} + +public type WriteResult { + Written(text: Text) + error Failed(node: Int, message: Text) +} diff --git a/lib/dever/net/listener.dever b/lib/dever/net/listener.dever new file mode 100644 index 0000000..bbc3d6d --- /dev/null +++ b/lib/dever/net/listener.dever @@ -0,0 +1,35 @@ +public type ListenResult { + Listening(listener: dever.system.Listener) + error Failed(message: Text) +} + +public type PortResult { + Bound(port: Int) + error Failed(message: Text) +} + +public listen(host: Text, port: Int) (listener: dever.system.Listener) { + listener = listening(dever.system.tcpListen(host, port)) +} + +listening(result: ListenResult.Listening(listener)) (value: dever.system.Listener) { value = listener } +listening(result: ListenResult.Failed(message)) (value: dever.system.Listener) { fail(ListenResult.Failed(message)) } + +public accept(listener: dever.system.Listener) (socket: dever.system.Socket) { + socket = connected(dever.system.tcpAccept(listener)) +} + +public port(listener: dever.system.Listener) (port: Int) { + port = bound(dever.system.tcpPort(listener)) +} + +bound(result: PortResult.Bound(port)) (value: Int) { value = port } +bound(result: PortResult.Failed(message)) (value: Int) { fail(PortResult.Failed(message)) } + +public closeListener(listener: dever.system.Listener) () { + closed(dever.system.tcpCloseListener(listener)) +} + +public connections(listener: dever.system.Listener) (result: AsyncStream) { + result = dever.system.tcpConnections(listener) +} diff --git a/lib/dever/net/socket.dever b/lib/dever/net/socket.dever new file mode 100644 index 0000000..3f45dbf --- /dev/null +++ b/lib/dever/net/socket.dever @@ -0,0 +1,59 @@ +public type ConnectResult { + Connected(socket: dever.system.Socket) + error Failed(message: Text) +} + +public type ReadStreamResult { + Streaming(stream: AsyncStream) + error Failed(message: Text) +} + +type SystemReadResult { + Read(bytes: Bytes) + End + error Failed(message: Text) +} + +public connect(host: Text, port: Int) (socket: dever.system.Socket) { + socket = connected(dever.system.tcpConnect(host, port)) +} + +public connectTimeout(address: Text, port: Int, milliseconds: Int) (socket: dever.system.Socket) { + socket = connected(dever.system.tcpConnectTimeout(address, port, milliseconds)) +} + +connected(result: ConnectResult.Connected(socket)) (value: dever.system.Socket) { value = socket } +connected(result: ConnectResult.Failed(message)) (value: dever.system.Socket) { fail(ConnectResult.Failed(message)) } + +public read(socket: dever.system.Socket, limit: Int) (state: dever.io.ReadState) { + state = readState(dever.system.tcpRead(socket, limit)) +} + +readState(result: SystemReadResult.Read(bytes)) (state: dever.io.ReadState) { state = dever.io.ReadState.Read(bytes) } +readState(result: SystemReadResult.End) (state: dever.io.ReadState) { state = dever.io.ReadState.End } +readState(result: SystemReadResult.Failed(message)) (state: dever.io.ReadState) { fail(dever.io.ReadResult.Failed(message)) } + +public write(socket: dever.system.Socket, bytes: Bytes) () { + wrote(dever.system.tcpWrite(socket, bytes)) +} + +public timeout(socket: dever.system.Socket, milliseconds: Int) () { + wrote(dever.system.tcpTimeout(socket, milliseconds)) +} + +wrote(result: dever.io.WriteResult.Done) () {} +wrote(result: dever.io.WriteResult.Failed(message)) () { fail(dever.io.WriteResult.Failed(message)) } + +public close(socket: dever.system.Socket) () { + closed(dever.system.tcpClose(socket)) +} + +closed(result: dever.io.CloseResult.Closed) () {} +closed(result: dever.io.CloseResult.Failed(message)) () { fail(dever.io.CloseResult.Failed(message)) } + +public chunks(socket: dever.system.Socket, limit: Int) (stream: AsyncStream) { + stream = readStream(dever.system.tcpChunks(socket, limit)) +} + +readStream(result: ReadStreamResult.Streaming(stream)) (value: AsyncStream) { value = stream } +readStream(result: ReadStreamResult.Failed(message)) (value: AsyncStream) { fail(ReadStreamResult.Failed(message)) } diff --git a/lib/dever/process.dever b/lib/dever/process.dever new file mode 100644 index 0000000..27e4c39 --- /dev/null +++ b/lib/dever/process.dever @@ -0,0 +1,26 @@ +public type Output { + code: Int + stdout: Bytes + stderr: Bytes +} + +public type Error { + error Failed(message: Text) +} + +public type ArgumentsResult { + Read(values: List) + error Failed(message: Text) +} + +public arguments() (values: List) { + values = argumentsValue(dever.system.processArguments()) +} + +argumentsValue(result: ArgumentsResult.Read(values)) (value: List) { + value = values +} + +argumentsValue(result: ArgumentsResult.Failed(message)) (value: List) { + fail(ArgumentsResult.Failed(message)) +} diff --git a/lib/dever/site.dever b/lib/dever/site.dever new file mode 100644 index 0000000..63f5f17 --- /dev/null +++ b/lib/dever/site.dever @@ -0,0 +1 @@ +public key() (value: Text) { value = dever.system.siteKey() } diff --git a/lib/dever/sse.dever b/lib/dever/sse.dever new file mode 100644 index 0000000..4b93b62 --- /dev/null +++ b/lib/dever/sse.dever @@ -0,0 +1,17 @@ +public type Event { + event: Text + data: Text + id: Text? + retry_ms: Int? +} + +public start(reply: dever.system.HttpReply, headers: List) () { + wrote(dever.system.sseStart(reply, headers)) +} + +public send(reply: dever.system.HttpReply, event: Event) () { + wrote(dever.system.sseSend(reply, event)) +} + +wrote(result: dever.io.WriteResult.Done) () {} +wrote(result: dever.io.WriteResult.Failed(message)) () { fail(dever.io.WriteResult.Failed(message)) } diff --git a/lib/dever/storage.dever b/lib/dever/storage.dever new file mode 100644 index 0000000..110a8e8 --- /dev/null +++ b/lib/dever/storage.dever @@ -0,0 +1,11 @@ +public type PutResult { + Stored(key: Uuid) + error Failed(message: Text) +} + +public put(upload: Upload) (key: Uuid) { + key = stored(dever.system.uploadPut(upload)) +} + +stored(result: PutResult.Stored(value)) (key: Uuid) { key = value } +stored(result: PutResult.Failed(message)) (key: Uuid) { fail(PutResult.Failed(message)) } diff --git a/lib/dever/task.dever b/lib/dever/task.dever new file mode 100644 index 0000000..930fc5f --- /dev/null +++ b/lib/dever/task.dever @@ -0,0 +1,12 @@ +public sleep(millis: Int) () { + slept(dever.system.taskSleep(millis)) +} + +slept(result: dever.time.SleepResult.Done) () {} +slept(result: dever.time.SleepResult.Failed(message)) () { + fail(dever.time.SleepResult.Failed(message)) +} + +public ticks(millis: Int) (values: AsyncStream) { + values = dever.system.taskTicks(millis) +} diff --git a/lib/dever/time.dever b/lib/dever/time.dever new file mode 100644 index 0000000..cde034a --- /dev/null +++ b/lib/dever/time.dever @@ -0,0 +1,13 @@ +public type SleepResult { + Done + error Failed(message: Text) +} + +public sleep(millis: Int) () { + slept(dever.system.timeSleep(millis)) +} + +slept(result: SleepResult.Done) () {} +slept(result: SleepResult.Failed(message)) () { + fail(SleepResult.Failed(message)) +} diff --git a/lib/dever/time/calendar.dever b/lib/dever/time/calendar.dever new file mode 100644 index 0000000..eb936c6 --- /dev/null +++ b/lib/dever/time/calendar.dever @@ -0,0 +1,29 @@ +public type DateTimeResult { Read(value: DateTime) + error Failed(message: Text) } + +datetimeValue(result: DateTimeResult.Read(value)) (output: DateTime) pure { output = value } +datetimeValue(result: DateTimeResult.Failed(message)) (output: DateTime) pure { fail(DateTimeResult.Failed(message)) } + +public type DateResult { Read(value: Date) + error Failed(message: Text) } + +dateValue(result: DateResult.Read(value)) (output: Date) pure { output = value } +dateValue(result: DateResult.Failed(message)) (output: Date) pure { fail(DateResult.Failed(message)) } + +public type TimeResult { Read(value: Time) + error Failed(message: Text) } + +timeValue(result: TimeResult.Read(value)) (output: Time) pure { output = value } +timeValue(result: TimeResult.Failed(message)) (output: Time) pure { fail(TimeResult.Failed(message)) } + +public type DurationResult { Read(value: Duration) + error Failed(message: Text) } + +durationValue(result: DurationResult.Read(value)) (output: Duration) pure { output = value } +durationValue(result: DurationResult.Failed(message)) (output: Duration) pure { fail(DurationResult.Failed(message)) } + +public type TextResult { Read(value: Text) + error Failed(message: Text) } + +textValue(result: TextResult.Read(value)) (output: Text) pure { output = value } +textValue(result: TextResult.Failed(message)) (output: Text) pure { fail(TextResult.Failed(message)) } diff --git a/lib/dever/time/clock.dever b/lib/dever/time/clock.dever new file mode 100644 index 0000000..240c824 --- /dev/null +++ b/lib/dever/time/clock.dever @@ -0,0 +1,9 @@ +public type ClockResult { Read(value: Int) + error Failed(message: Text) } + +public unixMillis() (value: Int) { value = clockValue(dever.system.timeUnixMillis()) } + +public monotonicNanos() (value: Int) { value = clockValue(dever.system.timeMonotonicNanos()) } + +clockValue(result: ClockResult.Read(value)) (clock: Int) { clock = value } +clockValue(result: ClockResult.Failed(message)) (clock: Int) { fail(ClockResult.Failed(message)) } diff --git a/lib/dever/time/operations.dever b/lib/dever/time/operations.dever new file mode 100644 index 0000000..b4666d3 --- /dev/null +++ b/lib/dever/time/operations.dever @@ -0,0 +1,21 @@ +public now() (output: DateTime) { output = datetimeValue(dever.system.timeNow()) } + +public parseDatetime(text: Text) (output: DateTime) pure { output = datetimeValue(dever.system.timeParseDatetime(text)) } + +public formatDatetime(value: DateTime) (output: Text) pure { output = textValue(dever.system.timeFormatDatetime(value)) } + +public parseDate(text: Text) (output: Date) pure { output = dateValue(dever.system.timeParseDate(text)) } + +public formatDate(value: Date) (output: Text) pure { output = textValue(dever.system.timeFormatDate(value)) } + +public parseTime(text: Text) (output: Time) pure { output = timeValue(dever.system.timeParseTime(text)) } + +public formatTime(value: Time) (output: Text) pure { output = textValue(dever.system.timeFormatTime(value)) } + +public duration(millis: Int) (output: Duration) pure { output = dever.system.timeDuration(millis) } + +public add(value: DateTime, duration: Duration) (output: DateTime) pure { output = datetimeValue(dever.system.timeAdd(value, duration)) } + +public subtract(value: DateTime, duration: Duration) (output: DateTime) pure { output = datetimeValue(dever.system.timeSubtract(value, duration)) } + +public difference(later: DateTime, earlier: DateTime) (output: Duration) pure { output = durationValue(dever.system.timeDifference(later, earlier)) } diff --git a/lib/dever/tls.dever b/lib/dever/tls.dever new file mode 100644 index 0000000..664d8c3 --- /dev/null +++ b/lib/dever/tls.dever @@ -0,0 +1,27 @@ +public type ClientResult { + Ready(config: dever.system.ClientTls) + error Failed(message: Text) +} + +public type ServerResult { + Ready(config: dever.system.ServerTls) + error Failed(message: Text) +} + +public system() (config: dever.system.ClientTls) { + config = dever.system.tlsSystem() +} + +public client(ca_pem: Bytes) (config: dever.system.ClientTls) { + config = clientConfig(dever.system.tlsClient(ca_pem)) +} + +clientConfig(result: ClientResult.Ready(config)) (value: dever.system.ClientTls) { value = config } +clientConfig(result: ClientResult.Failed(message)) (value: dever.system.ClientTls) { fail(ClientResult.Failed(message)) } + +public server(cert_pem: Bytes, key_pem: Bytes) (config: dever.system.ServerTls) { + config = serverConfig(dever.system.tlsServer(cert_pem, key_pem)) +} + +serverConfig(result: ServerResult.Ready(config)) (value: dever.system.ServerTls) { value = config } +serverConfig(result: ServerResult.Failed(message)) (value: dever.system.ServerTls) { fail(ServerResult.Failed(message)) } diff --git a/lib/dever/websocket.dever b/lib/dever/websocket.dever new file mode 100644 index 0000000..8493ec3 --- /dev/null +++ b/lib/dever/websocket.dever @@ -0,0 +1,106 @@ +public type Limits { + message_bytes: Int + idle_ms: Int + write_ms: Int +} + +public type Message { + Text(value: Text) + Binary(value: Bytes) + Ping(value: Bytes) + Pong(value: Bytes) +} + +public type ConnectResult { + Connected(socket: dever.system.WebSocket) + error Failed(message: Text) +} + +public type ReceiveState { + Read(message: Message) + End +} + +public type ReceiveResult { + Done(state: ReceiveState) + error Failed(message: Text) +} + +type SystemReceiveResult { + Read(message: Message) + End + error Failed(message: Text) +} + +public type MessageEvent { + Read(message: Message) + error Failed(message: Text) +} + +public defaultLimits() (result: Limits) { + result = Limits { + message_bytes = 1048576 + idle_ms = 60000 + write_ms = 5000 + } +} + +public accept(reply: dever.system.HttpReply, limits: Limits) (socket: dever.system.WebSocket) { + socket = connected(dever.system.wsAccept(reply, limits)) +} + +public connect(address: Text, port: Int, target: Text, limits: Limits) (socket: dever.system.WebSocket) { + socket = connected(dever.system.wsConnect(address, port, target, limits)) +} + +public open(url: Text, tls: dever.system.ClientTls, limits: Limits) (socket: dever.system.WebSocket) { + socket = connected(dever.system.wsOpen(url, tls, limits)) +} + +connected(result: ConnectResult.Connected(socket)) (value: dever.system.WebSocket) { + value = socket +} + +connected(result: ConnectResult.Failed(message)) (value: dever.system.WebSocket) { + fail(ConnectResult.Failed(message)) +} + +public send(socket: dever.system.WebSocket, message: Message) () { + wrote(dever.system.wsSend(socket, message)) +} + +wrote(result: dever.io.WriteResult.Done) () {} + +wrote(result: dever.io.WriteResult.Failed(message)) () { + fail(dever.io.WriteResult.Failed(message)) +} + +public receive(socket: dever.system.WebSocket) (state: ReceiveState) { + state = receiveState(dever.system.wsReceive(socket)) +} + +receiveState(result: SystemReceiveResult.Read(message)) (state: ReceiveState) { + state = ReceiveState.Read(message) +} + +receiveState(result: SystemReceiveResult.End) (state: ReceiveState) { + state = ReceiveState.End +} + +receiveState(result: SystemReceiveResult.Failed(message)) (state: ReceiveState) { + fail(ReceiveResult.Failed(message)) +} + +public messages(socket: dever.system.WebSocket) (result: AsyncStream) { + result = dever.system.wsMessages(socket) +} + +public close(socket: dever.system.WebSocket, code: Int, reason: Text) () { + closed(dever.system.wsClose(socket, code, reason)) +} + +closed(result: dever.io.CloseResult.Closed) () {} + +closed(result: dever.io.CloseResult.Failed(message)) () { + fail(dever.io.CloseResult.Failed(message)) +} diff --git a/lib/doc/decimal.md b/lib/doc/decimal.md new file mode 100644 index 0000000..cc085f1 --- /dev/null +++ b/lib/doc/decimal.md @@ -0,0 +1,19 @@ +# 十进制 + +decimal128 数值的解析、格式化和舍入。 + +## decimal.round + +就近舍入,中点取偶数;places 为负时舍入到十、百等位。精度或结果无法表示时产生数值故障。 + +## decimal.parse + +解析有限且精确可表示的 Decimal128;格式非法或需要舍入时返回 null。 + +## decimal.toText + +输出规范化 Decimal 文本,去除多余尾零,零统一写为 0。 + +## decimal.fromInt + +把 Int 精确转换为 Decimal。 diff --git a/lib/doc/dever-api-cookie.md b/lib/doc/dever-api-cookie.md new file mode 100644 index 0000000..932c081 --- /dev/null +++ b/lib/doc/dever-api-cookie.md @@ -0,0 +1,44 @@ +# Cookie + +Cookie 写入会检查外部 Origin;Secret Cookie 使用带版本的字节编码。 + +## dever.api.SameSite + +限定浏览器跨站发送 Cookie 的策略。 + +分支: +- `Strict`:仅同站发送 +- `Lax`:同站请求和安全的顶层导航 +- `None`:允许跨站,必须同时启用 Secure + +## dever.api.CookieOptions + +属性控制浏览器发送范围,不用于服务端授权。 + +字段: +- `path`:路径 +- `domain`:可选域 +- `same_site`:浏览器跨站发送策略 +- `secure`:只通过 HTTPS 发送 +- `http_only`:禁止脚本读取 +- `max_age`:可选秒数,零表示删除 + +## dever.api.secureCookieOptions + +默认使用路径 /、SameSite=Lax、Secure 和 HttpOnly,不指定域和有效期。 + +## dever.api.cookie + +读取普通文本 Cookie,认证 Cookie 或 Secret 编码不能读为 Text。 + +## dever.api.secretCookie + +读取带 v1 前缀的无填充 base64url 字节;其他格式视为缺席。 + +## dever.api.setCookie + +暂存普通 Cookie,业务或编码失败时不发布。 + +## dever.api.setSecretCookie + +秘密 Cookie 必须保留 Secure 和 HttpOnly。 diff --git a/lib/doc/dever-api-request.md b/lib/doc/dever-api-request.md new file mode 100644 index 0000000..daa8592 --- /dev/null +++ b/lib/doc/dever-api-request.md @@ -0,0 +1,27 @@ +# 请求信息 + +读取当前 HTTP 请求的信息,客户端地址不信任转发头。 + +## dever.api.requestId + +读取服务端为当前请求生成的标识。 + +## dever.api.method + +读取实际请求方法。 + +## dever.api.path + +读取不含查询参数的路径。 + +## dever.api.clientAddress + +读取直接连接的客户端 IP,不采用 Forwarded 或 X-Forwarded-For。 + +## dever.api.header + +读取首个非敏感请求头,缺席时返回 null。 + +## dever.api.setHeader + +成功响应时发布,不能覆盖协议和 Cookie 字段。 diff --git a/lib/doc/dever-api.md b/lib/doc/dever-api.md new file mode 100644 index 0000000..69549cc --- /dev/null +++ b/lib/doc/dever-api.md @@ -0,0 +1,19 @@ +# API 错误 + +Error 映射为固定 HTTP 状态;内部故障和敏感消息不进入响应。 + +## dever.api.require + +条件为 true 时继续,false 时传播指定的 Error。用于已有业务条件的检查,不替代身份认证、对象权限、数据库约束或状态分支。不要在各领域重复定义仅负责 true/false 失败的一对函数。 + +## dever.api.Error + +业务可以显式返回安全错误;其他失败统一返回内部错误。 + +分支: +- `error Invalid`:请求无效 +- `error Unauthorized`:未认证 +- `error Forbidden`:无权访问 +- `error NotFound`:资源不存在 +- `error Conflict`:资源冲突 +- `error TooManyRequests`:请求过多 diff --git a/lib/doc/dever-auth-capability.md b/lib/doc/dever-auth-capability.md new file mode 100644 index 0000000..dffab52 --- /dev/null +++ b/lib/doc/dever-auth-capability.md @@ -0,0 +1,15 @@ +# 业务权限 + +业务权限属于当前认证租户和入口命名空间。注册与撤销使用当前写事务,授权数据保存在租户库;缺少租户或组件未启用时失败。 + +## dever.auth.hasProtectedRoles + +检查指定用户在当前可信租户任意站点是否拥有受保护角色;缺少可信租户时失败。 + +## dever.auth.registerCapability + +注册当前入口命名空间内的业务权限,重复注册返回同一键。只允许受保护写 API,显示名称和层级由业务维护。 + +## dever.auth.removeCapability + +在同一事务内撤销当前入口命名空间中的业务权限及其角色授权,重新注册不会恢复旧授权。 diff --git a/lib/doc/dever-auth-roles.md b/lib/doc/dever-auth-roles.md new file mode 100644 index 0000000..15efd0d --- /dev/null +++ b/lib/doc/dever-auth-roles.md @@ -0,0 +1,59 @@ +# 角色与权限 + +角色操作作用于当前认证租户和站点,不能用参数切换授权库。 + +## dever.auth.Permission + +一个接口或业务权限的名称、所属组件、领域、站点及操作信息。 + +## dever.auth.Role + +角色及其权限设置。protected 标识保留角色,permission_keys 是分配的权限键。 + +## dever.auth.permissions + +读取当前站点的有效权限目录。 + +## dever.auth.currentPermissions + +读取当前用户启用角色的有效权限并集。 + +## dever.auth.roles + +读取当前站点角色,包含禁用与受保护角色。 + +## dever.auth.role + +读取当前站点单个角色,不存在时返回 null。 + +## dever.auth.userRoles + +读取当前站点指定用户的角色。 + +## dever.auth.isOwner + +根据受保护角色判断 Owner,不依赖特殊用户编号。 + +## dever.auth.saveRole + +原子保存角色与权限,仅允许受保护写 API。 + +## dever.auth.grantRole + +授予普通角色,保留 Owner 由运维入口建立。 + +## dever.auth.revokeRole + +撤销普通角色。 + +## dever.auth.disableRole + +禁用普通角色,保留关系用于管理回显。 + +## dever.auth.deleteRole + +原子删除普通角色及授权关系。 + +## dever.auth.owner + +只检查受保护标记,角色是否启用不改变保留身份。 diff --git a/lib/doc/dever-auth-session.md b/lib/doc/dever-auth-session.md new file mode 100644 index 0000000..892b4b1 --- /dev/null +++ b/lib/doc/dever-auth-session.md @@ -0,0 +1,31 @@ +# 当前身份 + +读取已认证 HTTP 请求的身份。CMD、后台和并发子任务不能借用请求上下文。 + +## dever.auth.id + +读取 verify 建立的可信身份标识。 + +## dever.auth.session + +读取已验证的当前会话标识。 + +## dever.auth.userId + +读取当前认证用户的 Model ID;类型由 Provider 的 verify 输出确定。 + +## dever.auth.tenantId + +读取当前租户的 Model ID;类型由 Provider 的 verify 输出确定。 + +## dever.auth.issue + +签发当前站点会话,结果保持不透明 Secret。 + +## dever.auth.issueCookie + +检查同源后暂存 Cookie,入口提交并编码成功才发布。 + +## dever.auth.clearCookie + +检查同源后暂存清除 Cookie,失败响应不发布。 diff --git a/lib/doc/dever-auth-values.md b/lib/doc/dever-auth-values.md new file mode 100644 index 0000000..1721e9e --- /dev/null +++ b/lib/doc/dever-auth-values.md @@ -0,0 +1,13 @@ +# 身份声明 + +签名有效的 Claims 仍须由配置的 App verify 校验账号和会话。 + +## dever.auth.Claims + +传给 Provider verify 的认证声明。subject 是认证主体,session 是会话标识;site 由当前 HTTP 入口确定。 + +字段: +- `subject`:签发对象 +- `session`:会话标识 +- `tenant`:租户声明 +- `site`:当前站点 diff --git a/lib/doc/dever-bytes.md b/lib/doc/dever-bytes.md new file mode 100644 index 0000000..c191cf6 --- /dev/null +++ b/lib/doc/dever-bytes.md @@ -0,0 +1,55 @@ +# 字节 + +Bytes 保存原始字节,按 UTF-8 与 Text 显式转换。 + +## dever.bytes.DecodeResult + +UTF-8 解码结果:Decoded 保存文本,Failed 保存错误原因。 + +分支: +- `Decoded(text: Text)`:成功结果 +- `error Failed(message: Text)`:失败原因 + +## dever.bytes.BufferResult + +字节构造结果:Ready 保存字节,Failed 保存错误原因。 + +分支: +- `Ready(bytes: Bytes)`:成功结果 +- `error Failed(message: Text)`:失败原因 + +## dever.bytes.fromText + +按 UTF-8 编码复制文本字节。 + +## dever.bytes.toText + +完整校验 UTF-8;无效字节不会替换或忽略。 + +## dever.bytes.decoded + +成功时返回文本,失败时保持原错误身份。 + +## dever.bytes.fromInts + +按顺序转换 0–255 的 Int;超范围失败。 + +## dever.bytes.length + +返回字节数。 + +## dever.bytes.at + +使用从零开始的索引,越界返回 null。 + +## dever.bytes.slice + +截取左闭右开区间,负数、倒置或越界范围失败。 + +## dever.bytes.ready + +成功时返回字节,失败时保持原错误身份。 + +## dever.bytes.concat + +顺序连接两段字节,保留两个输入的值。 diff --git a/lib/doc/dever-crypto.md b/lib/doc/dever-crypto.md new file mode 100644 index 0000000..66acff8 --- /dev/null +++ b/lib/doc/dever-crypto.md @@ -0,0 +1,63 @@ +# 密码与摘要 + +密码哈希和校验会阻塞,异步流程用 blocking 调用;摘要和 HMAC 是纯计算。Secret 不可打印。 + +## dever.crypto.TokenResult + +随机 Secret 的生成结果,失败包含原因。 + +分支: +- `Ready(value: Secret)`:成功值 +- `error Failed(message: Text)`:失败原因 + +## dever.crypto.HashResult + +密码哈希的生成结果,成功包含 PHC 文本。 + +分支: +- `Ready(value: Text)`:成功值 +- `error Failed(message: Text)`:失败原因 + +## dever.crypto.VerifyResult + +密码校验结果;成功值表示密码是否匹配,失败表示哈希无效或校验无法完成。 + +分支: +- `Ready(value: Bool)`:成功值 +- `error Failed(message: Text)`:失败原因 + +## dever.crypto.token + +生成指定字节数的随机 Secret,允许 16–1024 字节。随机源不可用时失败。 + +## dever.crypto.passwordHash + +用 Argon2id v19 生成密码哈希。参数为 19456 KiB、2 轮、1 lane 和 32 字节结果。 + +## dever.crypto.passwordVerify + +校验密码与 PHC 哈希。密码不匹配返回 false;哈希损坏或资源参数超限时失败。 + +## dever.crypto.sha256 + +计算 SHA-256,返回 32 字节摘要。 + +## dever.crypto.hmacSha256 + +用 Secret 密钥计算 HMAC-SHA256,返回 32 字节结果。 + +## dever.crypto.constantTimeEq + +对相同长度的内容使用常量时间比较,长度不同时返回 false。 + +## dever.crypto.tokenValue + +取出随机 Secret;生成失败时继续传播错误。 + +## dever.crypto.hashValue + +取出密码哈希;生成失败时继续传播错误。 + +## dever.crypto.verifyValue + +取出密码是否匹配的结果;校验失败时继续传播错误。 diff --git a/lib/doc/dever-database.md b/lib/doc/dever-database.md new file mode 100644 index 0000000..6c9cdb4 --- /dev/null +++ b/lib/doc/dever-database.md @@ -0,0 +1,19 @@ +# 数据库错误 + +数据库错误不暴露 SQL、绑定值或连接凭据。 + +## dever.database.Error + +数据库的连接、约束、超时和解码错误。 + +分支: +- `error Pool(message: Text)`:连接池不可用 +- `error PoolExhausted(message: Text)`:等待连接超过期限 +- `error Connection(message: Text)`:连接失败 +- `error Timeout(message: Text)`:操作超时 +- `error Cancelled(message: Text)`:操作取消 +- `error Database(message: Text)`:数据库执行失败 +- `error Constraint(message: Text)`:违反约束 +- `error NotFound(message: Text)`:记录不存在 +- `error InvalidData(message: Text)`:存储值不符合类型 +- `error Migration(message: Text)`:schema 与迁移失败 diff --git a/lib/doc/dever-http-call.md b/lib/doc/dever-http-call.md new file mode 100644 index 0000000..bd7cf04 --- /dev/null +++ b/lib/doc/dever-http-call.md @@ -0,0 +1,35 @@ +# HTTP 调用 + +发送请求或处理完整响应,正文一次读入内存。 + +## dever.http.header + +构造 HTTP 头:名称转为小写,值按 UTF-8 编码。 + +## dever.http.headerValue + +按名称忽略大小写查找首个 HTTP 头,找不到返回 null。 + +## dever.http.namedHeader + +供 headerValue 匹配 HTTP 头名称。 + +## dever.http.foundHeader + +取出匹配头的原始字节值;没有匹配项时返回 null。 + +## dever.http.serve + +在给定监听器上处理 HTTP 请求,由 route 返回完整响应。 + +## dever.http.send + +向给定地址和端口发送 HTTP 请求,读取完整响应。 + +## dever.http.serveTls + +使用证书配置处理 HTTPS 请求。启用 HTTP/2 时须成功协商 h2。 + +## dever.http.readResponse + +取出 HTTP 响应;请求失败时继续传播错误。 diff --git a/lib/doc/dever-http-live.md b/lib/doc/dever-http-live.md new file mode 100644 index 0000000..526755b --- /dev/null +++ b/lib/doc/dever-http-live.md @@ -0,0 +1,48 @@ +# 流式响应 + +在请求处理函数中按块发送响应,结束时释放会话资源。 + +## dever.http.LiveLimits + +逐块限额及空闲期限,心跳期限必须短于空闲期限。 + +字段: +- `chunk_bytes`:单块上限 +- `idle_ms`:普通流空闲期限 +- `heartbeat_ms`:SSE 心跳周期 + +## dever.http.defaultLiveLimits + +默认每块 64 KiB、空闲期限 60 秒、心跳间隔 15 秒。 + +## dever.http.serveLive + +处理可分块回复的 HTTP 请求,route 通过 reply 写入响应。 + +## dever.http.serveLiveTls + +使用 TLS 处理可分块回复的 HTTP 请求。 + +## dever.http.respond + +通过 reply 一次发送完整响应。 + +## dever.http.start + +发送流式响应的状态码和响应头。 + +## dever.http.write + +向已开始的流式响应写入一块正文。 + +## dever.http.finish + +结束流式响应。 + +## dever.http.wroteLive + +写入成功时继续执行,失败时传播错误。 + +## dever.http.closedLive + +结束响应成功时继续执行,失败时传播错误。 diff --git a/lib/doc/dever-http-pool.md b/lib/doc/dever-http-pool.md new file mode 100644 index 0000000..c9b3ec3 --- /dev/null +++ b/lib/doc/dever-http-pool.md @@ -0,0 +1,45 @@ +# HTTP 连接池 + +连接池固定使用一个 origin,在创建它的任务作用域结束时关闭。 + +## dever.http.PoolLimits + +空闲保活和流读取限额。 + +字段: +- `idle_ms`:空闲连接期限 +- `chunk_bytes`:流分块上限 +- `read_ms`:流读取期限 + +## dever.http.ClientResult + +HTTP 连接池的创建结果。 + +分支: +- `Ready(client: dever.system.HttpClient)`:共享资源 +- `error Failed(message: Text)`:配置或资源失败 + +## dever.http.defaultPoolLimits + +使用有限的默认保活与读取配置。 + +## dever.http.client + +创建共享资源;第一次请求才连接。HTTPS 校验 CA 和主机名。 +未声明端口时使用协议默认端口;显式端口非法时立即报错。 + +## dever.http.request + +在固定 origin 发送独立请求;所有排队和 I/O 共用本次期限。 + +## dever.http.closeClient + +关闭所有连接并等待驱动释放;其他别名随后请求失败。 + +## dever.http.readClient + +取出连接池;创建失败时继续传播错误。 + +## dever.http.closedClient + +关闭成功时继续执行,失败时传播错误。 diff --git a/lib/doc/dever-http-stream.md b/lib/doc/dever-http-stream.md new file mode 100644 index 0000000..fedfc13 --- /dev/null +++ b/lib/doc/dever-http-stream.md @@ -0,0 +1,42 @@ +# 流式请求 + +通过连接池按需上传或下载正文。 + +## dever.http.StreamRequest + +流式上传总量不受 body_bytes 限制;每块受 chunk_bytes 限制,取块受 read_ms 限制。 + +字段: +- `method`:请求方法 +- `target`:路径及查询 +- `headers`:不含协议引擎拥有的长度或分块头 +- `body`:按背压取块的正文 + +## dever.http.StreamResponse + +响应头到达即返回;正文按需取块,EOF 无事件,终止错误最多交付一次。 + +字段: +- `status`:响应状态 +- `headers`:保留重复头 +- `body`:消费或关闭前占用请求名额 + +## dever.http.StreamResponseResult + +显式捕获响应头之前的配置、排队、传输或协议失败。 + +分支: +- `Read(response: StreamResponse)`:响应头与正文源 +- `error Failed(message: Text)`:打开失败 + +## dever.http.open + +发送普通 Request,返回流式响应。排队到响应头共用 timeout_ms,随后逐次读取用 read_ms。 + +## dever.http.openStream + +按需发送异步正文;取消消费或提前关闭时,HTTP/1 关闭连接,HTTP/2 仅取消当前流。 + +## dever.http.readStreamResponse + +把打开失败沿原选择类型传播;正文中的失败由消费函数处理。 diff --git a/lib/doc/dever-http-value.md b/lib/doc/dever-http-value.md new file mode 100644 index 0000000..7b5d4be --- /dev/null +++ b/lib/doc/dever-http-value.md @@ -0,0 +1,66 @@ +# HTTP 数据结构 + +请求、响应、HTTP 头及协议限额。 + +## dever.http.Header + +HTTP 头的名称和原始字节值。 + +字段: +- `name`:HTTP 头名称 +- `value`:未经文本转换的头部字节 + +## dever.http.Request + +HTTP 请求的方法、目标路径、头和完整正文。 + +字段: +- `method`:请求方法,例如 GET +- `target`:路径及查询字符串 +- `headers`:请求头,保留同名字段 +- `body`:完整请求正文字节 + +## dever.http.Response + +HTTP 响应的状态码、头和完整正文。 + +字段: +- `status`:HTTP 状态码 +- `headers`:响应头,保留同名字段 +- `body`:完整响应正文字节 + +## dever.http.Limits + +请求头、正文、请求期限和并发连接限制;http2=null 表示不启用 HTTP/2。 + +字段: +- `header_bytes`:请求头的字节上限 +- `body_bytes`:请求正文的字节上限 +- `timeout_ms`:请求期限;HTTP/2 服务连接在没有活动请求或响应流时,也按此期限回收。关闭握手最多再等待一个相同期限。 +- `connections`:并发连接上限 +- `http2`:HTTP/2 配置,null 表示关闭 + +## dever.http.Http2Limits + +HTTP/2 的并发流数、单流窗口和连接窗口配置。 + +字段: +- `streams`:单连接的并发流上限 +- `stream_window_bytes`:单流接收窗口,单位为字节 +- `connection_window_bytes`:连接接收窗口,单位为字节 + +## dever.http.ResponseResult + +完整响应或请求失败原因。 + +分支: +- `Read(response: Response)`:成功响应 +- `error Failed(message: Text)`:失败原因 + +## dever.http.defaultLimits + +默认限制:8 KiB 请求头、1 MiB 正文、5 秒期限、32 个连接,不启用 HTTP/2。 + +## dever.http.defaultHttp2Limits + +默认 HTTP/2 配置:16 个流,单流窗口 65535 字节,连接窗口 262144 字节。 diff --git a/lib/doc/dever-id.md b/lib/doc/dever-id.md new file mode 100644 index 0000000..cd15b04 --- /dev/null +++ b/lib/doc/dever-id.md @@ -0,0 +1,11 @@ +# 文本标识 + +Id 保存原始文本,与普通 Text 区分;不生成随机值或清洗内容。 + +## dever.id.fromText + +从文本构造稳定标识,保留空文本、大小写和 Unicode 内容。 + +## dever.id.toText + +还原标识的原始文本。 diff --git a/lib/doc/dever-io.md b/lib/doc/dever-io.md new file mode 100644 index 0000000..ad7e0f5 --- /dev/null +++ b/lib/doc/dever-io.md @@ -0,0 +1,116 @@ +# 文件与标准输出 + +File 副本共用游标和关闭状态。文件操作同步执行;异步流程用 blocking,不能在 pure 函数中调用。 + +## dever.io.OpenResult + +打开或新建文件的结果。 + +分支: +- `Opened(file: dever.system.File)`:成功状态 +- `error Failed(message: Text)`:失败原因 + +## dever.io.ReadState + +一次读取可短于请求长度;End 表示文件结束。 + +分支: +- `Read(bytes: Bytes)`:成功状态 +- `End`:成功状态 + +## dever.io.ReadResult + +显式捕获读取失败时保留 ReadState。 + +分支: +- `Done(state: ReadState)`:成功状态 +- `error Failed(message: Text)`:失败原因 + +## dever.io.SystemReadResult + +底层文件读取结果,在库内转换为 ReadState。 + +分支: +- `Read(bytes: Bytes)`:成功状态 +- `End`:成功状态 +- `error Failed(message: Text)`:失败原因 + +## dever.io.WriteResult + +写入全部字节或返回失败。 + +分支: +- `Done`:成功状态 +- `error Failed(message: Text)`:失败原因 + +## dever.io.CloseResult + +关闭共享文件句柄或返回失败。关闭不承诺数据已落盘。 + +分支: +- `Closed`:成功状态 +- `error Failed(message: Text)`:失败原因 + +## dever.io.ReadEvent + +流每次产生一个数据块;终止错误只产生一次,然后结束。 + +分支: +- `Chunk(bytes: Bytes)`:读取的数据 +- `error Failed(message: Text)`:读取失败 + +## dever.io.ReadStreamResult + +捕获流创建失败,已创建的流仍按需读取。 + +分支: +- `Streaming(stream: Stream)`:共享游标的同步流 +- `error Failed(message: Text)`:创建失败 + +## dever.io.println + +写入文本及换行;标准输出失败为带源码位置的 Fault。 + +## dever.io.open + +以只读方式打开现有文件。失败默认沿调用链传播。 + +## dever.io.create + +新建可读写文件;路径已经存在则失败,绝不截断现有文件。 + +## dever.io.opened + +成功时取得共享文件资源,失败时保留 OpenResult 错误身份。 + +## dever.io.read + +读取最多 limit 字节并推进共享游标;limit 必须为正,EOF 返回 End。 + +## dever.io.readState + +把底层读取结果转换为 ReadState;失败时传播 ReadResult.Failed。 + +## dever.io.write + +写入全部字节;失败使用 WriteResult 传播,空字节同样验证文件是否已关闭。 + +## dever.io.wrote + +成功时无输出,失败时保留 WriteResult 身份。 + +## dever.io.close + +关闭底层文件使所有别名失效;重复关闭失败。最后一个别名离开作用域也会释放文件。 + +## dever.io.closed + +成功时无输出,失败时保留 CloseResult 身份。 + +## dever.io.chunks + +先验证正数 limit 和文件未关闭,再创建按需读取的同步流。close(stream) 幂等,释放源别名但不关闭外部文件别名。 + +## dever.io.readStream + +成功时返回共享流,失败时保留创建错误身份。 diff --git a/lib/doc/dever-json-value.md b/lib/doc/dever-json-value.md new file mode 100644 index 0000000..09e019a --- /dev/null +++ b/lib/doc/dever-json-value.md @@ -0,0 +1,39 @@ +# JSON 文档 + +Document 用节点列表表示 JSON 树,子节点排在父节点之前。 + +## dever.json.value.Value + +JSON 节点的六种值:null、布尔、数字、字符串、数组和对象。数字保留原始文本。 + +分支: +- `Null`:空值 +- `Boolean(value: Bool)`:布尔值 +- `Number(raw: Text)`:原始数字 +- `String(text: Text)`:字符串 +- `Array(children: List)`:子节点 ID +- `Object(members: Map)`:字段到子节点 ID + +## dever.json.value.Document + +由根节点 ID 和节点列表组成的文档;节点 ID 是列表下标。 + +字段: +- `root`:根节点 ID +- `nodes`:按节点 ID 排列的节点列表 + +## dever.json.value.ParseResult + +文档解析结果。失败位置从零开始,按 Unicode 码点计数。 + +分支: +- `Parsed(document: Document)`:解析成功 +- `error Failed(position: Int, message: Text)`:解析失败 + +## dever.json.value.WriteResult + +文档编码结果。失败包含节点 ID 和原因。 + +分支: +- `Written(text: Text)`:编码成功 +- `error Failed(node: Int, message: Text)`:编码失败 diff --git a/lib/doc/dever-json.md b/lib/doc/dever-json.md new file mode 100644 index 0000000..39094a8 --- /dev/null +++ b/lib/doc/dever-json.md @@ -0,0 +1,137 @@ +# JSON + +Json 保留已验证的 JSON 内容和数字原文,支持直接解码为具体类型;Document 用于显式遍历节点。 + +## dever.json.JsonResult + +解析成功返回不透明 Json,失败保留错误文本。 + +分支: +- `Parsed(value: Json)`:解析成功 +- `error Failed(message: Text)`:解析失败 + +## dever.json.TextResult + +读取 JSON 字符串或返回类型错误。 + +分支: +- `Read(value: Text)`:字符串值 +- `error Failed(message: Text)`:类型错误 + +## dever.json.IntResult + +读取精确 Int 数字或返回类型错误。 + +分支: +- `Read(value: Int)`:整数值 +- `error Failed(message: Text)`:类型错误 + +## dever.json.fromText + +从文本解析 JSON,应用 16 MiB、64 层和 65536 值限制。 + +## dever.json.toText + +返回已经验证的 JSON 原文。 + +## dever.json.setProperty + +按对象字段名替换或追加一个 JSON 子值。 + +## dever.json.propertyValue + +读取对象字段;缺失字段返回 null,非对象输入传播 PropertyResult.Failed。 + +## dever.json.asText + +读取 JSON 字符串值。 + +## dever.json.asInteger + +读取不带小数和指数的 i64 JSON 数字。 + +## dever.json.encode + +把标量、公开 record、List 或可空值编码为 Json,无需手工构造 JSON 节点。 + +## dever.json.decode + +把 Json 解码为接收位置指定的类型,例如 `input: CreateInput = dever.json.decode(value)`。字段或类型不符时失败。 + +不复制 JSON 文本、不建立 Document,不自动清洗 Text。普通 API 参数已有具体类型时无需再调用;未知结构才使用动态 Json/Document。 + +## dever.json.parsed + +取出解析后的 Json;解析失败时继续传播错误。 + +## dever.json.parse + +把 JSON 文本解析为扁平 dever.json.value.Document。 + +## dever.json.stringify + +验证并编码手工构造的 dever.json.value.Document。 + +## dever.json.node + +读取节点;缺失节点返回 null。 + +## dever.json.property + +读取对象字段的节点 ID。 + +## dever.json.element + +读取数组元素的节点 ID。 + +## dever.json.parsedDocument + +取出文档;解析失败时保留位置和原因并传播错误。 + +## dever.json.written + +取出编码文本;编码失败时继续传播错误。 + +## dever.json.textValue + +取出 JSON 字符串;类型不匹配时传播错误。 + +## dever.json.intValue + +取出 JSON 整数;类型不匹配时传播错误。 + +## dever.json.PropertyResult + +区分缺失字段和非对象输入;JSON null 是存在的 Json 值。 + +分支: +- `Found(value: Json?)`:字段值或缺失 +- `error Failed(message: Text)`:非对象输入 + +## dever.json.propertyResult + +取出字段值;缺失字段返回 null,读取失败时传播错误。 + +## dever.json.object + +按 Map 插入顺序构造对象,写入时限制大小。 + +## dever.json.array + +按列表顺序构造 JSON 数组。 + +## dever.json.string + +转义并编码字符串。 + +## dever.json.integer + +编码精确 i64 整数。 + +## dever.json.boolean + +编码布尔值。 + +## dever.json.nullValue + +构造存在的 JSON null。 diff --git a/lib/doc/dever-net-listener.md b/lib/doc/dever-net-listener.md new file mode 100644 index 0000000..d08c8be --- /dev/null +++ b/lib/doc/dever-net-listener.md @@ -0,0 +1,47 @@ +# TCP 监听 + +监听器副本共用关闭状态,网络等待使用异步运行时。 + +## dever.net.ListenResult + +TCP 监听结果,成功包含监听器。 + +分支: +- `Listening(listener: dever.system.Listener)`:监听成功 +- `error Failed(message: Text)`:监听失败 + +## dever.net.PortResult + +读取监听端口的结果。 + +分支: +- `Bound(port: Int)`:绑定端口 +- `error Failed(message: Text)`:资源失败 + +## dever.net.listen + +在给定 host 和 port 创建 TCP 监听器。 + +## dever.net.listening + +取出监听器;监听失败时继续传播错误。 + +## dever.net.accept + +等待并接收一个 TCP 连接。 + +## dever.net.port + +读取监听器实际绑定的端口。 + +## dever.net.bound + +取出已绑定端口;读取失败时继续传播错误。 + +## dever.net.closeListener + +关闭监听器,停止接收新连接。 + +## dever.net.connections + +按需接收 TCP 连接,返回异步连接流。 diff --git a/lib/doc/dever-net-socket.md b/lib/doc/dever-net-socket.md new file mode 100644 index 0000000..544b0b7 --- /dev/null +++ b/lib/doc/dever-net-socket.md @@ -0,0 +1,76 @@ +# TCP 连接 + +连接副本共用读写和关闭状态,网络等待使用异步运行时。 + +## dever.net.ConnectResult + +TCP 连接结果,成功包含 socket。 + +分支: +- `Connected(socket: dever.system.Socket)`:连接成功 +- `error Failed(message: Text)`:连接失败 + +## dever.net.ReadStreamResult + +TCP 读取流的创建结果。 + +分支: +- `Streaming(stream: AsyncStream)`:读取流 +- `error Failed(message: Text)`:创建失败 + +## dever.net.SystemReadResult + +底层 TCP 读取结果,在库内转换为 ReadState。 + +分支: +- `Read(bytes: Bytes)`:读取数据 +- `End`:数据结束 +- `error Failed(message: Text)`:读取失败 + +## dever.net.connect + +连接给定主机和端口,支持 IP 地址或主机名。 + +## dever.net.connectTimeout + +在指定毫秒期限内连接 IP 地址和端口;此入口不解析主机名。 + +## dever.net.connected + +取出 socket;连接失败时继续传播错误。 + +## dever.net.read + +读取最多 limit 字节,连接结束时返回 End。 + +## dever.net.readState + +转换 TCP 读取状态;失败时传播 dever.io.ReadResult.Failed。 + +## dever.net.write + +向 TCP 连接写入全部字节。 + +## dever.net.timeout + +设置后续 TCP 读写的毫秒期限。 + +## dever.net.wrote + +写入成功时继续执行,失败时传播错误。 + +## dever.net.close + +关闭 TCP 连接及其共享别名。 + +## dever.net.closed + +关闭成功时继续执行,失败时传播错误。 + +## dever.net.chunks + +按需读取 TCP 数据块,返回异步字节事件流。 + +## dever.net.readStream + +取出读取流;创建失败时继续传播错误。 diff --git a/lib/doc/dever-process.md b/lib/doc/dever-process.md new file mode 100644 index 0000000..640548c --- /dev/null +++ b/lib/doc/dever-process.md @@ -0,0 +1,35 @@ +# 进程与命令 + +读取当前进程参数和受管命令结果,参数不通过 shell 执行。 + +## dever.process.Output + +已启动命令的退出码与原始输出字节,非零退出码也是正常结果。 + +字段: +- `code`:退出码 +- `stdout`:标准输出 +- `stderr`:标准错误 + +## dever.process.Error + +外部命令 Port 可声明的执行错误。 + +分支: +- `error Failed(message: Text)`:失败原因 + +## dever.process.ArgumentsResult + +读取当前进程参数的具体结果,无法表示为 UTF-8 的参数明确失败。 + +分支: +- `Read(values: List)`:参数列表 +- `error Failed(message: Text)`:读取失败 + +## dever.process.arguments + +跳过程序路径 argv[0],保留之后所有参数的顺序和内容。 + +## dever.process.argumentsValue + +取出进程参数;读取失败时继续传播错误。 diff --git a/lib/doc/dever-site.md b/lib/doc/dever-site.md new file mode 100644 index 0000000..926f2ab --- /dev/null +++ b/lib/doc/dever-site.md @@ -0,0 +1,7 @@ +# 当前站点 + +站点由服务配置和入口确定,不能用业务参数切换。 + +## dever.site.key + +读取当前入口匹配的站点标识。 diff --git a/lib/doc/dever-sse.md b/lib/doc/dever-sse.md new file mode 100644 index 0000000..1a0113c --- /dev/null +++ b/lib/doc/dever-sse.md @@ -0,0 +1,25 @@ +# SSE + +在 HTTP 流式响应中发送事件和心跳。 + +## dever.sse.Event + +数据保留换行与空 id;重连历史由应用维护。 + +字段: +- `event`:事件名称 +- `data`:事件正文 +- `id`:可空事件编号 +- `retry_ms`:可空重试间隔 + +## dever.sse.start + +提交 SSE 头并启用空闲心跳。 + +## dever.sse.send + +编码完整事件并受单块限额约束。 + +## dever.sse.wrote + +传播 SSE 写入失败。 diff --git a/lib/doc/dever-storage.md b/lib/doc/dever-storage.md new file mode 100644 index 0000000..ca8985e --- /dev/null +++ b/lib/doc/dever-storage.md @@ -0,0 +1,19 @@ +# 上传存储 + +同领域 Adapter 用 storage.put 接收一次性 Upload,不暴露临时文件路径。 + +## dever.storage.PutResult + +上传持久化的结果,成功包含文件的 Uuid。 + +分支: +- `Stored(key: Uuid)`:持久化后的文件标识。 +- `error Failed(message: Text)`:文件写入或同步失败。 + +## dever.storage.put + +在 Adapter 中持久化请求上传,成功返回文件 Uuid;失败时传播错误。 + +## dever.storage.stored + +取出已存文件的 Uuid;保存失败时继续传播错误。 diff --git a/lib/doc/dever-task.md b/lib/doc/dever-task.md new file mode 100644 index 0000000..7aaf56b --- /dev/null +++ b/lib/doc/dever-task.md @@ -0,0 +1,15 @@ +# 异步等待 + +挂起当前任务,不占用线程等待。 + +## dever.task.sleep + +等待指定毫秒数,错误使用 dever.time.SleepResult 传播。所在函数自动推导为异步。 + +## dever.task.slept + +异步等待成功时继续执行,时长无效时传播错误。 + +## dever.task.ticks + +按需等待周期信号,首个序号为 1,跳过落后的周期;关闭释放定时器。 diff --git a/lib/doc/dever-time-calendar.md b/lib/doc/dever-time-calendar.md new file mode 100644 index 0000000..3ef5863 --- /dev/null +++ b/lib/doc/dever-time-calendar.md @@ -0,0 +1,63 @@ +# 日历结果 + +日期时间、日期、时间和时长操作的结果类型。 + +## dever.time.DateTimeResult + +日期时间操作的结果,失败包含原因。 + +分支: +- `Read(value: DateTime)`:成功值 +- `error Failed(message: Text)`:失败原因 + +## dever.time.datetimeValue + +取出日期时间;操作失败时继续传播错误。 + +## dever.time.DateResult + +日期操作的结果,失败包含原因。 + +分支: +- `Read(value: Date)`:成功值 +- `error Failed(message: Text)`:失败原因 + +## dever.time.dateValue + +取出日期;操作失败时继续传播错误。 + +## dever.time.TimeResult + +日内时间操作的结果,失败包含原因。 + +分支: +- `Read(value: Time)`:成功值 +- `error Failed(message: Text)`:失败原因 + +## dever.time.timeValue + +取出日内时间;操作失败时继续传播错误。 + +## dever.time.DurationResult + +时长操作的结果,失败包含原因。 + +分支: +- `Read(value: Duration)`:成功值 +- `error Failed(message: Text)`:失败原因 + +## dever.time.durationValue + +取出时长;操作失败时继续传播错误。 + +## dever.time.TextResult + +格式化文本操作的结果,失败包含原因。 + +分支: +- `Read(value: Text)`:成功值 +- `error Failed(message: Text)`:失败原因 + +## dever.time.textValue + +取出格式化文本;操作失败时继续传播错误。 diff --git a/lib/doc/dever-time-clock.md b/lib/doc/dever-time-clock.md new file mode 100644 index 0000000..e198e46 --- /dev/null +++ b/lib/doc/dever-time-clock.md @@ -0,0 +1,23 @@ +# 时钟 + +提供系统毫秒时钟和进程内单调纳秒时钟。 + +## dever.time.ClockResult + +时钟读数或读取失败原因。 + +分支: +- `Read(value: Int)`:成功值 +- `error Failed(message: Text)`:失败原因 + +## dever.time.unixMillis + +读取 Unix epoch 起的系统时钟毫秒数。 + +## dever.time.monotonicNanos + +读取从本进程首次调用起经过的单调纳秒数。 + +## dever.time.clockValue + +取出时钟读数;读取失败时继续传播错误。 diff --git a/lib/doc/dever-time-operations.md b/lib/doc/dever-time-operations.md new file mode 100644 index 0000000..d301334 --- /dev/null +++ b/lib/doc/dever-time-operations.md @@ -0,0 +1,47 @@ +# 日期与时长 + +使用 UTC 日期时间和毫秒时长。 + +## dever.time.now + +读取当前 UTC 日期时间,精度为毫秒。 + +## dever.time.parseDatetime + +解析 RFC3339 和明确偏移,最多三位小数;拒绝闰秒及转换后超出四位 UTC 年份的值。 + +## dever.time.formatDatetime + +输出 UTC YYYY-MM-DDTHH:MM:SS[.sss]Z;零毫秒省略小数。 + +## dever.time.parseDate + +解析严格的 YYYY-MM-DD,验证实际日历日期。 + +## dever.time.formatDate + +输出 YYYY-MM-DD,Date 表示 UTC 午夜毫秒。 + +## dever.time.parseTime + +解析 HH:MM:SS 或 HH:MM:SS.sss;拒绝闰秒和无效钟点。 + +## dever.time.formatTime + +输出 HH:MM:SS[.sss],零毫秒省略小数。 + +## dever.time.duration + +构造有符号毫秒时长,保留完整 Int 范围。 + +## dever.time.add + +相加并检查 i64 溢出;结果是否能格式化由格式化操作判断。 + +## dever.time.subtract + +相减并检查 i64 溢出;结果是否能格式化由格式化操作判断。 + +## dever.time.difference + +计算有符号毫秒差,溢出时传播 DurationResult.Failed。 diff --git a/lib/doc/dever-time.md b/lib/doc/dever-time.md new file mode 100644 index 0000000..306aa84 --- /dev/null +++ b/lib/doc/dever-time.md @@ -0,0 +1,19 @@ +# 同步等待 + +等待占用当前线程;异步流程使用 dever.task.sleep。 + +## dever.time.SleepResult + +等待成功或无法表示的时长;零毫秒合法,负数失败。 + +分支: +- `Done`:等待完成 +- `error Failed(message: Text)`:时长错误 + +## dever.time.sleep + +阻塞当前线程直到指定毫秒数过去。 + +## dever.time.slept + +等待成功时继续执行,时长无效时传播错误。 diff --git a/lib/doc/dever-tls.md b/lib/doc/dever-tls.md new file mode 100644 index 0000000..e958a9b --- /dev/null +++ b/lib/doc/dever-tls.md @@ -0,0 +1,39 @@ +# TLS + +构造可共享的 TLS 配置,连接时验证证书和目标名称,不提供跳过校验的模式。 + +## dever.tls.ClientResult + +显式捕获客户端 TLS 配置的输入失败。 + +分支: +- `Ready(config: dever.system.ClientTls)`:配置成功 +- `error Failed(message: Text)`:配置错误 + +## dever.tls.ServerResult + +显式捕获服务端证书或私钥的输入失败。 + +分支: +- `Ready(config: dever.system.ServerTls)`:配置成功 +- `error Failed(message: Text)`:配置错误 + +## dever.tls.system + +使用随二进制提供的公共 CA,不读取机器上的可变证书配置。 + +## dever.tls.client + +仅信任指定 PEM CA;无证书或非法 PEM 默认传播失败。 + +## dever.tls.clientConfig + +取出客户端 TLS 配置;创建失败时继续传播错误。 + +## dever.tls.server + +读取证书链及匹配的单个私钥,返回共享服务端配置。 + +## dever.tls.serverConfig + +取出服务端 TLS 配置;创建失败时继续传播错误。 diff --git a/lib/doc/dever-websocket.md b/lib/doc/dever-websocket.md new file mode 100644 index 0000000..a48c3c9 --- /dev/null +++ b/lib/doc/dever-websocket.md @@ -0,0 +1,112 @@ +# WebSocket + +使用 HTTP/1 升级,连接随创建任务或服务 handler 结束而关闭。不支持 HTTP/2 升级、子协议或压缩。 + +## dever.websocket.Limits + +限制完整消息、接收等待和写入期限。 + +字段: +- `message_bytes`:完整消息字节上限 +- `idle_ms`:一次接收的毫秒期限 +- `write_ms`:写入和握手的毫秒期限 + +## dever.websocket.Message + +协议引擎交付完整消息;Ping 和 Pong 最多 125 字节。 + +分支: +- `Text(value: Text)`:文本 +- `Binary(value: Bytes)`:二进制 +- `Ping(value: Bytes)`:保活请求 +- `Pong(value: Bytes)`:保活响应 + +## dever.websocket.ConnectResult + +WebSocket 连接或协议升级的结果。 + +分支: +- `Connected(socket: dever.system.WebSocket)`:已连接 +- `error Failed(message: Text)`:连接失败 + +## dever.websocket.ReceiveState + +区分完整消息与正常关闭。 + +分支: +- `Read(message: Message)`:完整消息 +- `End`:正常关闭 + +## dever.websocket.ReceiveResult + +接收操作的显式捕获结果。 + +分支: +- `Done(state: ReceiveState)`:接收状态 +- `error Failed(message: Text)`:接收失败 + +## dever.websocket.SystemReceiveResult + +系统边界结果在库内转换为公开接收状态。 + +分支: +- `Read(message: Message)`:完整消息 +- `End`:正常关闭 +- `error Failed(message: Text)`:接收失败 + +## dever.websocket.MessageEvent + +流在故障时产生一次失败后结束。 + +分支: +- `Read(message: Message)`:完整消息 +- `error Failed(message: Text)`:终止失败 + +## dever.websocket.defaultLimits + +默认消息 1 MiB、接收 60 秒、写入与握手 5 秒。 + +## dever.websocket.accept + +只升级当前 HTTP/1.1 请求。失败时仍可回复普通 HTTP 响应;handler 退出关闭所有 socket 别名。 + +## dever.websocket.connect + +连接数字 IP 和 origin-form 路径,不自动重试。 + +## dever.websocket.open + +支持 ws/wss 和 DNS;WSS 校验证书和目标名称,拒绝用户信息及 fragment。 +未声明端口时使用协议默认端口;显式端口非法时在连接前报错。 + +## dever.websocket.connected + +取出 WebSocket;连接失败时继续传播错误。 + +## dever.websocket.send + +发送一次完整消息;方向锁与写入共用期限,已开始写帧的取消会关闭连接。 + +## dever.websocket.wrote + +消息发送成功时继续执行,失败时传播错误。 + +## dever.websocket.receive + +按需读取协议帧,先回复 Ping 再交付应用;接收超时关闭资源。 + +## dever.websocket.receiveState + +正常关闭映射为 End,协议失败保持 ReceiveResult 身份。 + +## dever.websocket.messages + +惰性消费消息;正常关闭结束,终止故障只产生一次 Failed;关闭流释放源别名。 + +## dever.websocket.close + +校验关闭码和至多 123 UTF-8 字节原因,并在写期限内等待对端关闭。 + +## dever.websocket.closed + +关闭成功时继续执行,失败时传播错误。 diff --git a/lib/doc/float.md b/lib/doc/float.md new file mode 100644 index 0000000..d1048f8 --- /dev/null +++ b/lib/doc/float.md @@ -0,0 +1,15 @@ +# 浮点数 + +Float 文本使用 NaN、inf 和 -inf 表示非有限值。 + +## float.fromInt + +把 Int 转换为 Float;超过 Float 有效整数精度时按浮点规则舍入。 + +## float.parse + +接受十进制、科学计数法和精确拼写 NaN、inf、-inf;格式非法时返回 null。 + +## float.toText + +输出 Float 文本,包括 NaN、inf 和 -inf。 diff --git a/lib/doc/int.md b/lib/doc/int.md new file mode 100644 index 0000000..948020a --- /dev/null +++ b/lib/doc/int.md @@ -0,0 +1,11 @@ +# 整数 + +64 位有符号整数与十进制文本转换。 + +## int.parse + +解析有符号十进制 Int;格式非法或超出 i64 范围时返回 null。 + +## int.toText + +把 Int 转换为十进制文本。 diff --git a/lib/doc/math.md b/lib/doc/math.md new file mode 100644 index 0000000..3454030 --- /dev/null +++ b/lib/doc/math.md @@ -0,0 +1,35 @@ +# 数学 + +Float 数学函数,角度单位为弧度。 + +## math.sqrt + +计算平方根;负数产生 NaN。 + +## math.sin + +计算以弧度表示的正弦值。 + +## math.cos + +计算以弧度表示的余弦值。 + +## math.log + +计算自然对数,遵循 Float 的非有限值规则。 + +## math.pow + +计算 value 的 exponent 次幂,遵循 Float 规则。 + +## math.isNan + +判断 Float 是否为 NaN。 + +## math.isFinite + +判断 Float 是否为有限值。 + +## math.isInfinite + +判断 Float 是否为正无穷或负无穷。 diff --git a/lib/doc/text.md b/lib/doc/text.md new file mode 100644 index 0000000..bc3063d --- /dev/null +++ b/lib/doc/text.md @@ -0,0 +1,63 @@ +# 文本 + +Text 使用 UTF-8;位置和长度按 Unicode 码点计算。 + +## text.trim + +移除文本两端的 Unicode 空白。 + +## text.lower + +按 Unicode 规则转换为小写。 + +## text.upper + +按 Unicode 规则转换为大写,转换结果可以包含更多字符。 + +## text.codepoint + +文本恰好包含一个 Unicode 标量时返回其整数值,否则返回 null。 + +## text.fromCodepoint + +把有效 Unicode 标量值转换为文本;负数、代理区和超出范围的值返回 null。 + +## text.at + +按从 0 开始的 Unicode 标量位置取单个字符;越界返回 null。 + +## text.slice + +按 Unicode 标量位置返回半开区间 [start, end);负数、倒序或越界返回 null。 + +## text.indexOf + +返回首个匹配子串的 Unicode 标量位置;未匹配返回 null,空 pattern 返回 0。 + +## text.contains + +判断文本是否包含给定子串。 + +## text.startsWith + +判断文本是否以给定前缀开头。 + +## text.endsWith + +判断文本是否以给定后缀结尾。 + +## text.split + +按给定分隔文本拆分;空分隔符按 Unicode 标量拆分,不产生首尾空项。 + +## text.replace + +替换所有匹配子串;空 pattern 会在字符边界及首尾插入 replacement。 + +## text.isEmpty + +判断文本是否为空。 + +## text.join + +按列表顺序拼接,在相邻元素之间插入 separator;空列表产生空文本。 diff --git a/lib/doc/uuid.md b/lib/doc/uuid.md new file mode 100644 index 0000000..ed7a3c5 --- /dev/null +++ b/lib/doc/uuid.md @@ -0,0 +1,11 @@ +# UUID + +独立的 128 位标识类型,可作 Map 键;解析标准 8-4-4-4-12 形式,输出小写。 + +## uuid.parse + +解析恰好 36 字符的标准形式;输入无效时返回 null。 + +## uuid.toText + +输出小写的标准连字符形式。 diff --git a/lib/float.dever b/lib/float.dever new file mode 100644 index 0000000..4636360 --- /dev/null +++ b/lib/float.dever @@ -0,0 +1,11 @@ +public fromInt(value: Int) (result: Float) { + result = dever.system.floatFromInt(value) +} + +public parse(value: Text) (result: Float?) { + result = dever.system.floatParse(value) +} + +public toText(value: Float) (result: Text) { + result = dever.system.floatToText(value) +} diff --git a/lib/int.dever b/lib/int.dever new file mode 100644 index 0000000..416b4c1 --- /dev/null +++ b/lib/int.dever @@ -0,0 +1,7 @@ +public parse(value: Text) (result: Int?) { + result = dever.system.intParse(value) +} + +public toText(value: Int) (result: Text) { + result = dever.system.intToText(value) +} diff --git a/lib/math.dever b/lib/math.dever new file mode 100644 index 0000000..9b8b1ed --- /dev/null +++ b/lib/math.dever @@ -0,0 +1,31 @@ +public sqrt(value: Float) (result: Float) { + result = dever.system.floatSqrt(value) +} + +public sin(value: Float) (result: Float) { + result = dever.system.floatSin(value) +} + +public cos(value: Float) (result: Float) { + result = dever.system.floatCos(value) +} + +public log(value: Float) (result: Float) { + result = dever.system.floatLog(value) +} + +public pow(value: Float, exponent: Float) (result: Float) { + result = dever.system.floatPow(value, exponent) +} + +public isNan(value: Float) (result: Bool) { + result = dever.system.floatIsNan(value) +} + +public isFinite(value: Float) (result: Bool) { + result = dever.system.floatIsFinite(value) +} + +public isInfinite(value: Float) (result: Bool) { + result = dever.system.floatIsInfinite(value) +} diff --git a/lib/text.dever b/lib/text.dever new file mode 100644 index 0000000..a77dbf3 --- /dev/null +++ b/lib/text.dever @@ -0,0 +1,59 @@ +public trim(value: Text) (result: Text) { + result = dever.system.textTrim(value) +} + +public lower(value: Text) (result: Text) { + result = dever.system.textLower(value) +} + +public upper(value: Text) (result: Text) { + result = dever.system.textUpper(value) +} + +public codepoint(value: Text) (result: Int?) { + result = dever.system.textCodepoint(value) +} + +public fromCodepoint(value: Int) (result: Text?) { + result = dever.system.textFromCodepoint(value) +} + +public at(value: Text, index: Int) (result: Text?) { + result = dever.system.textAt(value, index) +} + +public slice(value: Text, start: Int, end: Int) (result: Text?) { + result = dever.system.textSlice(value, start, end) +} + +public indexOf(value: Text, pattern: Text) (result: Int?) { + result = dever.system.textIndexOf(value, pattern) +} + +public contains(value: Text, pattern: Text) (result: Bool) { + result = dever.system.textContains(value, pattern) +} + +public startsWith(value: Text, prefix: Text) (result: Bool) { + result = dever.system.textStartsWith(value, prefix) +} + +public endsWith(value: Text, suffix: Text) (result: Bool) { + result = dever.system.textEndsWith(value, suffix) +} + +public split(value: Text, separator: Text) (result: List) { + result = dever.system.textSplit(value, separator) +} + +public replace(value: Text, pattern: Text, replacement: Text) (result: Text) { + result = dever.system.textReplace(value, pattern, replacement) +} + +public isEmpty(value: Text) (result: Bool) { + result = dever.system.textIsEmpty(value) +} + +public join(values: List, separator: Text) (result: Text) { + result = dever.system.textJoin(values, separator) +} diff --git a/lib/uuid.dever b/lib/uuid.dever new file mode 100644 index 0000000..7e20094 --- /dev/null +++ b/lib/uuid.dever @@ -0,0 +1,7 @@ +public parse(value: Text) (result: Uuid?) { + result = dever.system.uuidParse(value) +} + +public toText(value: Uuid) (result: Text) { + result = dever.system.uuidToText(value) +} diff --git a/rust/Cargo.lock b/rust/Cargo.lock new file mode 100644 index 0000000..74bb83a --- /dev/null +++ b/rust/Cargo.lock @@ -0,0 +1,2609 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures 0.2.17", + "password-hash", + "zeroize", +] + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "boxcar" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f64beae40a84da1b4b26ff2761a5b895c12adc41dc25aaee1c4f2bbfe97a6e" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] + +[[package]] +name = "cc" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "num-traits", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "deadpool" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e98a7e119cd347f4201e1159b19831029e203e2d8b790547708e8157b4acf1e" +dependencies = [ + "deadpool-runtime", + "tokio", +] + +[[package]] +name = "deadpool-postgres" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65a536565624b97fc19f758cd01b15d12908d3344425066efc8162236fbd3749" +dependencies = [ + "async-trait", + "deadpool", + "getrandom 0.4.3", + "tokio", + "tokio-postgres", + "tracing", +] + +[[package]] +name = "deadpool-runtime" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2657f61fb1dd8bf37a8d51093cc7cee4e77125b22f7753f49b289f831bec2bae" +dependencies = [ + "tokio", +] + +[[package]] +name = "deadpool-sqlite" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b02743f8fad68dc9bb2851c0a4a1d63190d8e3885c5dea20f8a0a2a6bff9a" +dependencies = [ + "deadpool", + "deadpool-sync", + "rusqlite", +] + +[[package]] +name = "deadpool-sync" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e385cc95d3d582c328b36d1ff90feac061102b001894b555e6b465a2e0eaabbf" +dependencies = [ + "deadpool-runtime", +] + +[[package]] +name = "dec" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ae126ef80702bd514a66279b6dac1e58ff1dbcb12024f7110de95608348fd7" +dependencies = [ + "decnumber-sys", + "libc", + "paste", + "static_assertions", +] + +[[package]] +name = "decnumber-sys" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b4bc33814bd5bcd46dd13f9471a29ab1a22c4701ae0c4a182e45e8336d1a5b" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "der_derive", + "flagset", + "zeroize", +] + +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dever" +version = "0.0.1" +dependencies = [ + "base64 0.22.1", + "dec", + "dever-runtime", + "flate2", + "goblin", + "hmac 0.12.1", + "node-semver", + "pep440_rs", + "pep508_rs", + "pulldown-cmark", + "ring", + "rustix", + "serde", + "serde_json", + "sha2 0.10.9", + "tar", + "ureq", + "url", + "zip", +] + +[[package]] +name = "dever-runtime" +version = "0.0.1" +dependencies = [ + "ahash", + "argon2", + "base64 0.22.1", + "bytes", + "chrono", + "deadpool-postgres", + "deadpool-sqlite", + "dec", + "futures-util", + "getrandom 0.2.17", + "hmac 0.12.1", + "http-body-util", + "hyper", + "hyper-util", + "indexmap", + "rustix", + "rustls", + "rustls-native-certs", + "serde", + "serde_json", + "sha2 0.10.9", + "subtle", + "tokio", + "tokio-postgres", + "tokio-postgres-rustls", + "tokio-rustls", + "tokio-tungstenite", + "tokio-util", + "tower-service", + "uuid", + "webpki-roots", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" + +[[package]] +name = "flagset" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-macro", + "futures-sink", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "goblin" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17582616a7718cca54cec18e534a76c7c4aec11a8b9a85695712f262fd15a4c8" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "h2" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a596f1b20ed2cc5ecac41a164aaebc7258057060f06c0cf7a2ba3991ee7990fb" +dependencies = [ + "hashbrown 0.17.1", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "libc", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libredox" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6480ccc157a1389bb2e4891b24751b0f798ba640d22386f23143fbcc89da195a" +dependencies = [ + "libc", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1d20bef17f513b9b3004532233187769cd072d790971f4e4da0e346eb6401e8" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "cfg-if", + "miette-derive", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "node-semver" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b1a233ea5dc37d2cfba31cfc87a5a56cc2a9c04e3672c15d179ca118dae40a7" +dependencies = [ + "bytecount", + "miette", + "nom", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "objc2-core-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pep440_rs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31095ca1f396e3de32745f42b20deef7bc09077f918b085307e8eab6ddd8fb9c" +dependencies = [ + "once_cell", + "serde", + "unicode-width 0.2.2", + "unscanny", + "version-ranges", +] + +[[package]] +name = "pep508_rs" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faee7227064121fcadcd2ff788ea26f0d8f2bd23a0574da11eca23bc935bcc05" +dependencies = [ + "boxcar", + "indexmap", + "itertools", + "once_cell", + "pep440_rs", + "regex", + "rustc-hash", + "serde", + "smallvec", + "thiserror 1.0.69", + "unicode-width 0.2.2", + "url", + "urlencoding", + "version-ranges", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_shared", + "serde", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "postgres-protocol" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08808e3c483c46e999108051c78334f473d5adb59d78bb80a1268c7e6aa6c514" +dependencies = [ + "base64 0.22.1", + "byteorder", + "bytes", + "fallible-iterator 0.2.0", + "hmac 0.13.0", + "md-5", + "memchr", + "rand", + "sha2 0.11.0", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "851ca9db4932932d69f3ea811b1abe63087a0f740a47692619dd40d4899b68be" +dependencies = [ + "bytes", + "fallible-iterator 0.2.0", + "postgres-protocol", + "uuid", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulldown-cmark" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" +dependencies = [ + "bitflags", + "memchr", + "unicase", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsqlite-vfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" +dependencies = [ + "hashbrown 0.16.1", + "thiserror 2.0.20", +] + +[[package]] +name = "rusqlite" +version = "0.40.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f2a97da3e3873c73cb2a2e71b35c40ff95e0b1eefa8d72d8499a6928c3b5b3" +dependencies = [ + "bitflags", + "fallible-iterator 0.3.0", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", + "sqlite-wasm-rs", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6725596c3f2c3a0aef021139e145d4eafe314a6623e4680ca83852b2c67ab2ba" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scroll" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1257cd4248b4132760d6524d6dda4e053bc648c9070b960929bf50cfb1e7add" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1a36a382ed65dbcc0ab47fd5e9a94112417ccd34560a392ef3b7b0f0ec39148" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tls_codec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" +dependencies = [ + "tls_codec_derive", + "zeroize", +] + +[[package]] +name = "tls_codec_derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tokio-postgres" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a528f7d280f6d5b9cd149635c8705b0dd049754bc67d81d31fa25169a93809d3" +dependencies = [ + "async-trait", + "byteorder", + "bytes", + "fallible-iterator 0.2.0", + "futures-channel", + "futures-util", + "log", + "parking_lot", + "percent-encoding", + "phf", + "pin-project-lite", + "postgres-protocol", + "postgres-types", + "rand", + "socket2", + "tokio", + "tokio-util", + "whoami", +] + +[[package]] +name = "tokio-postgres-rustls" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c2ad44aa0ae96db89c4742212ed41645b2f597311ff6e1945542a4d9fadc2fb" +dependencies = [ + "rustls", + "sha2 0.11.0", + "tokio", + "tokio-postgres", + "tokio-rustls", + "x509-cert", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17a073bfed563fa236697a068031408a93cd9522e08abf9933ead3e73411bd71" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48ac77174b19c110a50ab2128b24215ac9cb40e0e12e093fb602d175c569d22" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror 2.0.20", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unscanny" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9df2af067a7953e9c3831320f35c1cc0600c30d44d9f7a12b01db1cd88d6b47" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7ac20be9b7726e0bbdbf974c059676d9acb1cd414961f570a4e8231cacd7fc" +dependencies = [ + "base64 0.23.1", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "ureq-proto", + "utf8-zero", + "webpki-roots", +] + +[[package]] +name = "ureq-proto" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86fd172ccca569e458f61b6bdd6220965a9ef36e672a6852953b51a0e1583be" +dependencies = [ + "base64 0.23.1", + "http", + "httparse", + "log", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-ranges" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e9bd4e9c9ff6a2a9b5969462ba26216af3e010df0377dad8320ab515262ef8" +dependencies = [ + "smallvec", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42" +dependencies = [ + "wasi 0.14.7+wasi-0.2.4", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.9", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626c4bac6755d76ffc12cb01b2eac751db1996b9e0041de9aa02c8c211ddc82c" +dependencies = [ + "libc", + "libredox", + "objc2-system-configuration", + "wasite", + "web-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid 0.9.6", + "der", + "spki", + "tls_codec", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "flate2", + "indexmap", + "memchr", + "thiserror 2.0.20", + "zopfli", +] + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 0000000..cb5928d --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,127 @@ +[workspace] +members = ["src/runtime"] +resolver = "3" + +[workspace.package] +version = "0.0.1" +edition = "2024" +rust-version = "1.95" +publish = false + +[workspace.lints.rust] +unsafe_code = "forbid" + +[workspace.dependencies] +dever-runtime = { path = "src/runtime" } +pulldown-cmark = { version = "=0.13.4", default-features = false } +dec = "=0.4.11" +getrandom = "=0.2.17" +argon2 = { version = "=0.5.3", features = ["zeroize"] } +sha2 = "=0.10.9" +hmac = "=0.12.1" +base64 = "=0.22.1" +subtle = "=2.6.1" +zeroize = "=1.9.0" +uuid = { version = "=1.28.0", default-features = false } +chrono = { version = "=0.4.45", default-features = false, features = ["std"] } +indexmap = "=2.14.0" +ahash = "=0.8.12" +serde = { version = "=1.0.228", features = ["derive"] } +serde_json = { version = "=1.0.145", features = ["raw_value"] } +node-semver = "=2.2.0" +url = "=2.5.8" +zip = { version = "=2.4.2", default-features = false, features = ["deflate"] } +rustix = { version = "=1.1.2", features = ["net", "process", "fs"] } +tokio = { version = "=1.53.1", features = ["rt-multi-thread", "sync", "time", "macros"] } +tokio-util = { version = "=0.7.19", features = ["rt"] } +futures-util = { version = "=0.3.34", default-features = false, features = ["std", "sink"] } +tokio-tungstenite = { version = "=0.30.0", default-features = false, features = ["handshake"] } +hyper = { version = "=1.11.1", features = ["http1", "http2", "client", "server"] } +hyper-util = { version = "=0.1.20", features = ["tokio", "client-legacy", "http1", "http2"] } +http-body-util = "=0.1.5" +bytes = "=1.12.1" +rustls = { version = "=0.23.44", default-features = false, features = ["std", "ring", "tls12"] } +tokio-rustls = { version = "=0.26.5", default-features = false, features = ["ring", "tls12"] } +webpki-roots = "=1.0.8" +tower-service = "=0.3.3" +deadpool-sqlite = { version = "=0.14.0", features = ["bundled", "hooks", "functions"] } +deadpool-postgres = "=0.14.2" +tokio-postgres = { version = "=0.7.18", features = ["with-uuid-1"] } +tokio-postgres-rustls = { version = "=0.14.0", default-features = false, features = ["ring"] } +rustls-native-certs = "=0.8.4" + +[package] +name = "dever" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +publish.workspace = true + +[features] +postgres = ["dever-runtime/postgres"] + +[[bin]] +name = "dever" +path = "src/cli/main.rs" + +[[example]] +name = "native-release" +path = "src/cli/release/main.rs" + +[dependencies] +dec.workspace = true +dever-runtime = { workspace = true, features = ["wire", "calendar", "uuid"] } +pulldown-cmark.workspace = true +serde.workspace = true +serde_json.workspace = true +sha2.workspace = true +base64.workspace = true +node-semver.workspace = true +url.workspace = true +zip.workspace = true +ureq = { version = "=3.4.2", default-features = false, features = ["rustls"] } +ring = "=0.17.14" +goblin = "=0.10.7" +tar = { version = "=0.4.44", default-features = false } +flate2 = "=1.1.5" +pep440_rs = "=0.7.3" +pep508_rs = "=0.9.2" + +[target.'cfg(target_os = "linux")'.dependencies] +rustix.workspace = true + +[dev-dependencies] +dever-runtime = { workspace = true, features = ["http"] } +hmac.workspace = true + +[lints] +workspace = true + +[[test]] +name = "syntax" +path = "../test/rust/syntax.rs" + +[[test]] +name = "compiler" +path = "../test/rust/compiler.rs" + +[[test]] +name = "native" +path = "../test/rust/native.rs" + +[[bench]] +name = "performance" +path = "../test/rust/performance.rs" +harness = false + +[profile.release] +codegen-units = 1 +lto = "fat" +overflow-checks = true +strip = "symbols" + +[profile.dev] +debug = 0 + +[profile.test] +debug = 0 diff --git a/rust/README.md b/rust/README.md new file mode 100644 index 0000000..d8dff6b --- /dev/null +++ b/rust/README.md @@ -0,0 +1,74 @@ +# 编译器开发 + +编译路线为 Dever → 具体类型 Rust → Cargo/rustc → 原生程序。语法、检查、代码生成与运行库各自负责一层。 + +## 目录 + +| 路径 | 内容 | +| --- | --- | +| `src/syntax/` | 源码位置、词法、语法和诊断 | +| `src/compiler/project/` | 项目、配置与依赖加载 | +| `src/compiler/check/`、`model/` | 类型检查和已检查的程序 | +| `src/compiler/emit/`、`build/` | Rust 生成与原生构建 | +| `src/compiler/inspect/` | 符号、说明、调用关系和流程图 | +| `src/runtime/` | 运行库,复用 Rust 标准库及已有依赖 | +| `src/process/` | 子进程启动、等待与取消 | +| `src/cli/` | 命令行入口 | +| `../lib/`、`../sdk/` | Dever 标准库与外部 Worker SDK | +| `../test/rust/` | 检查器、原生执行、运行库和性能测试 | + +只有本目录及 `src/runtime/` 两份 Cargo 清单。CLI 不单独拆包,生成程序只依赖运行库。 + +## 构建 + +需要 Rust 1.95.0 和 C 编译工具,版本见 `rust-toolchain.toml`。在本目录执行;平台开发使用已有 dev 容器。 + +```sh +cargo +1.95.0 fetch --locked +cargo +1.95.0 build --locked --offline --features postgres --bin dever +compiler="${CARGO_TARGET_DIR:-target}/debug/dever" +"$compiler" new ../target/hello-example +"$compiler" check ../target/hello-example +"$compiler" test ../target/hello-example +"$compiler" run ../target/hello-example -- hello.greeting.greet '{"name":"Dever"}' +``` + +`CARGO_TARGET_DIR` 可指定构建缓存。已有依赖缓存时省略 fetch;锁文件不随日常构建更新。项目和显式输出文件须使用新路径。 + +## 检查与测试 + +```sh +cargo +1.95.0 fmt --all --check +cargo +1.95.0 clippy --locked --offline --workspace --all-targets --all-features --no-deps -- -D warnings +cargo +1.95.0 test --locked --offline --test syntax --test compiler +cargo +1.95.0 test --locked --offline --test native inspection:: +cargo +1.95.0 test --locked --offline --test native toolchain:: +``` + +按改动选择现有测试:运行库测试位于 `../test/rust/runtime/`,应用原生测试位于 `../test/rust/native/`。数据库测试使用独立配置,不能连接业务数据库。测试通过数以真实执行结果为准,ignored 不计为通过。 + +语法和运行行为以 [语言规范](../LANGUAGE.md)及相关测试为准;实现约束见 [AGENTS](../AGENTS.md)。性能测试见 [测试方法](../test/performance/README.md)。 + +## 打包 + +从同一份源码构建发行编译器,再在仓库根组装: + +```sh +cargo +1.95.0 build --locked --offline --release --features postgres --bin dever +compiler="$(realpath "${CARGO_TARGET_DIR:-target}/release/dever")" +cd .. +python3 build/online.py --compiler "$compiler" --work /tmp/dever-online --output target/releases/0.0.1/online +python3 build/development.py --compiler "$compiler" --work /tmp/dever-offline --output target/releases/0.0.1/dever-0.0.1-linux-x86_64-development.tar.xz +``` + +组装环境为 Debian 13,工作目录和输出须不存在。在线包与离线包共用 `build/development.py` 的输入准备;底层 `dever release` 只组装已提供的编译器、源码和清单。 + +脚本保留许可证、锁定依赖及链接所需文件,输出 SHA-256。归档不带构建缓存、用户配置或凭据;只裁剪复制进包的工具,不修改系统安装。 + +发布前在独立目录验证创建、检查、测试、构建和运行。模板、标准库说明嵌入编译器,修改后须重建编译器和安装包。打包不自动上传、安装或切换开发环境。 + +## 目标平台 + +`dever build --target` 支持 `linux-x86_64` 和 `linux-aarch64`。交叉构建须预备目标标准库、链接器和原生依赖;`run/test` 在本机执行。当前安装包只提供已验证的 Linux x86_64 环境。 + +发行构建使用 release、fat LTO、单代码生成单元、溢出检查和符号裁剪。业务程序仍保留 Dever 源码位置诊断。 diff --git a/rust/rust-toolchain.toml b/rust/rust-toolchain.toml new file mode 100644 index 0000000..12dd525 --- /dev/null +++ b/rust/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.95.0" +profile = "minimal" +components = ["rustfmt", "clippy"] diff --git a/rust/src/cli/args/mod.rs b/rust/src/cli/args/mod.rs new file mode 100644 index 0000000..ad29fd8 --- /dev/null +++ b/rust/src/cli/args/mod.rs @@ -0,0 +1,100 @@ +//! 解析候选 CLI 的命令、源码路径和显式入口。 +//! 这里只检查参数形状;源码是否合法由 compiler 判定。 + +use std::path::PathBuf; + +mod tenant; + +pub struct Args { + pub command: String, + pub source: PathBuf, + pub output: Option, + pub entry: String, + pub target: Option, + pub arguments: Vec, +} + +pub fn parse(mut arguments: impl Iterator) -> Result, String> { + let command = arguments.next().unwrap_or_else(|| "help".into()); + if matches!(command.as_str(), "help" | "--help" | "-h") { + println!( + r#"dever new +dever skill path [project-root] +dever skill install +dever check +dever inspect [--symbol package.function] [--depth 1] [--json] +dever inspect --links [--check] +dever api [--output ] +dever emit +dever fmt [--check] +dever test [--filter component.domain.topic] [--timeout-ms 60000] +dever build --output [--entry package.function] [--target linux-x86_64|linux-aarch64] +dever run [--entry package.function] +dever run -- [--tenant ] component.domain.command '' +dever tenant migrate +dever tenant owner +dever tenant component enable|disable +dever package add|update|remove|list|doctor [spec...] +dever package export --output +dever lib install +dever release --output + +Source accepts .dever, a package directory or a project with module/. Descriptions live in doc/. +Core entry defaults to main.main. Applications start HTTP APIs or invoke a CMD. +run reads the project's config/setting.json; build outputs use deployment configuration. +Online installations prepare the build environment in the background; run/test/build wait when needed. +Once prepared, locked dependencies build offline. release packages a prepared local compiler candidate."# + ); + return Ok(None); + } + if command == "--version" { + println!("dever {}", env!("CARGO_PKG_VERSION")); + return Ok(None); + } + if command == "tenant" { + return tenant::parse(arguments).map(Some); + } + if !matches!(command.as_str(), "check" | "api" | "emit" | "build" | "run") { + return Err(format!("unknown command '{command}'")); + } + let source = arguments.next().ok_or("missing source path")?.into(); + let mut output = None; + let mut entry = "main".to_owned(); + let mut forwarded = Vec::new(); + let mut target = None; + while let Some(argument) = arguments.next() { + match argument.as_str() { + "--" if command == "run" => { + forwarded.extend(arguments); + break; + } + "--output" if output.is_none() => { + output = Some(arguments.next().ok_or("missing output path")?.into()) + } + "--entry" => entry = arguments.next().ok_or("missing entry name")?, + "--target" if command == "build" && target.is_none() => { + let value = arguments.next().ok_or("missing build target")?; + target = Some(dever::compiler::build::BuildTarget::parse(&value)?); + } + _ => return Err(format!("unknown or repeated argument '{argument}'")), + } + } + if command == "build" && output.is_none() { + return Err("build requires --output".into()); + } + if !matches!(command.as_str(), "build" | "api") && output.is_some() { + return Err("--output is only valid for build or api".into()); + } + Ok(Some(Args { + command, + source, + output, + entry, + target, + arguments: forwarded, + })) +} + +#[cfg(test)] +#[path = "../../../../test/rust/cli/args.rs"] +mod tests; diff --git a/rust/src/cli/args/tenant.rs b/rust/src/cli/args/tenant.rs new file mode 100644 index 0000000..d576ec6 --- /dev/null +++ b/rust/src/cli/args/tenant.rs @@ -0,0 +1,54 @@ +//! 运维命令复用当前项目生成的原生入口,CLI 不直接打开部署数据库。 + +use super::Args; + +pub(super) fn parse(mut arguments: impl Iterator) -> Result { + let operation = arguments.next().ok_or("missing tenant operation")?; + let action = if operation == "component" { + Some(arguments.next().ok_or("missing component operation")?) + } else { + None + }; + let source = arguments.next().ok_or("missing project root")?.into(); + let tenant = arguments + .next() + .and_then(|value| value.parse::().ok()) + .filter(|value| *value > 0) + .ok_or("tenant requires a positive internal id")?; + let (command, input) = match operation.as_str() { + "migrate" => ("dever.tenant.migrate", "null".to_owned()), + "component" => { + let command = match action.as_deref() { + Some("enable") => "dever.tenant.enable", + Some("disable") => "dever.tenant.disable", + _ => return Err("expected component enable or disable".into()), + }; + let component = arguments.next().ok_or("missing component name")?; + (command, serde_json::to_string(&component).unwrap()) + } + "owner" => { + let site = arguments.next().ok_or("missing owner site")?; + let user = arguments + .next() + .and_then(|value| value.parse::().ok()) + .filter(|value| *value > 0) + .ok_or("owner requires a positive internal user id")?; + ( + "dever.auth.owner", + serde_json::json!({"site": site, "user_id": user}).to_string(), + ) + } + _ => return Err("expected tenant migrate, owner or component".into()), + }; + if arguments.next().is_some() { + return Err("unexpected tenant command argument".into()); + } + Ok(Args { + command: "run".into(), + source, + output: None, + entry: "main".into(), + target: None, + arguments: vec!["--tenant".into(), tenant.to_string(), command.into(), input], + }) +} diff --git a/rust/src/cli/dependency/mod.rs b/rust/src/cli/dependency/mod.rs new file mode 100644 index 0000000..405f8ba --- /dev/null +++ b/rust/src/cli/dependency/mod.rs @@ -0,0 +1,46 @@ +//! 依赖命令只解析参数;解析、缓存和锁的所有权留在 project::dependency。 + +use dever::compiler::project::dependency; +use std::path::Path; + +/// 由主 CLI 在加载 Project 前分派,避免首次 add 被缺失锁阻止。 +pub fn execute(command: &str, arguments: &[String]) -> Result { + let action = arguments.first().ok_or("missing dependency action")?; + let root = arguments.get(1).ok_or("missing project root")?; + let root = Path::new(root); + let specs = &arguments[2..]; + match (command, action.as_str()) { + ("package", "export") => { + if specs.len() != 4 || specs[2] != "--output" { + return Err("dever package export --output ".into()); + } + dependency::export(root, &specs[0], &specs[1], Path::new(&specs[3])) + } + ("package", _) => dependency::execute(action, root, specs), + ("lib", _) => { + let mut target = dever::compiler::toolchain::BuildTarget::host()?; + let mut values = Vec::new(); + let mut arguments = specs.iter(); + let mut selected = false; + while let Some(value) = arguments.next() { + if value == "--target" { + if selected + || !matches!(action.as_str(), "add" | "update" | "install" | "doctor") + { + return Err( + "--target is accepted once for Lib add/update/install/doctor".into(), + ); + } + target = dever::compiler::toolchain::BuildTarget::parse( + arguments.next().ok_or("missing --target value")?, + )?; + selected = true; + } else { + values.push(value.clone()); + } + } + dependency::library::command::execute(action, root, &values, target) + } + _ => Err("expected package or lib command".into()), + } +} diff --git a/rust/src/cli/main.rs b/rust/src/cli/main.rs new file mode 100644 index 0000000..9ef355d --- /dev/null +++ b/rust/src/cli/main.rs @@ -0,0 +1,160 @@ +//! 最小引导 CLI:串联检查、生成 Rust、构建和执行。 +//! run 只管理自己创建的临时目录与直接子进程,不激活全局工具链。 + +mod args; +mod dependency; +mod project; +mod release; +mod tools; + +use dever::compiler::{build, emit, project::Project}; +use std::io::Write; + +fn main() -> std::process::ExitCode { + match execute() { + Ok(code) => std::process::ExitCode::from(code), + Err(error) => { + eprintln!("{error}"); + std::process::ExitCode::FAILURE + } + } +} + +fn execute() -> Result { + let raw = std::env::args().skip(1).collect::>(); + if let Some(result) = tools::execute(&raw) { + return result; + } + if let Some(command) = raw + .first() + .filter(|name| matches!(name.as_str(), "package" | "lib")) + { + println!("{}", dependency::execute(command, &raw[1..])?); + return Ok(0); + } + let Some(arguments) = args::parse(raw.into_iter())? else { + return Ok(0); + }; + let project = Project::load(&arguments.source)?; + if arguments.command == "api" { + let snapshot = project.api_snapshot()?; + if let Some(path) = arguments.output { + let mut file = std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&path) + .map_err(|error| format!("{}: {error}", path.display()))?; + if let Err(error) = file + .write_all(snapshot.as_bytes()) + .and_then(|()| file.sync_all()) + { + let _ = std::fs::remove_file(&path); + return Err(format!("{}: {error}", path.display())); + } + } else { + std::io::stdout() + .lock() + .write_all(snapshot.as_bytes()) + .map_err(|error| error.to_string())?; + } + return Ok(0); + } + let program = project.check()?; + if !program.application() && !arguments.arguments.is_empty() { + return Err("core fixture entries do not accept command arguments".into()); + } + match arguments.command.as_str() { + "check" => println!("checked {} source files", project.sources.len()), + "emit" => { + // 核心库没有固定入口,emit 保留所有函数供检查;应用按 CMD 输出。 + let functions = if !program.application() { + (0..program.functions.len()).collect() + } else { + program.entries().collect() + }; + print!("{}", emit::generate(&program, &project.sources, &functions)); + if program.application() { + print!("{}", emit::application::generate(&program)); + } + } + "build" => { + let binary = build::build_for_target( + &program, + &project.sources, + &arguments.entry, + arguments.output.as_ref().unwrap(), + arguments.target.unwrap_or(build::BuildTarget::host()?), + )?; + println!("{}", binary.display()); + } + "run" => { + let name = format!( + ".dever-run-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_err(|error| error.to_string())? + .as_nanos() + ); + // 应用二进制与持久 data 使用项目根;退出只删本次成功构建的临时文件。 + let directory = if program.application() { + arguments + .source + .canonicalize() + .map_err(|error| error.to_string())? + } else { + let directory = std::env::temp_dir().join(&name); + std::fs::create_dir(&directory).map_err(|error| error.to_string())?; + directory + }; + let destination = directory.join(if program.application() { &name } else { "app" }); + let mut built = false; + let result = (|| { + let binary = + build::build(&program, &project.sources, &arguments.entry, &destination)?; + built = true; + let mut forwarded = Vec::new(); + if program.has_adapters() || program.has_http() || program.has_database() { + let root = arguments + .source + .canonicalize() + .map_err(|error| error.to_string())?; + forwarded.extend([ + "--config".into(), + root.join("config/setting.json") + .to_string_lossy() + .into_owned(), + ]); + } + forwarded.extend(arguments.arguments.iter().cloned()); + let process = dever::process::command::start( + binary.as_os_str(), + &forwarded, + &std::env::current_dir().map_err(|error| error.to_string())?, + false, + ) + .map_err(|error| error.to_string())?; + Ok(process + .wait() + .map_err(|error| error.to_string())? + .code() + .unwrap_or(1) as u8) + })(); + let cleanup = if !program.application() { + std::fs::remove_dir_all(&directory) + } else if built { + std::fs::remove_file(&destination) + } else { + Ok(()) + }; + // 主操作失败优先保留原错误;只有执行成功时才把清理失败向外报告。 + return match (result, cleanup) { + (Err(error), _) => Err(error), + (Ok(0), Err(error)) => Err(error.to_string()), + (Ok(code), _) => Ok(code), + }; + } + _ => unreachable!(), + } + Ok(0) +} diff --git a/rust/src/cli/project/mod.rs b/rust/src/cli/project/mod.rs new file mode 100644 index 0000000..b493a47 --- /dev/null +++ b/rust/src/cli/project/mod.rs @@ -0,0 +1,18 @@ +//! 项目创建入口不读取目标项目配置,也不安装或激活任何工具。 + +use std::path::Path; + +pub fn execute(arguments: &[String]) -> Result { + let mut directory = None; + for argument in arguments { + if !argument.starts_with('-') && directory.is_none() { + directory = Some(argument); + } else { + return Err("usage: dever new ".into()); + } + } + let directory = directory.ok_or("usage: dever new ")?; + dever::compiler::project::create(Path::new(directory))?; + println!("created {directory}"); + Ok(0) +} diff --git a/rust/src/cli/release/main.rs b/rust/src/cli/release/main.rs new file mode 100644 index 0000000..7537166 --- /dev/null +++ b/rust/src/cli/release/main.rs @@ -0,0 +1,15 @@ +//! 私有作者入口:只组装已准备的本地候选包,不签名、不安装、不联网。 + +#[path = "mod.rs"] +mod release; + +fn main() -> std::process::ExitCode { + let arguments = std::env::args().skip(1).collect::>(); + match release::execute(&arguments) { + Ok(code) => std::process::ExitCode::from(code), + Err(error) => { + eprintln!("{error}"); + std::process::ExitCode::FAILURE + } + } +} diff --git a/rust/src/cli/release/mod.rs b/rust/src/cli/release/mod.rs new file mode 100644 index 0000000..1b3d6fe --- /dev/null +++ b/rust/src/cli/release/mod.rs @@ -0,0 +1,19 @@ +//! 两个作者入口共用同一本地打包实现,不附带签名、安装或发布行为。 + +pub fn execute(arguments: &[String]) -> Result { + let [author, flag, output] = arguments else { + return Err("local release requires --output ".into()); + }; + if flag != "--output" { + return Err("local release requires --output ".into()); + } + let manifest = dever::compiler::toolchain::packaging::create( + std::path::Path::new(author), + std::path::Path::new(output), + )?; + println!( + "created unsigned local bootstrap {} for {} at {output}", + manifest.version, manifest.target + ); + Ok(0) +} diff --git a/rust/src/cli/tools/inspect.rs b/rust/src/cli/tools/inspect.rs new file mode 100644 index 0000000..2822c62 --- /dev/null +++ b/rust/src/cli/tools/inspect.rs @@ -0,0 +1,72 @@ +//! 源码查询与领域索引共用同一份检查结果,不参与应用构建。 + +use dever::compiler::project::Project; +use std::io::Write; +use std::path::Path; + +pub(super) fn execute(arguments: &[String]) -> Result { + let source = arguments + .first() + .ok_or("usage: dever inspect [--symbol name] [--depth 1] [--json] | --links [--check]")?; + let mut arguments = arguments[1..].iter(); + let mut symbol = None; + let mut depth = None; + let mut json = false; + let mut links = false; + let mut check = false; + while let Some(argument) = arguments.next() { + match argument.as_str() { + "--symbol" if symbol.is_none() => { + symbol = Some(arguments.next().ok_or("missing symbol name")?) + } + "--depth" if depth.is_none() => { + depth = Some( + arguments + .next() + .ok_or("missing depth")? + .parse::() + .map_err(|_| "depth must be a nonnegative integer")?, + ); + } + "--json" if !json => json = true, + "--links" if !links => links = true, + "--check" if !check => check = true, + _ => return Err(format!("unknown or repeated argument '{argument}'")), + } + } + if depth.is_some() && symbol.is_none() { + return Err("--depth requires --symbol".into()); + } + if check && !links { + return Err("--check requires --links".into()); + } + if links && (symbol.is_some() || depth.is_some() || json) { + return Err("--links cannot be combined with --symbol, --depth or --json".into()); + } + let path = Path::new(source) + .canonicalize() + .map_err(|error| format!("{source}: {error}"))?; + let mut index = Project::load(&path)?.inspect()?; + if links { + if !index.documentation_errors.is_empty() { + return Err(index.documentation_errors.join("\n")); + } + return super::links::execute(&path, index.links(&path)?, check); + } + if let Some(symbol) = symbol { + index.select(symbol, depth.unwrap_or(1))?; + } + let mut output = std::io::stdout().lock(); + if json { + serde_json::to_writer(&mut output, &index).map_err(|error| error.to_string())?; + writeln!(output).map_err(|error| error.to_string())?; + } else { + for error in &index.documentation_errors { + eprintln!("{error}"); + } + output + .write_all(index.text().as_bytes()) + .map_err(|error| error.to_string())?; + } + Ok(0) +} diff --git a/rust/src/cli/tools/links.rs b/rust/src/cli/tools/links.rs new file mode 100644 index 0000000..ac4beaf --- /dev/null +++ b/rust/src/cli/tools/links.rs @@ -0,0 +1,84 @@ +//! 只管理带生成标记的领域 link.md;先核对全部目标,再写入实际变化。 + +use dever::compiler::inspect::LINK_MARKER; +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; + +pub(super) fn execute( + root: &Path, + expected: BTreeMap, + check: bool, +) -> Result { + let count = expected.len(); + let mut existing = BTreeMap::new(); + // 固定两层业务目录,不扫描数据、依赖目录或自行创建新的组织层级。 + for component in directories(&root.join("module"))? { + for domain in directories(&component)? { + let path = domain.join("link.md"); + match fs::read_to_string(&path) { + Ok(text) => { + existing.insert(path, text); + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(error) => return Err(format!("{}: {error}", path.display())), + } + } + } + let mut changed = Vec::new(); + for (path, text) in expected { + let previous = existing.remove(&path); + if previous + .as_ref() + .is_some_and(|value| !value.starts_with(LINK_MARKER)) + { + return Err(format!( + "{}: refusing to overwrite a handwritten link.md", + path.display() + )); + } + if previous.as_ref() != Some(&text) { + changed.push((path, Some(text))); + } + } + for (path, text) in existing { + if text.starts_with(LINK_MARKER) { + changed.push((path, None)); + } + } + for (path, text) in &changed { + let action = if check { + "stale" + } else if let Some(text) = text { + fs::write(path, text).map_err(|error| format!("{}: {error}", path.display()))?; + "updated" + } else { + fs::remove_file(path).map_err(|error| format!("{}: {error}", path.display()))?; + "removed" + }; + println!("{action} {}", path.strip_prefix(root).unwrap().display()); + } + println!( + "checked {count} domain indexes; {} {}", + changed.len(), + if check { "stale" } else { "changed" } + ); + Ok(u8::from(check && !changed.is_empty())) +} + +fn directories(path: &Path) -> Result, String> { + let mut directories = Vec::new(); + for entry in fs::read_dir(path).map_err(|error| format!("{}: {error}", path.display()))? { + let entry = entry.map_err(|error| format!("{}: {error}", path.display()))?; + if entry + .file_type() + .map_err(|error| format!("{}: {error}", entry.path().display()))? + .is_dir() + && !entry.file_name().to_string_lossy().starts_with('.') + { + directories.push(entry.path()); + } + } + directories.sort(); + Ok(directories) +} diff --git a/rust/src/cli/tools/mod.rs b/rust/src/cli/tools/mod.rs new file mode 100644 index 0000000..d17fa17 --- /dev/null +++ b/rust/src/cli/tools/mod.rs @@ -0,0 +1,39 @@ +//! 正式源码工具在普通应用参数解析前分派,输出退出码交回唯一 CLI 入口。 + +use dever::compiler::format; +use std::path::Path; +mod inspect; +mod links; +mod skill; +mod testing; + +pub fn execute(arguments: &[String]) -> Option> { + match arguments.first().map(String::as_str) { + Some("new") => Some(super::project::execute(&arguments[1..])), + Some("release") => Some(super::release::execute(&arguments[1..])), + Some("skill") => Some(skill::execute(&arguments[1..])), + Some("inspect") => Some(inspect::execute(&arguments[1..])), + Some("fmt") => Some(format(arguments)), + Some("test") => Some(testing::execute(arguments)), + _ => None, + } +} + +fn format(arguments: &[String]) -> Result { + let source = arguments.get(1).ok_or("fmt requires a source path")?; + let check = match &arguments[2..] { + [] => false, + [flag] if flag == "--check" => true, + _ => return Err("usage: dever fmt [--check]".into()), + }; + let report = format::project(Path::new(source), check)?; + for path in &report.changed { + println!( + "{} {}", + if check { "unformatted" } else { "formatted" }, + path.display() + ); + } + println!("checked {} source files", report.checked); + Ok(u8::from(check && !report.changed.is_empty())) +} diff --git a/rust/src/cli/tools/skill.rs b/rust/src/cli/tools/skill.rs new file mode 100644 index 0000000..e340235 --- /dev/null +++ b/rust/src/cli/tools/skill.rs @@ -0,0 +1,35 @@ +//! skill 只解析当前候选的规则并更新调用者项目,不操作全局安装。 + +use dever::compiler::toolchain::skill; +use std::path::Path; + +pub(super) fn execute(arguments: &[String]) -> Result { + match arguments { + [command] if command == "path" => { + let directory = std::env::current_dir().map_err(|error| error.to_string())?; + let project = match std::fs::symlink_metadata(directory.join("module")) { + Ok(_) => Some(directory.as_path()), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => None, + Err(error) => return Err(error.to_string()), + }; + println!("{}", skill::path(project)?.display()); + } + [command, project] if command == "path" => { + println!("{}", skill::path(Some(Path::new(project)))?.display()); + } + [command] if command == "install" => { + let directory = std::env::current_dir().map_err(|error| error.to_string())?; + let changed = skill::install(&directory)?; + println!( + "{}", + if changed { + "installed Dever guidance in AGENTS.md" + } else { + "Dever guidance is current" + } + ); + } + _ => return Err("usage: dever skill path [project-root] | dever skill install".into()), + } + Ok(0) +} diff --git a/rust/src/cli/tools/testing.rs b/rust/src/cli/tools/testing.rs new file mode 100644 index 0000000..b870e3d --- /dev/null +++ b/rust/src/cli/tools/testing.rs @@ -0,0 +1,60 @@ +//! Test 参数独立校验;过滤发生在全源码检查之后、编译之前。 + +use dever::compiler::testing::{self, Suite}; +use std::path::Path; +use std::time::Duration; + +pub(super) fn execute(arguments: &[String]) -> Result { + let root = arguments.get(1).ok_or("test requires a project root")?; + let mut filter = None; + let mut timeout = None; + let mut options = arguments[2..].iter(); + while let Some(option) = options.next() { + match option.as_str() { + "--filter" if filter.is_none() => { + filter = Some(options.next().ok_or("missing test filter")?.as_str()) + } + "--timeout-ms" if timeout.is_none() => { + let text = options.next().ok_or("missing test timeout")?; + if text.is_empty() || !text.bytes().all(|value| value.is_ascii_digit()) { + return Err("test timeout must be positive integer milliseconds".into()); + } + let value: u64 = text.parse().map_err(|_| "invalid test timeout")?; + if value == 0 { + return Err("test timeout must be positive".into()); + } + timeout = Some(Duration::from_millis(value)); + } + _ => return Err(format!("unknown or repeated test argument '{option}'")), + } + } + let suite = Suite::load(Path::new(root))?; + let report = testing::run(&suite, filter, timeout.unwrap_or(Duration::from_secs(60)))?; + for case in &report.cases { + println!( + "{} {} ({:.3}s)", + if case.timed_out { + "TIMEOUT" + } else if case.success { + "PASS" + } else { + "FAIL" + }, + case.name, + case.elapsed.as_secs_f64() + ); + if !case.stdout.is_empty() { + println!("stdout [{}]:\n{}", case.name, case.stdout); + } + if !case.stderr.is_empty() { + eprintln!("stderr [{}]:\n{}", case.name, case.stderr); + } + } + let passed = report.cases.iter().filter(|case| case.success).count(); + println!( + "{passed} passed; {} failed; {} not run", + report.cases.len() - passed, + report.not_run + ); + Ok(u8::from(!report.success())) +} diff --git a/rust/src/compiler/build/mod.rs b/rust/src/compiler/build/mod.rs new file mode 100644 index 0000000..e4e802d --- /dev/null +++ b/rust/src/compiler/build/mod.rs @@ -0,0 +1,265 @@ +//! 以生成内容标识构建缓存,并输出独立的原生程序。 +//! 构建锁保护同一缓存;用户指定的输出必须新建,不能覆盖已有文件。 + +mod resources; +mod toolchain; +pub use resources::Resource; + +pub use crate::compiler::toolchain::BuildTarget; +use crate::compiler::{ + emit, + model::{Program, usage::Usage}, +}; +use crate::syntax::source::Source; +use std::collections::BTreeSet; +use std::hash::{Hash, Hasher}; +use std::io::{Read, Seek}; +use std::path::{Path, PathBuf}; + +pub fn build( + program: &Program, + sources: &[Source], + entry: &str, + output: &Path, +) -> Result { + build_for_target(program, sources, entry, output, BuildTarget::host()?) +} + +/// 显式目标进入缓存身份和 rustc 参数;跨平台 Worker 尚未准备时直接报错。 +pub fn build_for_target( + program: &Program, + sources: &[Source], + entry: &str, + output: &Path, + target: BuildTarget, +) -> Result { + let resources = crate::compiler::project::dependency::worker::prepare(program, target)?; + build_with_resources(program, sources, entry, output, target, &resources) +} + +/// 已验证资源的发行消费与自有夹具共用同一生成、缓存和目标校验路径。 +pub fn build_with_resources( + program: &Program, + sources: &[Source], + entry: &str, + output: &Path, + target: BuildTarget, + resources: &[Resource], +) -> Result { + let (roots, main) = if program.application() { + if entry != "main" { + return Err("application entries come from API/CMD declarations, not --entry".into()); + } + ( + program.entries().collect::>(), + emit::application::generate(program), + ) + } else { + let (package, name) = entry.rsplit_once('.').unwrap_or(("main", entry)); + let index = program + .functions + .iter() + .position(|function| { + program.packages[function.signature.source] == package + && function.signature.name == name + && function.signature.inputs.is_empty() + && function.signature.static_parameters == 0 + }) + .ok_or_else(|| format!("no zero-input function '{entry}'"))?; + if program.functions[index] + .signature + .outputs + .iter() + .any(|(_, kind)| kind.contains_secret(&program.definitions)) + { + return Err("entry output contains Secret and cannot be rendered".into()); + } + if program.functions[index].signature.effects.database { + return Err("Model execution requires an application entry".into()); + } + if program.functions[index].signature.effects.request { + return Err("request context requires an application HTTP entry".into()); + } + let call = emit::task::entry( + &format!("function{index}"), + Vec::new(), + program.functions[index].signature.effects, + program.has_workers(program.reachable([index])), + ); + ( + [index].into(), + format!( + "fn main() {{ match {call} {{ Ok(value) => println!(\"{{:?}}\", value), Err(error) => {{ eprintln!(\"{{error}}\"); std::process::exit(1); }} }} }}\n" + ), + ) + }; + let functions = program.reachable(roots.iter().copied()); + let mut source = emit::generate(program, sources, &roots); + source.push_str(&main); + if program.external_workers().next().is_some() && resources.is_empty() { + return Err("external Adapter requires prepared Worker resources".into()); + } + resources::source(resources, &mut source)?; + let features = runtime_features(program, &functions); + compile_for_target(&source, &features, output, false, target, resources) +} + +/// 源码测试与正式构建共享缓存、离线工具链和不可覆盖输出;测试只改变优化配置。 +pub(crate) fn compile( + source: &str, + features: &[&str], + output: &Path, + testing: bool, +) -> Result { + compile_for_target(source, features, output, testing, BuildTarget::host()?, &[]) +} + +fn compile_for_target( + source: &str, + features: &[&str], + output: &Path, + testing: bool, + target: BuildTarget, + resources: &[Resource], +) -> Result { + let rust_root = crate::compiler::toolchain::rust_root()?; + toolchain::prepare(&rust_root)?; + toolchain::require_target(&rust_root, target)?; + let profile = if testing { + "opt-level = 1\nlto = false\ncodegen-units = 16\noverflow-checks = true\nstrip = \"symbols\"" + } else { + "lto = \"fat\"\ncodegen-units = 1\noverflow-checks = true\nstrip = \"symbols\"" + }; + let mut hash = std::collections::hash_map::DefaultHasher::new(); + source.hash(&mut hash); + // 相同 Rust 源码可选择不同数据库驱动;依赖特性也是生成项目的身份。 + features.hash(&mut hash); + // Cargo 清单属于构建输入;优化或符号配置变化时不能复用旧清单目录。 + profile.hash(&mut hash); + target.hash(&mut hash); + // 相对依赖使已使用过的开发包也能移动;路径布局参与旧缓存的区分。 + let runtime_path = "../../../src/runtime"; + runtime_path.hash(&mut hash); + let name = format!("dever-program-{:016x}", hash.finish()); + let directory = rust_root.join("target/generated").join(&name); + std::fs::create_dir_all(&directory).map_err(|error| error.to_string())?; + let lock = std::fs::OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(directory.join("build.lock")) + .map_err(|error| error.to_string())?; + // 持有锁直到构建并复制产物完成,避免同一生成项目被并发改写。 + lock.lock().map_err(|error| error.to_string())?; + resources::stage(&directory, resources)?; + let features = format!(", features = {features:?}"); + let manifest = format!( + "[package]\nname = {name:?}\nversion = \"0.0.1\"\nedition = \"2024\"\n[workspace]\n[[bin]]\nname = {name:?}\npath = \"main.rs\"\n[dependencies]\ndever-runtime = {{ path = {:?}{features} }}\n[profile.release]\n{profile}\n", + runtime_path + ); + write_source(&directory.join("main.rs"), source)?; + write_source(&directory.join("Cargo.toml"), &manifest)?; + let binary = toolchain::compile( + &rust_root, + &directory, + &rust_root.join("target/programs"), + &name, + target, + )?; + let mut binary = std::fs::File::open(&binary).map_err(|error| error.to_string())?; + let mut header = [0; 64]; + binary + .read_exact(&mut header) + .map_err(|error| error.to_string())?; + target.validate_executable(&header)?; + binary.rewind().map_err(|error| error.to_string())?; + let permissions = binary + .metadata() + .map_err(|error| error.to_string())? + .permissions(); + let mut destination = std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(output) + .map_err(|error| format!("{}: {error}", output.display()))?; + let result = std::io::copy(&mut binary, &mut destination) + .and_then(|_| destination.set_permissions(permissions)) + .and_then(|()| destination.sync_all()); + if let Err(error) = result { + let _ = std::fs::remove_file(output); + return Err(error.to_string()); + } + output.canonicalize().map_err(|error| error.to_string()) +} + +/// 按实际输出的函数和类型选择依赖;类型引用本身不会启动运行时。 +pub fn runtime_features(program: &Program, functions: &BTreeSet) -> Vec<&'static str> { + let mut features = Vec::new(); + if program.has_workers(functions.iter().copied()) { + features.push("worker"); + } + if !program.jobs.is_empty() { + features.push("job"); + } + if program.has_roles() { + features.push("authorization"); + } + if program.has_auth() { + features.push("auth"); + } + if program.has_http() { + features.push("api"); + } + if !program.adapters.ports.is_empty() { + features.push("adapter"); + } + let usage = Usage::new(program, functions); + if usage.upload { + features.push("upload"); + } + if program.has_database() || usage.database { + features.push("database"); + if cfg!(feature = "postgres") { + features.push("postgres"); + } + } + if program.application() || usage.json { + features.push("wire"); + } + if usage.crypto { + features.push("crypto"); + } else if usage.secret { + features.push("secret"); + } + if usage.uuid { + features.push("uuid"); + } + if usage.calendar { + features.push("calendar"); + } + if program.has_http() || usage.http { + features.push("http"); + } else if usage.tls { + features.push("tls"); + } else if usage.net { + features.push("net"); + } else if usage.task { + features.push("task"); + } + features +} + +fn write_source(path: &Path, contents: &str) -> Result<(), String> { + match std::fs::read_to_string(path) { + Ok(previous) if previous == contents => Ok(()), + Ok(_) => Err(format!( + "generated source identity collision: {}", + path.display() + )), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + std::fs::write(path, contents).map_err(|error| error.to_string()) + } + Err(error) => Err(error.to_string()), + } +} diff --git a/rust/src/compiler/build/resources.rs b/rust/src/compiler/build/resources.rs new file mode 100644 index 0000000..e9d4937 --- /dev/null +++ b/rust/src/compiler/build/resources.rs @@ -0,0 +1,76 @@ +//! 资源是独立二进制输入;源码仅引用固定名称,摘要和模式进入生成内容身份。 + +use sha2::{Digest, Sha256}; +use std::{collections::BTreeSet, fmt::Write, io::Write as IoWrite, path::Path}; + +#[derive(Clone)] +pub struct Resource { + pub path: String, + pub bytes: Vec, + pub executable: bool, +} + +pub(super) fn source(resources: &[Resource], output: &mut String) -> Result<(), String> { + if resources.is_empty() { + return Ok(()); + } + if resources.len() > 4096 { + return Err("Worker resource count exceeds 4096".into()); + } + let mut paths = BTreeSet::new(); + let mut total = 0usize; + output.push_str("fn worker_assets() -> &'static dever_runtime::worker::Bundle {\nstatic FILES: &[dever_runtime::worker::Asset] = &[\n"); + for (index, resource) in resources.iter().enumerate() { + if resource.path.is_empty() + || resource.path.contains(['\\', ':', '\0']) + || resource + .path + .split('/') + .any(|part| matches!(part, "" | "." | "..")) + || !paths.insert(&resource.path) + { + return Err("invalid or duplicate Worker resource path".into()); + } + total = total + .checked_add(resource.bytes.len()) + .ok_or("Worker resources exceed size limit")?; + if total > 256 * 1024 * 1024 || resource.bytes.len() > 128 * 1024 * 1024 { + return Err("Worker resources exceed size limit".into()); + } + let sha256 = format!("{:x}", Sha256::digest(&resource.bytes)); + writeln!(output, "dever_runtime::worker::Asset {{ path: {:?}, bytes: include_bytes!(\"resource{index}\"), sha256: {sha256:?}, executable: {} }},", resource.path, resource.executable).unwrap(); + } + output.push_str("];\nstatic BUNDLE: dever_runtime::worker::Bundle = dever_runtime::worker::Bundle { files: FILES }; &BUNDLE }\n"); + Ok(()) +} + +pub(super) fn stage(directory: &Path, resources: &[Resource]) -> Result<(), String> { + for (index, resource) in resources.iter().enumerate() { + let path = directory.join(format!("resource{index}")); + let mut options = std::fs::OpenOptions::new(); + options.write(true).create_new(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o600); + } + match options.open(&path) { + Ok(mut file) => file + .write_all(&resource.bytes) + .and_then(|()| file.sync_all()) + .map_err(|error| error.to_string())?, + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => { + let metadata = + std::fs::symlink_metadata(&path).map_err(|error| error.to_string())?; + if !metadata.is_file() + || metadata.len() != resource.bytes.len() as u64 + || std::fs::read(&path).map_err(|error| error.to_string())? != resource.bytes + { + return Err("existing compiler resource input differs".into()); + } + } + Err(error) => return Err(error.to_string()), + } + } + Ok(()) +} diff --git a/rust/src/compiler/build/toolchain.rs b/rust/src/compiler/build/toolchain.rs new file mode 100644 index 0000000..99ad65c --- /dev/null +++ b/rust/src/compiler/build/toolchain.rs @@ -0,0 +1,106 @@ +//! 通过独立 process 模块调用固定版本的官方 Cargo/rustc。 +//! 保持离线 release 构建,失败直接返回实际退出状态与诊断。 + +use crate::compiler::toolchain::{BuildTarget, RUST_VERSION}; +use crate::process::{child::output::require_success, command}; +use std::ffi::OsString; +use std::path::{Path, PathBuf}; + +/// 在线安装与编译共用准备脚本和文件锁;就绪后的构建不会联网。 +pub(super) fn prepare(rust_root: &Path) -> Result<(), String> { + let tools = rust_root.parent().unwrap().join("toolchain"); + if !tools.join("online").is_file() || tools.join("ready").is_file() { + return Ok(()); + } + let child = command::start("bash".as_ref(), &[tools.join("prepare")], rust_root, false) + .map_err(|error| format!("cannot prepare Dever build environment: {error}"))?; + let status = child.wait().map_err(|error| error.to_string())?; + if !status.success() { + return Err(format!( + "Dever environment preparation failed ({status}). Details: {}. Run the command again to resume.", + tools.join("prepare.log").display() + )); + } + Ok(()) +} + +pub(super) fn compile( + rust_root: &Path, + project: &Path, + target: &Path, + name: &str, + platform: BuildTarget, +) -> Result { + let (program, mut arguments) = executable(rust_root, "cargo"); + arguments.extend([ + OsString::from("build"), + OsString::from("--offline"), + OsString::from("--release"), + OsString::from("--target-dir"), + target.as_os_str().to_owned(), + ]); + // 本机保留既有 Cargo 缓存;交叉目标使用 Cargo 自带的独立目标目录。 + let cross = platform != BuildTarget::host()?; + if cross { + arguments.extend([ + OsString::from("--target"), + OsString::from(platform.triple()), + ]); + } + let child = + command::start(&program, &arguments, project, true).map_err(|error| error.to_string())?; + let output = child.output().map_err(|error| error.to_string())?; + if !output.status.success() { + let log = project.join("build.log"); + std::fs::write(&log, &output.stderr).map_err(|error| error.to_string())?; + return Err(format!( + "Dever build failed ({}). Details: {}", + output.status, + log.display() + )); + } + let directory = if cross { + target.join(platform.triple()) + } else { + target.to_path_buf() + }; + Ok(directory.join("release").join(name)) +} + +pub(super) fn require_target(rust_root: &Path, target: BuildTarget) -> Result<(), String> { + if target == BuildTarget::host()? { + return Ok(()); + } + let (program, mut arguments) = executable(rust_root, "rustc"); + arguments.extend([ + "--print".into(), + "target-libdir".into(), + "--target".into(), + target.triple().into(), + ]); + let directory = std::env::current_dir().map_err(|error| error.to_string())?; + let child = command::start(&program, &arguments, &directory, true) + .map_err(|error| error.to_string())?; + let output = child.output().map_err(|error| error.to_string())?; + let output = require_success(output)?; + let location = std::str::from_utf8(&output) + .map_err(|error| error.to_string())? + .trim(); + if !Path::new(location).is_dir() { + return Err(format!( + "{} Rust standard library is not prepared for {RUST_VERSION}; cross builds require an offline target sysroot and linker", + target.name() + )); + } + Ok(()) +} + +/// 开发包直接使用私有工具链;源码作者仍使用 rustup 管理的固定版本。 +fn executable(rust_root: &Path, name: &str) -> (OsString, Vec) { + let tools = rust_root.parent().unwrap().join("toolchain"); + if tools.is_dir() { + (tools.join("bin").join(name).into_os_string(), Vec::new()) + } else { + (name.into(), vec![format!("+{RUST_VERSION}").into()]) + } +} diff --git a/rust/src/compiler/check/api.rs b/rust/src/compiler/check/api.rs new file mode 100644 index 0000000..c958430 --- /dev/null +++ b/rust/src/compiler/check/api.rs @@ -0,0 +1,53 @@ +//! 将已验证声明交给快照;模板保留参数合同,范围使用声明而非函数体推导结果。 + +use super::handler::Expansion; +use crate::compiler::model::{Signature, api, package}; +use crate::syntax::ast; +use std::collections::BTreeSet; + +pub(super) fn exports( + expansion: &Expansion, + signatures: &[Signature], + declarations: &[ast::TypeDeclaration], + packages: &[String], +) -> api::Exports { + let functions = expansion + .exports + .iter() + .map(|export| { + let mut signature = signatures[export.index].clone(); + signature + .output_ranges + .clone_from(&expansion.instances[export.index].signature.output_ranges); + let mut recoveries = BTreeSet::new(); + let mut pending = vec![export.index]; + let mut visited = BTreeSet::new(); + while let Some(index) = pending.pop() { + if !visited.insert(index) { + continue; + } + if let Some(reason) = &signatures[index].recovery { + recoveries.insert(reason.clone()); + } + pending.extend(expansion.instances[index].scope.targets.values().copied()); + } + api::Function { + signature, + handlers: export.handlers.clone(), + recoveries, + } + }) + .collect(); + let definitions = declarations + .iter() + .enumerate() + .filter(|(_, declaration)| { + declaration.public && !package::standard(&packages[declaration.span.source]) + }) + .map(|(index, _)| index) + .collect(); + api::Exports { + functions, + definitions, + } +} diff --git a/rust/src/compiler/check/application/http.rs b/rust/src/compiler/check/application/http.rs new file mode 100644 index 0000000..2354211 --- /dev/null +++ b/rust/src/compiler/check/application/http.rs @@ -0,0 +1,186 @@ +//! HTTP 声明复用 App 签名和 wire 合同;查询输入仅允许标量。 + +use super::*; + +pub(in crate::compiler::check) fn check( + entries: &[ast::HttpEntry], + signatures: &[Signature], + definitions: &[model::Definition], + names: &Types, + verifiers: &[model::auth::Verifier], +) -> Result, Diagnostic> { + let mut result = Vec::new(); + let mut seen = HashSet::new(); + for entry in entries { + let span = entry.span; + if !entry.public && verifiers.is_empty() { + return Err(Diagnostic::new( + "C014", + "authenticated HTTP requires a compiled App verify function", + span, + )); + } + let command = ast::Command { + name: entry.name.clone(), + target: entry.target.clone(), + long: entry.long, + span, + }; + let checked = commands(&[command], signatures, definitions, names, true)?; + let function = checked[0].function; + let signature = &signatures[function]; + if signature + .inputs + .iter() + .any(|(_, kind)| *kind == Type::Upload) + { + if entry.method != "post" + || signature + .inputs + .iter() + .filter(|(_, kind)| *kind == Type::Upload) + .count() + != 1 + { + return Err(Diagnostic::new( + "C013", + "Upload requires one top-level POST input", + span, + )); + } + for (_, kind) in &signature.inputs { + let inner = if let Type::Optional(inner) = kind { + inner.as_ref() + } else { + kind + }; + if !matches!( + inner, + Type::Upload + | Type::Bool + | Type::Int + | Type::Float + | Type::Decimal + | Type::Text + | Type::Uuid + | Type::Id + | Type::ModelId(_) + | Type::DateTime + | Type::Date + | Type::Time + | Type::Duration + ) && !matches!(inner, Type::Named(index) if matches!(definitions[*index].shape, Shape::Choice(_))) + { + return Err(Diagnostic::new( + "C013", + "multipart inputs must be Upload and scalars", + span, + )); + } + } + } + if (entry.public || entry.authenticated) && signature.effects.enqueue { + return Err(Diagnostic::new( + "C014", + "HTTP enqueue requires a protected permission-bound entry", + span, + )); + } + if (entry.public || entry.authenticated || entry.method == "get") + && signature.effects.authorization_write + { + return Err(Diagnostic::new( + "C014", + "authorization writes require a protected write API", + span, + )); + } + if entry.public && signature.effects.identity { + return Err(Diagnostic::new( + "C014", + "public HTTP cannot read trusted identity", + span, + )); + } + if signature.effects.request && signature.effects.blocking { + return Err(Diagnostic::new( + "C014", + "request context cannot move to a blocking worker", + span, + )); + } + if entry.method == "get" && signature.effects.database_write { + return Err(Diagnostic::new("C014", "GET cannot write to a Model", span)); + } + if matches!(entry.method.as_str(), "get" | "delete") { + for (_, kind) in &signature.inputs { + let inner = if let Type::Optional(inner) = kind { + inner + } else { + kind + }; + if !matches!( + inner, + Type::Bool + | Type::Int + | Type::Float + | Type::Decimal + | Type::Text + | Type::Uuid + | Type::Id + | Type::ModelId(_) + | Type::DateTime + | Type::Date + | Type::Time + | Type::Duration + ) && !matches!(inner, Type::Named(index) if matches!(definitions[*index].shape, Shape::Choice(_))) + { + return Err(Diagnostic::new( + "C005", + "HTTP query inputs must be scalar", + span, + )); + } + } + } + let identity = &names.packages[span.source]; + let (domain, _) = package::role(identity).unwrap(); + let topic = identity + .strip_prefix(&format!("{domain}.api.")) + .unwrap_or("") + .replace('.', "/"); + let path = format!( + "/{}/{}{}", + domain.replace('.', "/"), + if topic.is_empty() { + String::new() + } else { + format!("{topic}/") + }, + entry.name + ); + let method = entry.method.to_ascii_uppercase(); + if !seen.insert((method.clone(), path.clone())) { + return Err(Diagnostic::new( + "C002", + "duplicate HTTP method and path", + span, + )); + } + result.push(model::HttpEntry { + access: if entry.public { + model::auth::Access::Public + } else if entry.authenticated { + model::auth::Access::Authenticated + } else { + model::auth::Access::Protected + }, + method, + path, + topic, + function, + long: entry.long, + }); + } + Ok(result) +} diff --git a/rust/src/compiler/check/application/mod.rs b/rust/src/compiler/check/application/mod.rs new file mode 100644 index 0000000..f5db463 --- /dev/null +++ b/rust/src/compiler/check/application/mod.rs @@ -0,0 +1,383 @@ +//! 检查应用职责、CMD 绑定和可静态编码的边界类型。 +//! 入口只绑定本领域 App,所有业务参数在调用前完成严格解码。 + +use super::{names, package::Types}; +use crate::compiler::model::{self, Shape, Signature, Type, package}; +use crate::syntax::{ast, diagnostic::Diagnostic, source::Span}; +use std::collections::HashSet; + +pub(super) mod http; +pub(super) mod port; +pub(super) mod rest; + +pub(super) fn reachable( + entries: impl Iterator, + expansion: &super::handler::Expansion, + packages: &[String], +) -> Result<(), Diagnostic> { + let mut pending = entries.collect::>(); + if pending.is_empty() { + return Ok(()); + } + // 用展开后的静态调用边确认可达性;原型检查实例不能使死代码变成生产入口。 + let mut visited = HashSet::new(); + let mut declarations = HashSet::new(); + while let Some(index) = pending.pop() { + if !visited.insert(index) { + continue; + } + let instance = &expansion.instances[index]; + declarations.insert((instance.span.source, instance.span.start)); + pending.extend(&expansion.calls[index]); + } + for instance in &expansion.instances { + if !instance.prototype + && matches!( + package::role(&packages[instance.span.source]), + Some((_, "app" | "domain")) + ) + && !declarations.contains(&(instance.span.source, instance.span.start)) + { + return Err(Diagnostic::new( + "C016", + "application function is unreachable from production entries", + instance.span, + )); + } + } + Ok(()) +} + +pub(super) fn prepare(modules: &mut [ast::Module], packages: &[String]) -> Result<(), Diagnostic> { + for module in modules { + for declaration in &mut module.declarations { + let span = declaration.span(); + let role = package::role(&packages[span.source]).map(|(_, role)| role); + match declaration { + ast::Declaration::External(_) => { + if role != Some("adapter") { + return Err(Diagnostic::new( + "C006", + "external requires an Adapter source", + span, + )); + } + } + ast::Declaration::Persistence(_) => { + if role != Some("model") { + return Err(Diagnostic::new( + "C006", + "storage declarations require a Model source", + span, + )); + } + } + ast::Declaration::Rest(_) => { + if role != Some("api") { + return Err(Diagnostic::new( + "C006", + "REST declarations require an API source", + span, + )); + } + } + ast::Declaration::Job(value) => { + if role != Some("job") { + return Err(Diagnostic::new( + "C006", + "Job declarations require a Job source", + span, + )); + } + super::job::prepare(value)?; + } + ast::Declaration::Schedule(value) => { + names::identifier(&value.target, false, span)?; + if role != Some("job") { + return Err(Diagnostic::new( + "C006", + "schedule declarations require a Job source", + span, + )); + } + } + ast::Declaration::Database(name, _) => { + names::identifier(name, false, span)?; + if !matches!(role, Some("model" | "job")) { + return Err(Diagnostic::new( + "C006", + "database binding requires a Model source", + span, + )); + } + } + ast::Declaration::Http(value) => { + names::identifier(&value.name, false, span)?; + if role != Some("api") { + return Err(Diagnostic::new( + "C006", + "HTTP declarations require an API source", + span, + )); + } + } + ast::Declaration::Command(value) => { + names::identifier(&value.name, false, span)?; + if role != Some("api") { + return Err(Diagnostic::new( + "C006", + "CMD declarations require an API source", + span, + )); + } + } + ast::Declaration::Type(value) => { + if value.global && role != Some("model") { + return Err(Diagnostic::new( + "C006", + "global types belong to Model", + span, + )); + } + if value.setting && (role != Some("adapter") || value.public) { + return Err(Diagnostic::new( + "C006", + "setting belongs to an Adapter", + span, + )); + } + declaration_role(role, value.public, span)?; + value.public |= role == Some("app") + || (role == Some("model") + && (matches!(value.body, ast::TypeBody::Choice(_)) + || package::model_record(&packages[span.source]).as_deref() + == Some(value.name.as_str()))); + } + ast::Declaration::Function(value) => { + if role == Some("model") || (value.transaction && role != Some("app")) { + return Err(Diagnostic::new( + "C006", + "Model declares storage; transaction functions belong to App", + span, + )); + } + declaration_role(role, value.public, span)?; + if role == Some("adapter") + && (value.inputs.iter().any(|input| input.name == "setting") + || value.outputs.iter().any(|field| field.name == "setting")) + { + return Err(Diagnostic::new( + "C005", + "Adapter parameters cannot shadow setting", + span, + )); + } + if (role == Some("port")) != value.contract { + return Err(Diagnostic::new( + "C006", + "only Port declares bodyless operations with fails Choice", + span, + )); + } + value.public |= role == Some("app"); + } + } + } + } + Ok(()) +} + +fn declaration_role(role: Option<&str>, public: bool, span: Span) -> Result<(), Diagnostic> { + match role { + Some("app" | "domain" | "port" | "adapter" | "model" | "test") if public => Err( + Diagnostic::new("C006", "application declarations do not use public", span), + ), + Some("api") => Err(Diagnostic::new( + "C006", + "API sources declare entries, not types or functions", + span, + )), + Some("app" | "domain" | "port" | "adapter" | "model" | "test") | None => Ok(()), + Some(_) => Err(Diagnostic::new( + "U001", + "this application role is not implemented yet", + span, + )), + } +} + +pub(super) fn commands( + declarations: &[ast::Command], + signatures: &[Signature], + definitions: &[model::Definition], + names: &Types, + http: bool, +) -> Result, Diagnostic> { + let mut commands = Vec::new(); + let mut seen = HashSet::new(); + for declaration in declarations { + let span = declaration.span; + let (domain, _) = package::role(&names.packages[span.source]).unwrap(); + let function_name = declaration + .target + .strip_prefix("app.") + .filter(|name| !name.contains('.')) + .ok_or_else(|| { + Diagnostic::new("C006", "CMD must bind app.function in its own domain", span) + })?; + let matching = signatures + .iter() + .enumerate() + .filter(|(_, signature)| { + package::namespace(&names.packages[signature.source]) == domain + && signature.name == function_name + }) + .collect::>(); + let [(function, signature)] = matching.as_slice() else { + return Err(Diagnostic::new( + "C005", + "CMD target must resolve to one App signature", + span, + )); + }; + if !http && signature.effects.request { + return Err(Diagnostic::new( + "C014", + "CMD cannot access HTTP request context", + span, + )); + } + if declaration.long && signature.effects.unscoped_write { + return Err(Diagnostic::new( + "C014", + "long entry writes require an explicit transaction function", + span, + )); + } + if !declaration.long + && signature.effects.database_write + && signature.effects.transaction_unsafe() + { + return Err(Diagnostic::new( + "C014", + "writing entries cannot mix an automatic transaction with external or concurrent work", + span, + )); + } + if signature.static_parameters != 0 { + return Err(Diagnostic::new( + "C005", + "CMD cannot expose handler parameters", + span, + )); + } + for (_, kind) in &signature.inputs { + if http && *kind == Type::Upload { + continue; + } + wire_input(kind, definitions, span, true)?; + } + for (_, kind) in &signature.outputs { + wire_type(kind, definitions, span)?; + } + let name = format!("{domain}.{}", declaration.name); + if !seen.insert(name.clone()) { + return Err(Diagnostic::new( + "C002", + "duplicate application command", + span, + )); + } + commands.push(model::Command { + name, + function: *function, + long: declaration.long, + }); + } + Ok(commands) +} + +pub(super) fn wire_type( + kind: &Type, + definitions: &[model::Definition], + span: Span, +) -> Result<(), Diagnostic> { + wire_input(kind, definitions, span, false) +} + +fn wire_input( + kind: &Type, + definitions: &[model::Definition], + span: Span, + secret: bool, +) -> Result<(), Diagnostic> { + wire_inner(kind, definitions, span, secret, &mut HashSet::new()) +} + +fn wire_inner( + kind: &Type, + definitions: &[model::Definition], + span: Span, + secret: bool, + seen: &mut HashSet, +) -> Result<(), Diagnostic> { + match kind { + Type::Secret if secret => Ok(()), + Type::Bool + | Type::Int + | Type::Float + | Type::Decimal + | Type::Json + | Type::Text + | Type::Uuid + | Type::Id + | Type::ModelId(_) + | Type::DateTime + | Type::Date + | Type::Time + | Type::Duration => Ok(()), + Type::List(inner) | Type::Optional(inner) => { + wire_inner(inner, definitions, span, secret, seen) + } + Type::Named(index) if !seen.insert(*index) => Ok(()), + Type::Named(index) => match &definitions[*index].shape { + Shape::Record(fields) => { + if !definitions[*index].private_fields.is_empty() { + return Err(Diagnostic::new( + "C006", + "private fields cannot cross a command wire boundary", + span, + )); + } + if definitions[*index].ranges.iter().any(Option::is_some) { + return Err(Diagnostic::new( + "C011", + "constrained records cannot cross a command wire boundary", + span, + )); + } + for (_, kind) in fields { + wire_inner(kind, definitions, span, secret, seen)?; + } + Ok(()) + } + Shape::Choice(variants) + if variants + .iter() + .all(|variant| !variant.error && variant.fields.is_empty()) => + { + Ok(()) + } + Shape::Choice(_) => Err(Diagnostic::new( + "C005", + "only payload-free non-error Choice can cross a wire boundary", + span, + )), + }, + _ => Err(Diagnostic::new( + "U001", + "this command wire type is not implemented yet", + span, + )), + } +} diff --git a/rust/src/compiler/check/application/port/contract.rs b/rust/src/compiler/check/application/port/contract.rs new file mode 100644 index 0000000..ce1a948 --- /dev/null +++ b/rust/src/compiler/check/application/port/contract.rs @@ -0,0 +1,102 @@ +//! 检查 Port 的值签名和公开错误集合,沿用普通类型解析与合同规则。 + +use super::super::super::{names, package::Types, signature}; +use crate::compiler::model::{Definition, Failure, Shape, Signature, Type, package}; +use crate::syntax::{ast, diagnostic::Diagnostic}; + +pub(super) fn plain(declaration: &ast::Function) -> Result<(), Diagnostic> { + if declaration.pure + || declaration.recovery.is_some() + || declaration.inputs.iter().any( + |input| !matches!(&input.pattern, ast::Pattern::Typed(_, bounds) if bounds.is_empty()), + ) + || declaration + .outputs + .iter() + .any(|field| !field.bounds.is_empty()) + { + return Err(Diagnostic::new( + "C005", + "Port signatures require plain typed inputs and outputs without handlers, ranges, pure or recover", + declaration.span, + )); + } + Ok(()) +} + +pub(super) fn signature( + declaration: &ast::Function, + types: &Types, + definitions: &[Definition], +) -> Result { + plain(declaration)?; + for input in &declaration.inputs { + if let ast::Pattern::Typed(reference, _) = &input.pattern { + // 值类型必须能独立解析,不能把 Choice 分支模式当作 Port 参数类型。 + names::resolve(reference, types)?; + } + } + let signature = signature::check(std::slice::from_ref(declaration), types, definitions)?; + for (_, kind) in signature.inputs.iter().chain(&signature.outputs) { + names::validate(kind, definitions, declaration.span)?; + } + Ok(signature) +} + +pub(super) fn failures( + declaration: &ast::Function, + types: &Types, + definitions: &[Definition], +) -> Result, Diagnostic> { + let reference = declaration.fails.as_ref().ok_or_else(|| { + Diagnostic::new( + "C005", + "Port operations require fails with a public error Choice", + declaration.span, + ) + })?; + let Type::Named(index) = names::resolve(reference, types)? else { + return Err(Diagnostic::new( + "C005", + "fails requires a public App or standard error Choice", + reference.span, + )); + }; + let definition = &definitions[index]; + types.public_type(&Type::Named(index), reference.span)?; + if !matches!( + package::role(&types.packages[definition.source]), + Some((_, "app")) + ) && !package::standard(&types.packages[definition.source]) + { + return Err(Diagnostic::new( + "C006", + "fails requires a public App or standard error Choice", + reference.span, + )); + } + let Shape::Choice(variants) = &definition.shape else { + return Err(Diagnostic::new( + "C005", + "fails requires an error Choice", + reference.span, + )); + }; + let failures = variants + .iter() + .enumerate() + .filter(|(_, variant)| variant.error) + .map(|(variant, _)| Failure { + definition: index, + variant, + }) + .collect::>(); + if failures.is_empty() { + return Err(Diagnostic::new( + "C005", + "fails Choice must contain an explicit error variant", + reference.span, + )); + } + Ok(failures) +} diff --git a/rust/src/compiler/check/application/port/external.rs b/rust/src/compiler/check/application/port/external.rs new file mode 100644 index 0000000..01dbc13 --- /dev/null +++ b/rust/src/compiler/check/application/port/external.rs @@ -0,0 +1,295 @@ +//! 外部实现由所属 Port 生成唯一静态合同,输入输出继续使用具体 wire 类型。 + +use super::{Ports, Types, schema}; +use crate::compiler::model::{Definition, Shape, Type, adapter::External, package}; +use crate::syntax::{ast, diagnostic::Diagnostic, source::Span}; +use serde_json::{Value, json}; +use std::collections::{BTreeMap, BTreeSet}; + +impl Ports { + pub fn external( + &mut self, + declarations: &[ast::External], + groups: &[Vec], + names: &Types, + definitions: &[Definition], + types: &[ast::TypeDeclaration], + ) -> Result<(), Diagnostic> { + for declaration in declarations { + let span = declaration.span; + let source = span.source; + let error = |message: &str| Diagnostic::new("C006", message, span); + if self.external.contains_key(&source) + || groups.iter().any(|group| group[0].span.source == source) + || types + .iter() + .any(|kind| kind.span.source == source && !kind.setting) + { + return Err(error( + "external Adapter contains only one external and an optional setting declaration", + )); + } + if !matches!( + declaration.ecosystem.as_str(), + "exec" | "pip" | "npm" | "go" | "command" + ) { + return Err(error("unknown external ecosystem")); + } + if declaration.entry.contains(['\\', ':', '\0']) + || declaration + .entry + .split('/') + .any(|part| matches!(part, "" | "." | "..")) + { + return Err(error("external entry requires a portable relative path")); + } + let mut capabilities = BTreeSet::new(); + for capability in &declaration.capabilities { + if !matches!(capability.as_str(), "network" | "file" | "process" | "gpu") + || !capabilities.insert(capability.clone()) + { + return Err(error("unknown or repeated external capability")); + } + } + let mut libs = Vec::new(); + for request in &declaration.libs { + let request = if declaration.ecosystem == "exec" + || request.starts_with(&format!("{}:", declaration.ecosystem)) + { + request.clone() + } else { + format!("{}:{request}", declaration.ecosystem) + }; + if declaration.ecosystem == "command" + || !request.split_once(':').is_some_and(|(ecosystem, spec)| { + matches!(ecosystem, "pip" | "npm" | "go") + && spec.rsplit_once('@').is_some_and(|(name, version)| { + !name.is_empty() + && !name.contains(':') + && !version.is_empty() + && !version.contains('@') + }) + }) + || request.chars().any(char::is_whitespace) + || libs.contains(&request) + { + return Err(error( + "external Lib requires a unique exact name@version in its own ecosystem", + )); + } + libs.push(request); + } + let (domain, _) = package::role(&names.packages[source]).expect("checked Adapter role"); + let candidates: BTreeSet<_> = self + .operations + .keys() + .filter_map(|(owner, _)| { + package::role(&names.packages[*owner]) + .is_some_and(|(owner, _)| owner == domain) + .then_some(*owner) + }) + .collect(); + let port = (candidates.len() == 1) + .then(|| *candidates.first().unwrap()) + .ok_or_else(|| { + error("external Adapter requires one unambiguous Port in its domain") + })?; + if declaration.ecosystem == "command" + && (self.settings.contains_key(&source) || !libs.is_empty()) + { + return Err(error("external command cannot declare setting or lib")); + } + let topic = names.packages[port] + .strip_prefix(&format!("{domain}.port")) + .unwrap(); + let identity = format!("{domain}{topic}"); + let adapter = names.packages[source] + .strip_prefix(&format!("{domain}.adapter.")) + .unwrap_or("default"); + let mut inputs = BTreeMap::new(); + let mut outputs = BTreeMap::new(); + let mut errors = BTreeMap::new(); + let mut fingerprint = String::from("dever-component-v1;"); + if declaration.ecosystem != "exec" { + fingerprint.push_str(&format!("ecosystem={};", declaration.ecosystem)); + } + for lib in &libs { + fingerprint.push_str(&format!("lib={lib};")); + } + let setting = self + .settings + .get(&source) + .map(|index| { + schema::build( + &Type::Named(*index), + definitions, + &names.packages, + schema::Policy::SettingInput, + ) + }) + .transpose() + .map_err(|message| error(&message))?; + if let Some(setting) = &setting { + fingerprint.push_str(&format!("setting={};", setting.fingerprint)); + } else { + fingerprint.push_str("setting=none;"); + } + let mut operations = self + .operations + .iter() + .filter(|((owner, _), _)| *owner == port) + .map(|(key, contract)| (contract.span.start, key.clone())) + .collect::>(); + operations.sort_by_key(|(start, _)| *start); + let policy = if declaration.ecosystem == "command" { + if operations.len() != 1 { + return Err(error("command Adapter requires exactly one Port operation")); + } + command( + self.operations.get(&operations[0].1).unwrap(), + definitions, + &names.packages, + ) + .map_err(error)?; + schema::Policy::Command + } else { + schema::Policy::Output + }; + for (_, key) in operations { + let operation = &key.1; + let contract = self.operations.get_mut(&key).unwrap(); + fingerprint.push_str(&format!("operation={operation};")); + inputs.insert( + operation.clone(), + record( + &contract.signature.inputs, + definitions, + &names.packages, + span, + "input", + policy, + &mut fingerprint, + )?, + ); + outputs.insert( + operation.clone(), + record( + &contract.signature.outputs, + definitions, + &names.packages, + span, + "output", + policy, + &mut fingerprint, + )?, + ); + for failure in &contract.failures { + let definition = &definitions[failure.definition]; + let Shape::Choice(variants) = &definition.shape else { + unreachable!() + }; + let variant = &variants[failure.variant]; + let identity = format!( + "{}.{}.{}", + package::namespace(&names.packages[definition.source]), + definition.name, + variant.name + ); + fingerprint.push_str(&format!("{identity};")); + errors.insert( + identity, + record( + &variant.fields, + definitions, + &names.packages, + span, + "error-field", + policy, + &mut fingerprint, + )?, + ); + } + contract + .implementations + .push((source, format!("port.{operation}"))); + } + let setting = setting.map(|schema| schema.value); + let directory = names.packages[source] + .rsplit_once('.') + .unwrap() + .0 + .replace('.', "/"); + let entry = format!("module/{directory}/{}", declaration.entry); + let mut manifest = json!({"port":identity,"adapter":adapter,"ecosystem":declaration.ecosystem,"entry":entry,"libs":libs,"capabilities":capabilities,"operations":inputs.keys().collect::>(),"inputs":inputs,"outputs":outputs,"errors":errors,"setting":setting}); + manifest["schema"] = json!(schema::checksum(&fingerprint)); + self.adapters.insert(source, port); + self.external.insert( + source, + External { + span, + ecosystem: declaration.ecosystem.clone(), + entry, + capabilities: capabilities.into_iter().collect(), + libs: libs.into_iter().collect(), + contract: serde_json::to_string(&manifest).unwrap(), + }, + ); + } + Ok(()) + } +} + +fn record( + fields: &[(String, Type)], + definitions: &[Definition], + packages: &[String], + span: Span, + field_kind: &str, + policy: schema::Policy, + fingerprint: &mut String, +) -> Result { + let fields = fields + .iter() + .map(|(name, value)| { + let schema = schema::build(value, definitions, packages, policy) + .map_err(|message| Diagnostic::new("C005", message, span))?; + fingerprint.push_str(&format!("{field_kind}={name}:{};", schema.fingerprint)); + Ok((name.clone(), schema.value)) + }) + .collect::, Diagnostic>>()?; + Ok(json!({"type":"record", "fields":fields})) +} + +fn command( + operation: &super::Operation, + definitions: &[Definition], + packages: &[String], +) -> Result<(), &'static str> { + let signature = &operation.signature; + let inputs = &signature.inputs; + if !matches!(inputs.len(), 1 | 2) + || inputs[0] != ("args".into(), Type::List(Box::new(Type::Text))) + || inputs.len() == 2 && inputs[1] != ("stdin".into(), Type::Optional(Box::new(Type::Bytes))) + { + return Err("command Port inputs must be args: List and optional stdin: Bytes?"); + } + let [(name, Type::Named(output))] = signature.outputs.as_slice() else { + return Err("command Port output must be output: dever.process.Output"); + }; + let output = &definitions[*output]; + if name != "output" + || packages[output.source] != "dever.process" + || output.name != "Output" + || !matches!(&output.shape, Shape::Record(fields) if fields == &vec![("code".into(), Type::Int), ("stdout".into(), Type::Bytes), ("stderr".into(), Type::Bytes)]) + { + return Err("command Port must return the standard process.Output contract"); + } + let [failure] = operation.failures.as_slice() else { + return Err("command Port requires dever.process.Error"); + }; + let error = &definitions[failure.definition]; + if packages[error.source] != "dever.process" || error.name != "Error" { + return Err("command Port requires dever.process.Error"); + } + Ok(()) +} diff --git a/rust/src/compiler/check/application/port/mod.rs b/rust/src/compiler/check/application/port/mod.rs new file mode 100644 index 0000000..e0c94ee --- /dev/null +++ b/rust/src/compiler/check/application/port/mod.rs @@ -0,0 +1,265 @@ +//! 将无函数体 Port 合同绑定到同领域的原生 Adapter。 +//! 单实现直接调用;多实现共用合同并在入口读取配置后选择。 + +mod contract; +mod external; +mod schema; +mod settings; +mod types; + +use super::super::{names, package::Types}; +use crate::compiler::model::{Definition, Failure, Signature, package}; +use crate::syntax::{ast, diagnostic::Diagnostic, source::Span}; +use std::collections::{BTreeMap, HashMap, HashSet}; + +pub(in crate::compiler::check) struct Operation { + pub signature: Signature, + pub failures: Vec, + pub span: Span, + pub implementations: Vec<(usize, String)>, +} + +#[derive(Default)] +pub(crate) struct Ports { + pub(in crate::compiler::check) operations: BTreeMap<(usize, String), Operation>, + adapters: HashMap, + pub(in crate::compiler::check) settings: BTreeMap, + allowed: HashMap>, + external: BTreeMap, +} + +pub(crate) fn function_name( + function: &ast::Function, + packages: &[String], +) -> Result<(), Diagnostic> { + if matches!( + package::role(&packages[function.span.source]), + Some((_, "adapter" | "test")) + ) && function.name.starts_with("port.") + { + let (prefix, method) = function.name.rsplit_once('.').unwrap(); + for part in prefix.split('.') { + names::identifier(part, false, function.span)?; + } + names::function(method, function.span) + } else { + names::function(&function.name, function.span) + } +} + +impl Ports { + pub fn new( + declarations: &[ast::Function], + names: &Types, + definitions: &[Definition], + types: &[ast::TypeDeclaration], + ) -> Result { + let mut ports = Self { + settings: settings::check(types, definitions)?, + ..Self::default() + }; + for declaration in declarations { + let signature = contract::signature(declaration, names, definitions)?; + let failures = contract::failures(declaration, names, definitions)?; + let allowed = ports.allowed.entry(signature.source).or_default(); + for (_, kind) in signature.inputs.iter().chain(&signature.outputs) { + types::collect(kind, definitions, allowed); + } + if let Some(failure) = failures.first() { + types::collect( + &crate::compiler::model::Type::Named(failure.definition), + definitions, + allowed, + ); + } + let key = (signature.source, signature.name.clone()); + if ports + .operations + .insert( + key, + Operation { + signature, + failures, + span: declaration.span, + implementations: Vec::new(), + }, + ) + .is_some() + { + return Err(Diagnostic::new( + "C002", + "duplicate Port operation", + declaration.span, + )); + } + } + Ok(ports) + } + + pub fn bind<'group>( + &mut self, + groups: impl Iterator, + names: &Types, + ) -> Result<(), Diagnostic> { + for (group, signature) in groups { + let first = &group[0]; + if !signature.name.contains('.') { + continue; + } + let (source, name) = + names + .target(&signature.name, signature.source) + .ok_or_else(|| { + Diagnostic::new("C004", "unknown Port implementation target", first.span) + })?; + let operation = self + .operations + .get_mut(&(source, name.to_owned())) + .ok_or_else(|| Diagnostic::new("C004", "unknown Port operation", first.span))?; + if signature.static_parameters != 0 { + return Err(Diagnostic::new( + "C005", + "Port implementations cannot add handler parameters", + first.span, + )); + } + if signature.inputs != operation.signature.inputs + || signature.outputs != operation.signature.outputs + { + return Err(Diagnostic::new( + "C005", + "Adapter input and output names and types must match the Port contract", + first.span, + )); + } + if names.test_source != Some(signature.source) + && self + .adapters + .insert(signature.source, source) + .is_some_and(|previous| previous != source) + { + return Err(Diagnostic::new( + "C006", + "one Adapter file must implement exactly one Port", + first.span, + )); + } + operation + .implementations + .push((signature.source, signature.name.clone())); + } + for ((source, _), operation) in &self.operations { + if operation.implementations.is_empty() { + return Err(Diagnostic::new( + "C004", + "Port operation has no Adapter implementation", + operation.span, + )); + } + let expected = self + .operations + .iter() + .filter(|((port, _), _)| port == source) + .flat_map(|(_, operation)| { + operation.implementations.iter().map(|(source, _)| *source) + }) + .collect::>(); + let actual = operation + .implementations + .iter() + .map(|(source, _)| *source) + .collect::>(); + if actual != expected { + return Err(Diagnostic::new( + "C006", + "one Adapter file must implement the complete Port", + operation.span, + )); + } + } + for (source, identity) in names.packages.iter().enumerate() { + if matches!(package::role(identity), Some((_, "adapter"))) + && !self.adapters.contains_key(&source) + { + return Err(Diagnostic::new( + "C006", + "Adapter file must implement a Port", + Span { + source, + start: 0, + end: 0, + }, + )); + } + } + Ok(()) + } + + /// 调用者看到声明的错误全集;实现缩小失败集合不能缩小公开合同。 + pub fn finish(&self, signature: &mut Signature, span: Span) -> Result<(), Diagnostic> { + let Some(operation) = self.operations.values().find(|operation| { + operation + .implementations + .iter() + .any(|(source, name)| *source == signature.source && name == &signature.name) + }) else { + return Ok(()); + }; + if signature + .failures + .iter() + .any(|failure| !operation.failures.contains(failure)) + { + return Err(Diagnostic::new( + "C012", + "Adapter failure is outside the Port fails contract", + span, + )); + } + signature.failures.clone_from(&operation.failures); + signature.effects.unmediated_io = false; + Ok(()) + } + + pub fn layout(&self, names: &Types) -> crate::compiler::model::adapter::Adapters { + use crate::compiler::model::adapter::{Adapters, Implementation, Port}; + let mut ports = BTreeMap::>::new(); + for ((port, _), operation) in &self.operations { + for (source, _) in &operation.implementations { + let implementations = ports.entry(*port).or_default(); + if !implementations.contains(source) { + implementations.push(*source); + } + } + } + Adapters { + root: None, + ports: ports + .into_iter() + .map(|(source, mut implementations)| { + implementations.sort_unstable(); + let identity = &names.packages[source]; + let (domain, _) = package::role(identity).unwrap(); + let topic = identity.strip_prefix(&format!("{domain}.port")).unwrap(); + Port { + source, + name: format!("{domain}{topic}"), + implementations: implementations + .into_iter() + .map(|source| Implementation { + source, + name: names.packages[source] + .strip_prefix(&format!("{domain}.adapter.")) + .unwrap_or("default") + .to_owned(), + setting: self.settings.get(&source).copied(), + external: self.external.get(&source).cloned(), + }) + .collect(), + } + }) + .collect(), + functions: BTreeMap::new(), + } + } +} diff --git a/rust/src/compiler/check/application/port/schema.rs b/rust/src/compiler/check/application/port/schema.rs new file mode 100644 index 0000000..9c5ef71 --- /dev/null +++ b/rust/src/compiler/check/application/port/schema.rs @@ -0,0 +1,145 @@ +//! 保留 SDK v1 的 wire 节点顺序和指纹,避免签名不变却使旧锁/Worker 无法握手。 + +use crate::compiler::model::{Definition, Shape, Type, package}; +use serde_json::{Value, json}; +use std::collections::HashMap; + +pub(super) struct Schema { + pub value: Value, + pub fingerprint: String, +} + +#[derive(Clone, Copy, Debug)] +pub(super) enum Policy { + Output, + SettingInput, + Command, +} + +pub(super) fn build( + kind: &Type, + definitions: &[Definition], + packages: &[String], + policy: Policy, +) -> Result { + let mut nodes = Vec::new(); + let value = node( + kind, + definitions, + packages, + policy, + &mut nodes, + &mut HashMap::new(), + 0, + )? + .1; + Ok(Schema { + value, + fingerprint: format!("wire-v1:{policy:?}:{}", nodes.join("")), + }) +} + +fn node( + kind: &Type, + definitions: &[Definition], + packages: &[String], + policy: Policy, + nodes: &mut Vec, + known: &mut HashMap, + depth: usize, +) -> Result<(usize, Value), String> { + if depth > 64 { + return Err("Worker wire schema exceeds depth limit".into()); + } + if let Some(value) = known.get(kind) { + return Ok(value.clone()); + } + let (fingerprint, value) = match kind { + Type::Optional(inner) | Type::List(inner) => { + let (index, value) = node( + inner, + definitions, + packages, + policy, + nodes, + known, + depth + 1, + )?; + if matches!(kind, Type::Optional(_)) { + ( + format!("Nullable({index});"), + json!({"type":"nullable","value":value}), + ) + } else { + ( + format!("List({index});"), + json!({"type":"list","value":value}), + ) + } + } + Type::Named(index) => { + let definition = &definitions[*index]; + let Shape::Record(fields) = &definition.shape else { + return Err("Worker wire Choice is not supported".into()); + }; + if !definition.private_fields.is_empty() + || definition.ranges.iter().any(Option::is_some) + { + return Err("Worker wire fields must be public and unconstrained".into()); + } + let mut indices = Vec::new(); + let mut values = serde_json::Map::new(); + for (name, kind) in fields { + let (index, value) = + node(kind, definitions, packages, policy, nodes, known, depth + 1)?; + indices.push((name.clone(), index)); + values.insert(name.clone(), value); + } + let name = format!( + "{}.{}", + package::namespace(&packages[definition.source]), + definition.name + ); + ( + format!("Record({name:?},{indices:?});"), + json!({"type":"record","name":name,"fields":values}), + ) + } + Type::ModelId(index) => { + let definition = &definitions[*index]; + let name = format!( + "{}.{}.Id", + package::namespace(&packages[definition.source]), + definition.name + ); + ( + format!("ModelId({name:?});"), + json!({"type":"model_id","name":name}), + ) + } + _ => { + let name = match kind { + Type::Bool => "bool", Type::Int => "int64", Type::Float => "float64", + Type::Decimal => "decimal", Type::Text => "text", Type::Id => "id", + Type::Uuid => "uuid", Type::Json => "json", Type::DateTime => "datetime", + Type::Date => "date", Type::Time => "time", Type::Duration => "duration", + Type::Secret if matches!(policy, Policy::SettingInput) => "secret", + Type::Bytes if matches!(policy, Policy::Command) => "bytes", + _ => return Err("Worker wire values require portable public scalar/record/List types; Secret belongs only in setting".into()), + }; + (format!("{kind:?};"), json!({"type":name})) + } + }; + let index = nodes.len(); + nodes.push(fingerprint); + known.insert(kind.clone(), (index, value.clone())); + Ok((index, value)) +} + +pub(super) fn checksum(identity: &str) -> String { + let mut hash = 0xcbf29ce484222325u64; + for byte in identity.bytes() { + hash = (hash ^ u64::from(byte)).wrapping_mul(0x100000001b3); + } + format!("{hash:016x}") +} diff --git a/rust/src/compiler/check/application/port/settings.rs b/rust/src/compiler/check/application/port/settings.rs new file mode 100644 index 0000000..28c726d --- /dev/null +++ b/rust/src/compiler/check/application/port/settings.rs @@ -0,0 +1,52 @@ +//! setting 是 Adapter 私有的输入记录;沿用 wire 值形状,额外允许 Secret。 + +use crate::compiler::model::{Definition, Shape, Type}; +use crate::syntax::{ast, diagnostic::Diagnostic, source::Span}; +use std::collections::BTreeMap; + +pub(super) fn check( + declarations: &[ast::TypeDeclaration], + definitions: &[Definition], +) -> Result, Diagnostic> { + let mut settings = BTreeMap::new(); + for (index, declaration) in declarations + .iter() + .enumerate() + .filter(|(_, value)| value.setting) + { + validate(&Type::Named(index), definitions, declaration.span)?; + settings.insert(declaration.span.source, index); + } + Ok(settings) +} + +fn validate(kind: &Type, definitions: &[Definition], span: Span) -> Result<(), Diagnostic> { + match kind { + Type::Secret => Ok(()), + Type::List(inner) | Type::Optional(inner) => validate(inner, definitions, span), + Type::Named(index) => { + let definition = &definitions[*index]; + let Shape::Record(fields) = &definition.shape else { + return Err(Diagnostic::new( + "C005", + "setting requires wire records, not Choice", + span, + )); + }; + if !definition.private_fields.is_empty() + || definition.ranges.iter().any(Option::is_some) + { + return Err(Diagnostic::new( + "C005", + "setting records cannot contain private or constrained fields", + span, + )); + } + for (_, kind) in fields { + validate(kind, definitions, span)?; + } + Ok(()) + } + _ => super::super::wire_type(kind, definitions, span), + } +} diff --git a/rust/src/compiler/check/application/port/types.rs b/rust/src/compiler/check/application/port/types.rs new file mode 100644 index 0000000..711d59e --- /dev/null +++ b/rust/src/compiler/check/application/port/types.rs @@ -0,0 +1,61 @@ +//! Port/Adapter 只获得合同实际使用的 App 类型,以及所实现 Port 的私有 DTO。 +//! 检查覆盖类型声明、函数签名和函数体引用,不能用完整路径绕过职责边界。 + +use super::{Ports, Types}; +use crate::compiler::model::{Definition, Shape, Type, package}; +use crate::syntax::diagnostic::Diagnostic; +use std::collections::HashSet; + +pub(super) fn collect(kind: &Type, definitions: &[Definition], found: &mut HashSet) { + match kind { + Type::Named(index) if found.insert(*index) => match &definitions[*index].shape { + Shape::Record(fields) => { + for (_, kind) in fields { + collect(kind, definitions, found); + } + } + Shape::Choice(variants) => { + for variant in variants { + for (_, kind) in &variant.fields { + collect(kind, definitions, found); + } + } + } + }, + Type::List(inner) + | Type::Optional(inner) + | Type::Stream(inner) + | Type::AsyncStream(inner) + | Type::Channel(inner) => collect(inner, definitions, found), + Type::Map(key, value) | Type::MapEntry(key, value) => { + collect(key, definitions, found); + collect(value, definitions, found); + } + _ => {} + } +} + +impl Ports { + pub fn check_types(&self, types: &Types, definitions: &[Definition]) -> Result<(), Diagnostic> { + for &(source, index, span) in types.role_types.borrow().iter() { + let definition = &definitions[index]; + let port = self.adapters.get(&source).copied().unwrap_or(source); + let allowed = match package::role(&types.packages[definition.source]) { + Some((_, "app")) => self + .allowed + .get(&port) + .is_some_and(|allowed| allowed.contains(&index)), + Some((_, "port")) => definition.source == port, + _ => true, + }; + if !allowed { + return Err(Diagnostic::new( + "C006", + "Port/Adapter type is outside its Port contract", + span, + )); + } + } + Ok(()) + } +} diff --git a/rust/src/compiler/check/application/rest/bindings.rs b/rust/src/compiler/check/application/rest/bindings.rs new file mode 100644 index 0000000..4e1cb99 --- /dev/null +++ b/rust/src/compiler/check/application/rest/bindings.rs @@ -0,0 +1,202 @@ +//! 字段表达式复用普通函数的类型、失败和效果检查;不会在运行时解释表达式。 + +use crate::{ + compiler::model::*, + syntax::{ast, diagnostic::Diagnostic}, +}; + +pub(in crate::compiler::check) fn prepare( + declarations: &[ast::TypeDeclaration], + groups: &mut Vec>, + packages: &[String], +) -> Result<(), Diagnostic> { + for declaration in declarations { + if !matches!( + package::role(&packages[declaration.span.source]), + Some((_, "model")) + ) { + continue; + } + let ast::TypeBody::Record(fields) = &declaration.body else { + continue; + }; + for field in fields { + if field.rest.owner.is_some() + && (field.rest.create.is_some() + || field.rest.replace.is_some() + || field.rest.search.is_some()) + { + return Err(Diagnostic::new( + "C014", + "owner cannot combine with REST field bindings", + field.span, + )); + } + for (action, value) in [ + ("create", &field.rest.create), + ("replace", &field.rest.replace), + ("search", &field.rest.search), + ] { + let Some(value) = value else { + continue; + }; + let input = uses_input(value); + if input && field.private { + return Err(Diagnostic::new( + "C014", + "private REST fields cannot read client input", + field.span, + )); + } + if action == "search" && (!input || field.private || field.kind.nullable) { + return Err(Diagnostic::new( + "C014", + "REST search requires public non-null client input", + field.span, + )); + } + let mut kind = field.kind.clone(); + kind.storage.clear(); + let mut output = field.clone(); + output.kind = kind.clone(); + output.name = "value".into(); + output.private = false; + output.unique = false; + output.index = false; + output.rest = Default::default(); + output.persistence = Default::default(); + groups.push(vec![ast::Function { + contract: false, + name: name(action, &field.name), + transaction: false, + inputs: if input { + vec![ast::Parameter { + name: "input".into(), + pattern: ast::Pattern::Typed(kind, vec![]), + span: field.span, + }] + } else { + vec![] + }, + outputs: vec![output], + pure: false, + recovery: None, + fails: None, + public: false, + body: vec![ast::Statement::Assign { + annotation: None, + target: ast::Expression { + kind: ast::ExpressionKind::Name("value".into()), + span: value.span, + depth: 1, + }, + value: value.clone(), + }], + span: value.span, + }]); + } + } + } + Ok(()) +} + +// 源码词法不能构造这个名字,字段转换不会意外成为可调用的 Model 方法。 +pub(super) fn name(action: &str, field: &str) -> String { + format!("#rest_{action}_{field}") +} + +fn uses_input(value: &ast::Expression) -> bool { + use ast::ExpressionKind::*; + match &value.kind { + Name(name) => name == "input", + Call(target, values) => uses_input(target) || values.iter().any(uses_input), + Field(base, _) | Unary(_, base) | Channel(_, base) => uses_input(base), + Binary(_, left, right) => uses_input(left) || uses_input(right), + List(values) => values.iter().any(uses_input), + Map(values) => values + .iter() + .any(|(key, value)| uses_input(key) || uses_input(value)), + Record(_, fields) => fields.iter().any(|(_, value)| uses_input(value)), + Literal(_) => false, + } +} + +/// 沿静态调用图检查所有叶子,只读请求能力不能被包装函数变成响应写入。 +pub(super) fn validate( + root: usize, + functions: &[Option], + calls: &[Vec], + packages: &[String], +) -> Result<(), Diagnostic> { + let mut pending = vec![root]; + let mut seen = std::collections::HashSet::new(); + while let Some(index) = pending.pop() { + if !seen.insert(index) { + continue; + } + let function = functions[index].as_ref().unwrap(); + let effects = function.signature.effects; + let span = function.clauses[0].span; + if (Effects { + request: false, + identity: false, + authentication: false, + ..effects + }) != Effects::default() + { + return Err(Diagnostic::new( + "C014", + "REST field bindings permit only values and read-only request context", + span, + )); + } + if matches!( + package::role(&packages[function.signature.source]), + Some((_, "domain")) + ) && !function.signature.pure + { + return Err(Diagnostic::new( + "C014", + "REST field Domain functions must be pure", + span, + )); + } + for clause in &function.clauses { + for statement in &clause.body { + let (Statement::Assign { value, .. } + | Statement::Action(value) + | Statement::Fail(value)) = statement; + readonly(value)?; + } + } + pending.extend(&calls[index]); + } + Ok(()) +} + +fn readonly(value: &Expression) -> Result<(), Diagnostic> { + if let Node::Auth(call, _) = &value.kind { + use auth::Operation::*; + if !matches!( + call.operation, + Id | Session + | UserId + | TenantId + | OwnsUser + | Site + | RequestId + | Method + | Path + | ClientAddress + | Header + | Cookie + ) { + return Err(Diagnostic::new( + "C014", + "REST field binding cannot mutate response or expose secrets", + value.span, + )); + } + } + value.children(readonly) +} diff --git a/rust/src/compiler/check/application/rest/mod.rs b/rust/src/compiler/check/application/rest/mod.rs new file mode 100644 index 0000000..26bac76 --- /dev/null +++ b/rust/src/compiler/check/application/rest/mod.rs @@ -0,0 +1,248 @@ +//! REST 从同领域唯一 Model 得到固定字段合同,私有字段永不成为输入或输出。 + +use super::*; +use crate::compiler::model::{Function, persistence::Model, rest}; +mod bindings; +pub(in crate::compiler::check) use bindings::prepare; + +pub(in crate::compiler::check) struct Context<'program> { + pub models: &'program [Model], + pub declarations: &'program [ast::TypeDeclaration], + pub definitions: &'program [model::Definition], + pub packages: &'program [String], + pub http: &'program [model::HttpEntry], + pub verifiers: &'program [model::auth::Verifier], + pub signatures: &'program [model::Signature], + pub functions: &'program [Option], + pub calls: &'program [Vec], +} + +pub(in crate::compiler::check) fn check( + entries: &[ast::Rest], + context: Context<'_>, +) -> Result, Diagnostic> { + let Context { + models, + declarations, + definitions, + packages, + http, + verifiers, + signatures, + functions, + calls, + } = context; + for (index, signature) in signatures.iter().enumerate() { + if matches!( + package::role(&packages[signature.source]), + Some((_, "model")) + ) { + bindings::validate(index, functions, calls, packages)?; + } + } + let mut result = Vec::new(); + let mut seen = HashSet::new(); + for entry in entries { + let span = entry.span; + if verifiers.is_empty() { + return Err(Diagnostic::new( + "C014", + "REST requires a compiled App verify function", + span, + )); + } + let identity = &packages[span.source]; + let (namespace, _) = package::role(identity).unwrap(); + let candidates = models + .iter() + .enumerate() + .filter(|(_, model)| { + package::role(&packages[model.source]) == Some((namespace, "model")) + && entry.model.as_ref().is_none_or(|selector| { + packages[model.source] == format!("{namespace}.{selector}") + }) + }) + .collect::>(); + if candidates.len() != 1 { + return Err(Diagnostic::new( + "C014", + "REST requires one same-domain Model", + span, + )); + } + let (index, model) = candidates[0]; + let definition = &definitions[model.definition]; + let read = (0..model.fields.len()) + .filter(|position| !definition.private_fields.contains(position)) + .collect::>(); + for position in &read { + if matches!(model.fields[*position].1, Type::Bytes) { + return Err(Diagnostic::new( + "C005", + "REST field cannot be serialized by the API codec", + span, + )); + } + if model.columns[*position].choice.is_none() { + wire_type(&model.fields[*position].1, definitions, span)?; + } + } + let ast::TypeBody::Record(fields) = &declarations[model.definition].body else { + unreachable!() + }; + let mut owner = None; + let mut search = None; + let mut create = Vec::new(); + let mut replace = Vec::new(); + for (position, field) in fields.iter().enumerate().skip(2) { + if let Some(expression) = &field.rest.owner { + let ast::ExpressionKind::Call(target, args) = &expression.kind else { + return Err(Diagnostic::new( + "C014", + "owner requires user_id() or tenant_id()", + field.span, + )); + }; + let user = match target.path().as_deref() { + Some("dever.auth.userId") => true, + Some("dever.auth.tenantId") => false, + _ => { + return Err(Diagnostic::new( + "C014", + "owner requires user_id() or tenant_id()", + field.span, + )); + } + }; + if owner.is_some() + || !args.is_empty() + || !field.persistence.is_empty() + || !matches!(model.fields[position].1, Type::Int | Type::ModelId(_)) + { + return Err(Diagnostic::new( + "C014", + "REST requires one non-null integer owner", + field.span, + )); + } + owner = Some(rest::Owner { position, user }); + continue; + } + for (action, value, target) in [ + ("create", &field.rest.create, &mut create), + ("replace", &field.rest.replace, &mut replace), + ] { + if model.columns[position].generated { + if value.is_some() { + return Err(Diagnostic::new( + "C014", + "generated fields cannot have REST write bindings", + field.span, + )); + } + continue; + } + let function = value.as_ref().map(|_| { + signatures + .iter() + .position(|signature| { + signature.source == model.source + && signature.name == bindings::name(action, &field.name) + }) + .expect("prepared REST binding") + }); + if function.is_none() && field.private { + if action == "create" + && !matches!(model.fields[position].1, Type::Optional(_)) + && model.columns[position].default.is_none() + { + return Err(Diagnostic::new( + "C014", + "REST field is private and required without a source", + field.span, + )); + } + continue; + } + target.push(rest::Binding { + position, + input: function.is_none_or(|index| !signatures[index].inputs.is_empty()), + function, + }); + } + if field.rest.search.is_some() { + if matches!(field.name.as_str(), "page" | "size") { + return Err(Diagnostic::new( + "C014", + "REST search field cannot use reserved pagination names", + field.span, + )); + } + if search.is_some() + || !(field.index || field.unique) + || matches!( + model.fields[position].1, + Type::Json | Type::Bytes | Type::Optional(_) + ) + { + return Err(Diagnostic::new( + "C014", + "REST permits one indexed scalar search field", + field.span, + )); + } + let function = signatures + .iter() + .position(|signature| { + signature.source == model.source + && signature.name == bindings::name("search", &field.name) + }) + .expect("prepared REST binding"); + search = Some(rest::Binding { + position, + input: true, + function: Some(function), + }); + } + } + let topic = identity + .strip_prefix(&format!("{namespace}.api.")) + .unwrap_or("") + .replace('.', "/"); + let path = format!( + "/{}{}{}", + namespace.replace('.', "/"), + if topic.is_empty() { "" } else { "/" }, + topic + ); + if seen.iter().any(|previous: &String| { + previous == &path + || previous.starts_with(&format!("{path}/")) + || path.starts_with(&format!("{previous}/")) + }) || http + .iter() + .any(|entry| entry.path == path || entry.path.starts_with(&format!("{path}/"))) + { + return Err(Diagnostic::new( + "C002", + "duplicate HTTP route with REST", + span, + )); + } + seen.insert(path.clone()); + let (component, domain) = namespace.split_once('.').unwrap(); + result.push(rest::Entry { + model: index, + path, + topic, + component: component.into(), + domain: domain.into(), + read, + create, + replace, + search, + owner, + }); + } + Ok(result) +} diff --git a/rust/src/compiler/check/async_stream/mod.rs b/rust/src/compiler/check/async_stream/mod.rs new file mode 100644 index 0000000..252730b --- /dev/null +++ b/rust/src/compiler/check/async_stream/mod.rs @@ -0,0 +1,69 @@ +//! 异步流复用已有元素类型;构造不拉取,消费时才推导挂起效果。 + +use super::{Context, names}; +use crate::compiler::model::*; +use crate::syntax::{ast, diagnostic::Diagnostic}; + +impl Context<'_> { + pub(super) fn stream( + &mut self, + arguments: &[ast::Expression], + expected: Option<&Type>, + source: &ast::Expression, + ) -> Result { + let [value] = arguments else { + return Err(self.error(source, "stream requires one List, Bytes or Channel")); + }; + let context = match expected { + Some(Type::AsyncStream(inner)) + if matches!(value.kind, ast::ExpressionKind::List(_)) => + { + Some(Type::List(inner.clone())) + } + _ => None, + }; + let value = self.expression(value, context.as_ref())?; + let item = match &value.value_type { + Type::List(inner) | Type::Channel(inner) => inner.clone(), + Type::Bytes => Box::new(Type::Int), + _ => return Err(self.error(source, "stream requires a List, Bytes or Channel")), + }; + let value_type = Type::AsyncStream(item); + names::validate(&value_type, self.definitions, source.span)?; + self.coerce( + Expression { + value_type, + kind: Node::Stream(Box::new(value)), + span: source.span, + }, + expected, + ) + } + + pub(super) fn ticks( + &mut self, + arguments: &[ast::Expression], + expected: Option<&Type>, + source: &ast::Expression, + ) -> Result { + if self.definition_names.packages[self.source] != "dever.task" { + return Err(Diagnostic::new( + "C006", + "tick intrinsic belongs to the official dever.task package", + source.span, + )); + } + let [millis] = arguments else { + return Err(self.error(source, "ticks requires one duration")); + }; + let millis = self.expression(millis, Some(&Type::Int))?; + self.coerce( + Expression { + value_type: Type::AsyncStream(Box::new(Type::Int)), + kind: Node::Ticks(Box::new(millis)), + span: source.span, + }, + expected, + ) + } +} diff --git a/rust/src/compiler/check/auth/authorization.rs b/rust/src/compiler/check/auth/authorization.rs new file mode 100644 index 0000000..8bcb399 --- /dev/null +++ b/rust/src/compiler/check/auth/authorization.rs @@ -0,0 +1,79 @@ +//! 授权函数复用标准名义记录和错误,写效果沿普通调用图传播到 API 声明。 + +use super::super::Context; +use crate::{ + compiler::model::*, + syntax::{ast, diagnostic::Diagnostic}, +}; + +impl Context<'_> { + pub(in crate::compiler::check) fn authorization( + &mut self, + operation: authorization::Operation, + arguments: &[ast::Expression], + expected: Option<&Type>, + source: &ast::Expression, + ) -> Result { + use authorization::Operation::*; + if self.definition_names.packages[self.source] != "dever.auth" { + return Err(Diagnostic::new( + "C006", + "authorization intrinsics belong to dever.auth", + source.span, + )); + } + let inputs = match operation { + Permissions | CurrentPermissions | Roles => vec![], + Role | Disable | Delete => vec![Type::Text], + UserRoles | HasProtectedRoles => vec![Type::Int], + RegisterCapability | RemoveCapability => vec![Type::Uuid], + Save => vec![ + Type::Text, + Type::Text, + Type::Bool, + Type::List(Box::new(Type::Text)), + ], + Grant | Revoke => vec![Type::Int, Type::Text], + }; + if inputs.len() != arguments.len() { + return Err(self.error(source, "authorization argument count differs")); + } + let values = arguments + .iter() + .zip(&inputs) + .map(|(value, kind)| self.expression(value, Some(kind))) + .collect::, _>>()?; + let definition = |name: &str| { + self.definition_names + .resolve(name, self.source, source.span)? + .ok_or_else(|| self.error(source, "authorization standard type is missing")) + }; + let output = match operation { + HasProtectedRoles => Type::Bool, + RegisterCapability => Type::Text, + Save | Grant | Revoke | Disable | Delete | RemoveCapability => Type::Unit, + Permissions | CurrentPermissions | Roles | Role | UserRoles => { + let record = + definition(if matches!(operation, Permissions | CurrentPermissions) { + "dever.auth.Permission" + } else { + "dever.auth.Role" + })?; + Type::List(Box::new(Type::Named(record))) + } + }; + let call = authorization::Call { + operation, + api_error: definition("dever.api.Error")?, + database_error: definition("dever.database.Error")?, + }; + self.coerce( + Expression { + kind: Node::Authorization(call, values), + value_type: output, + span: source.span, + }, + expected, + ) + } +} diff --git a/rust/src/compiler/check/auth/mod.rs b/rust/src/compiler/check/auth/mod.rs new file mode 100644 index 0000000..61c6aae --- /dev/null +++ b/rust/src/compiler/check/auth/mod.rs @@ -0,0 +1,171 @@ +//! Claims 到身份的唯一桥接是普通 App 函数;配置不能指定未编译或越权的入口。 + +use super::{handler::Expansion, package::Types}; +use crate::{ + compiler::model::*, + syntax::{diagnostic::Diagnostic, source::Span}, +}; +use std::collections::HashSet; + +mod authorization; +mod operations; + +pub(super) fn verifiers( + signatures: &[Signature], + functions: &[Option], + definitions: &[Definition], + models: &[persistence::Model], + names: &Types, + expansion: &Expansion, +) -> Result, Diagnostic> { + let Some(claims) = definitions.iter().position(|definition| { + definition.name == "Claims" && names.packages[definition.source] == "dever.auth" + }) else { + return Ok(Vec::new()); + }; + let mut entries = Vec::new(); + for (function, signature) in signatures.iter().enumerate() { + if signature.inputs.len() != 1 + || signature.inputs[0].1 != Type::Named(claims) + || !matches!( + package::role(&names.packages[signature.source]), + Some((_, "app")) + ) + { + continue; + } + let span = expansion.instances[function].span; + if signature.transaction + || signature.effects.database_write + || signature.effects.transaction_unsafe() + || signature.effects.output + || signature.effects.request + || signature.effects.handler + || signature.static_parameters != 0 + { + return Err(Diagnostic::new( + "C014", + "Provider verify permits only value work, clock reads and global Model reads", + span, + )); + } + let [(_, Type::Named(identity))] = signature.outputs.as_slice() else { + return Err(Diagnostic::new( + "C005", + "Provider verify must return its own identity record", + span, + )); + }; + let definition = &definitions[*identity]; + let Shape::Record(fields) = &definition.shape else { + return Err(Diagnostic::new( + "C005", + "Provider identity must be a record", + span, + )); + }; + if names.owner(definition.source) != names.owner(signature.source) + || !definition.private_fields.is_empty() + || definition.ranges.iter().any(Option::is_some) + || fields.len() != 3 + { + return Err(Diagnostic::new( + "C005", + "Provider identity must expose exactly id, user_id and tenant_id in its own App", + span, + )); + } + let id = fields + .iter() + .position(|(name, kind)| name == "id" && *kind == Type::Text) + .ok_or_else(|| Diagnostic::new("C005", "Provider identity requires id: Text", span))?; + let user = model_id(fields, "user_id", span)?; + let tenant = model_id(fields, "tenant_id", span)?; + global_reads(function, functions, models, &expansion.calls, span)?; + entries.push(auth::Verifier { + function, + name: format!( + "{}.{}", + package::namespace(&names.packages[signature.source]), + signature.name + ), + claims, + id, + user, + tenant, + }); + } + Ok(entries) +} + +fn model_id( + fields: &[(String, Type)], + name: &str, + span: Span, +) -> Result<(usize, usize), Diagnostic> { + fields + .iter() + .enumerate() + .find_map(|(position, (field, kind))| { + if field == name + && let Type::Optional(inner) = kind + && let Type::ModelId(model) = inner.as_ref() + { + Some((position, *model)) + } else { + None + } + }) + .ok_or_else(|| { + Diagnostic::new( + "C005", + "Provider user_id and tenant_id require nullable nominal Model IDs", + span, + ) + }) +} + +fn global_reads( + root: usize, + functions: &[Option], + models: &[persistence::Model], + calls: &[Vec], + span: Span, +) -> Result<(), Diagnostic> { + fn value( + expression: &Expression, + models: &[persistence::Model], + span: Span, + ) -> Result<(), Diagnostic> { + if let Node::Database(query, _) = &expression.kind + && query.models().any(|model| !models[model].global) + { + return Err(Diagnostic::new( + "C014", + "Provider verify cannot read a tenant Model", + span, + )); + } + expression.children(|child| value(child, models, span)) + } + let mut pending = vec![root]; + let mut seen = HashSet::new(); + while let Some(index) = pending.pop() { + if !seen.insert(index) { + continue; + } + let function = functions[index].as_ref().expect("checked function order"); + for clause in &function.clauses { + for statement in &clause.body { + let (Statement::Assign { + value: expression, .. + } + | Statement::Action(expression) + | Statement::Fail(expression)) = statement; + value(expression, models, span)?; + } + } + pending.extend(&calls[index]); + } + Ok(()) +} diff --git a/rust/src/compiler/check/auth/operations.rs b/rust/src/compiler/check/auth/operations.rs new file mode 100644 index 0000000..69fa57b --- /dev/null +++ b/rust/src/compiler/check/auth/operations.rs @@ -0,0 +1,119 @@ +//! 身份操作保留名义 ID;外部调用者不能直接使用标准库的底层接收点。 + +use super::super::Context; +use crate::{ + compiler::model::*, + syntax::{ast, diagnostic::Diagnostic}, +}; + +impl Context<'_> { + pub(in crate::compiler::check) fn auth( + &mut self, + operation: auth::Operation, + arguments: &[ast::Expression], + expected: Option<&Type>, + source: &ast::Expression, + ) -> Result { + use auth::Operation::*; + let package = &self.definition_names.packages[self.source]; + let named_id = matches!(&source.kind, ast::ExpressionKind::Call(target, _) if matches!(target.path().as_deref(), Some("dever.auth.userId" | "dever.auth.tenantId"))) + && matches!(expected, Some(Type::Optional(inner)) if matches!(inner.as_ref(), Type::ModelId(_))); + if operation != OwnsUser && !named_id && package != operation.package() { + return Err(Diagnostic::new( + "C006", + "request intrinsics belong to their official package", + source.span, + )); + } + let mut model = None; + let (inputs, output) = match operation { + Id | Session | Site => (vec![], Type::Text), + UserId | TenantId => { + let inner = match expected { + Some(Type::Optional(inner)) if matches!(inner.as_ref(), Type::ModelId(_)) => { + let Type::ModelId(index) = inner.as_ref() else { + unreachable!() + }; + model = Some(*index); + *inner.clone() + } + _ => Type::Int, + }; + (vec![], Type::Optional(Box::new(inner))) + } + OwnsUser => { + let [argument] = arguments else { + return Err(self.error(source, "owns_user requires one nominal Model ID")); + }; + let value = self.expression(argument, None)?; + let Type::ModelId(index) = value.value_type else { + return Err(self.error(source, "owns_user requires one nominal Model ID")); + }; + model = Some(index); + (vec![Type::ModelId(index)], Type::Bool) + } + Issue | IssueCookie => ( + vec![Type::Text, Type::Text, Type::Optional(Box::new(Type::Text))], + if operation == Issue { + Type::Secret + } else { + Type::Unit + }, + ), + ClearCookie => (vec![], Type::Unit), + RequestId | Method | Path | ClientAddress => (vec![], Type::Text), + Header | Cookie => (vec![Type::Text], Type::Optional(Box::new(Type::Text))), + SecretCookie => (vec![Type::Text], Type::Optional(Box::new(Type::Secret))), + SetHeader => (vec![Type::Text, Type::Text], Type::Unit), + SetCookie | SetSecretCookie => { + let definition = self + .definition_names + .resolve("dever.api.CookieOptions", self.source, source.span)? + .ok_or_else(|| self.error(source, "CookieOptions is missing"))?; + ( + vec![ + Type::Text, + if operation == SetCookie { + Type::Text + } else { + Type::Secret + }, + Type::Named(definition), + ], + Type::Unit, + ) + } + }; + if inputs.len() != arguments.len() { + return Err(self.error(source, "request operation argument count differs")); + } + let values = arguments + .iter() + .zip(&inputs) + .map(|(value, kind)| self.expression(value, Some(kind))) + .collect::, _>>()?; + let error = self + .definitions + .iter() + .position(|definition| { + definition.name == "Error" + && self.definition_names.packages[definition.source] == "dever.api" + }) + .ok_or_else(|| self.error(source, "request operations require dever.api.Error"))?; + self.coerce( + Expression { + kind: Node::Auth( + auth::Call { + operation, + error, + model, + }, + values, + ), + value_type: output, + span: source.span, + }, + expected, + ) + } +} diff --git a/rust/src/compiler/check/bytes/mod.rs b/rust/src/compiler/check/bytes/mod.rs new file mode 100644 index 0000000..364ca5a --- /dev/null +++ b/rust/src/compiler/check/bytes/mod.rs @@ -0,0 +1,83 @@ +//! 校验官方 bytes 底层操作的权限、参数及名义返回类型。 +//! 转换失败必须进入库定义的业务错误,不能降为无类型字符串或数值 Fault。 + +use super::Context; +use crate::compiler::model::*; +use crate::syntax::{ast, diagnostic::Diagnostic}; + +impl Context<'_> { + pub(super) fn bytes( + &mut self, + operation: BytesOperation, + arguments: &[ast::Expression], + expected: Option<&Type>, + source: &ast::Expression, + ) -> Result { + if self.definition_names.packages[self.source] != "dever.bytes" { + return Err(Diagnostic::new( + "C006", + "byte intrinsics belong to the official dever.bytes package", + source.span, + )); + } + let inputs = match operation { + BytesOperation::FromText => vec![Type::Text], + BytesOperation::FromInts => vec![Type::List(Box::new(Type::Int))], + BytesOperation::ToText | BytesOperation::Length => vec![Type::Bytes], + BytesOperation::At => vec![Type::Bytes, Type::Int], + BytesOperation::Slice => vec![Type::Bytes, Type::Int, Type::Int], + BytesOperation::Concat => vec![Type::Bytes, Type::Bytes], + }; + if arguments.len() != inputs.len() { + return Err(self.error(source, "byte operation argument count differs")); + } + let values = arguments + .iter() + .zip(inputs) + .map(|(argument, kind)| self.expression(argument, Some(&kind))) + .collect::, _>>()?; + let value_type = match operation { + BytesOperation::FromText | BytesOperation::Concat => Type::Bytes, + BytesOperation::Length => Type::Int, + BytesOperation::At => Type::Int.optional(), + BytesOperation::ToText => { + self.byte_result("DecodeResult", "Decoded", "text", Type::Text, source)? + } + BytesOperation::FromInts | BytesOperation::Slice => { + self.byte_result("BufferResult", "Ready", "bytes", Type::Bytes, source)? + } + }; + self.coerce( + Expression { + kind: Node::Bytes(operation, values), + value_type, + span: source.span, + }, + expected, + ) + } + + fn byte_result( + &self, + name: &str, + success: &str, + field: &str, + kind: Type, + source: &ast::Expression, + ) -> Result { + let index = self + .definition_names + .resolve(name, self.source, source.span)? + .ok_or_else(|| self.error(source, "missing byte result type"))?; + let valid = matches!(&self.definitions[index].shape, Shape::Choice(variants) if variants.len() == 2 + && variants[0].name == success && !variants[0].error && variants[0].fields == [(field.into(), kind)] + && variants[1].name == "Failed" && variants[1].error && variants[1].fields == [("message".into(), Type::Text)]); + if !valid { + return Err(self.error( + source, + "byte result type does not match its intrinsic contract", + )); + } + Ok(Type::Named(index)) + } +} diff --git a/rust/src/compiler/check/calendar/mod.rs b/rust/src/compiler/check/calendar/mod.rs new file mode 100644 index 0000000..949aa54 --- /dev/null +++ b/rust/src/compiler/check/calendar/mod.rs @@ -0,0 +1,76 @@ +//! 只把官方 time 原语映射到对应名义结果;时间值没有隐式 Int 转换。 + +use super::Context; +use crate::compiler::model::*; +use crate::syntax::{ast, diagnostic::Diagnostic}; + +impl Context<'_> { + pub(super) fn calendar( + &mut self, + operation: CalendarOperation, + arguments: &[ast::Expression], + expected: Option<&Type>, + source: &ast::Expression, + ) -> Result { + if self.definition_names.packages[self.source] != "dever.time" { + return Err(Diagnostic::new( + "C006", + "calendar intrinsics belong to the official dever.time package", + source.span, + )); + } + let inputs = match operation { + CalendarOperation::UnixMillis + | CalendarOperation::MonotonicNanos + | CalendarOperation::Now => vec![], + CalendarOperation::ParseDateTime + | CalendarOperation::ParseDate + | CalendarOperation::ParseTime => vec![Type::Text], + CalendarOperation::FormatDateTime => vec![Type::DateTime], + CalendarOperation::FormatDate => vec![Type::Date], + CalendarOperation::FormatTime => vec![Type::Time], + CalendarOperation::Duration => vec![Type::Int], + CalendarOperation::Add | CalendarOperation::Subtract => { + vec![Type::DateTime, Type::Duration] + } + CalendarOperation::Difference => vec![Type::DateTime, Type::DateTime], + }; + if inputs.len() != arguments.len() { + return Err(self.error(source, "calendar argument count differs")); + } + let values = arguments + .iter() + .zip(inputs) + .map(|(argument, kind)| self.expression(argument, Some(&kind))) + .collect::, _>>()?; + let value_type = if matches!(operation, CalendarOperation::Duration) { + Type::Duration + } else { + let (name, kind) = match operation { + CalendarOperation::UnixMillis | CalendarOperation::MonotonicNanos => { + ("ClockResult", Type::Int) + } + CalendarOperation::Now + | CalendarOperation::ParseDateTime + | CalendarOperation::Add + | CalendarOperation::Subtract => ("DateTimeResult", Type::DateTime), + CalendarOperation::ParseDate => ("DateResult", Type::Date), + CalendarOperation::ParseTime => ("TimeResult", Type::Time), + CalendarOperation::FormatDateTime + | CalendarOperation::FormatDate + | CalendarOperation::FormatTime => ("TextResult", Type::Text), + CalendarOperation::Difference => ("DurationResult", Type::Duration), + CalendarOperation::Duration => unreachable!(), + }; + self.io_result(name, &[("Read", vec![("value", kind)])], source)? + }; + self.coerce( + Expression { + kind: Node::Calendar(operation, values), + value_type, + span: source.span, + }, + expected, + ) + } +} diff --git a/rust/src/compiler/check/call.rs b/rust/src/compiler/check/call.rs new file mode 100644 index 0000000..2a8c418 --- /dev/null +++ b/rust/src/compiler/check/call.rs @@ -0,0 +1,341 @@ +//! 解析普通调用、捕获操作、集合操作和受限标准库底层操作。 +//! 静态目标优先使用 handler 展开结果;参数按已检查的签名求值。 + +use super::Context; +use crate::compiler::model::*; +use crate::syntax::{ast, diagnostic::Diagnostic}; + +impl Context<'_> { + pub(super) fn call( + &mut self, + target: &ast::Expression, + arguments: &[ast::Expression], + expected: Option<&Type>, + source: &ast::Expression, + ) -> Result { + let name = target + .path() + .ok_or_else(|| self.error(target, "call target must be statically named"))?; + if name == "dever.json.decode" { + return self.decode_json(arguments, expected, source); + } + if matches!( + name.as_str(), + "assert" | "assertEq" | "secret" | "dever.test.advanceClock" | "dever.test.drainJobs" + ) { + return self.test_operation(&name, arguments, expected, source); + } + if let Some(operation) = upload::Operation::named(&name) { + return self.upload(operation, arguments, expected, source); + } + if matches!(name.as_str(), "dever.job.enqueue" | "dever.job.enqueueAt") { + return self.enqueue(name.ends_with("At"), arguments, expected, source); + } + if let Some(value) = self.database(&name, arguments, expected, source)? { + return Ok(value); + } + if let Some(operation) = auth::Operation::named(&name) { + return self.auth(operation, arguments, expected, source); + } + if let Some(operation) = authorization::Operation::named(&name) { + return self.authorization(operation, arguments, expected, source); + } + // 身份 ID 只在这两个固定入口接受期望的名义类型,不开放普通 Int 转换。 + if matches!(name.as_str(), "dever.auth.userId" | "dever.auth.tenantId") + && matches!(expected, Some(Type::Optional(inner)) if matches!(inner.as_ref(), Type::ModelId(_))) + { + let operation = if name.ends_with("userId") { + auth::Operation::UserId + } else { + auth::Operation::TenantId + }; + return self.auth(operation, arguments, expected, source); + } + if let Some(&index) = self.scope.targets.get(&source.span.start) { + let signature = &self.signatures[index]; + if arguments.len() != signature.inputs.len() { + return Err(self.error(source, "function argument count differs")); + } + let mut values = Vec::new(); + for (argument, (_, kind)) in arguments.iter().zip(&signature.inputs) { + if signature.name == "encode" + && self.definition_names.packages[signature.source] == "dever.json" + { + // 具体 wire 编码由编译器生成,失败传播继续复用普通源码函数。 + values.push(self.encode_json(argument, kind, source)?); + } else { + values.push(self.expression(argument, Some(kind))?); + } + } + return self.coerce( + Expression { + kind: Node::Call(index, values), + value_type: signature.output(), + span: source.span, + }, + expected, + ); + } + if name == "result" { + if arguments.len() != 1 { + return Err(self.error(source, "result requires one function call")); + } + return self.capture(&arguments[0], expected, source); + } + if let Some(operation) = Schedule::named(&name) { + return self.schedule(operation, arguments, expected, source); + } + if name == "stream" { + return self.stream(arguments, expected, source); + } + if name == "dever.system.taskTicks" { + return self.ticks(arguments, expected, source); + } + if name == "send" { + return self.send(arguments, expected, source); + } + if name == "receive" { + return self.receive(arguments, expected, source); + } + if name == "group" { + return self.group(arguments, expected, source); + } + if name == "timeout" { + return self.timeout(arguments, expected, source); + } + if name == "race" { + return self.race(arguments, expected, source); + } + if matches!(name.as_str(), "wait" | "stop") { + return self.join(name == "stop", arguments, expected, source); + } + if matches!( + name.as_str(), + "dever.system.timeSleep" | "dever.system.taskSleep" + ) { + let timer = if name == "dever.system.taskSleep" { + Timer::Delay + } else { + Timer::Sleep + }; + return self.timer(timer, arguments, expected, source); + } + if name == "close" { + let [argument] = arguments else { + return Err(self.error(source, "close requires one stream")); + }; + let value = self.expression(argument, None)?; + if !matches!( + value.value_type, + Type::Stream(_) | Type::AsyncStream(_) | Type::Channel(_) + ) { + return Err(self.error( + source, + "core close requires a Stream, AsyncStream or Channel", + )); + } + return self.coerce( + Expression { + kind: Node::Close(Box::new(value)), + value_type: Type::Unit, + span: source.span, + }, + expected, + ); + } + if let Some((index, variant)) = super::names::variant( + &name, + self.source, + self.definition_names, + self.definitions, + source.span, + )? { + return self.variant(index, variant, arguments, expected, source); + } + if arguments.len() == 1 && matches!(name.as_str(), "length" | "sum") { + let numeric = match expected { + Some(Type::Optional(inner)) => Some(inner.as_ref()), + kind => kind, + }; + let context = if name == "sum" + && matches!(&arguments[0].kind, ast::ExpressionKind::List(values) if values.is_empty()) + { + numeric + .filter(|kind| kind.numeric()) + .map(|kind| Type::List(Box::new(kind.clone()))) + } else { + None + }; + let value = self.expression(&arguments[0], context.as_ref())?; + let value_type = match (name.as_str(), &value.value_type) { + ("length", Type::Text | Type::Bytes | Type::List(_) | Type::Map(_, _)) => Type::Int, + ("sum", Type::List(inner)) if inner.numeric() => *inner.clone(), + _ => return Err(self.error(source, "invalid collection operation")), + }; + let kind = if name == "sum" { + Node::Sum(Box::new(value)) + } else { + Node::Length(Box::new(value)) + }; + return self.coerce( + Expression { + kind, + value_type, + span: source.span, + }, + expected, + ); + } + if let Some(operation) = Operation::named(&name) { + return self.collection(operation, arguments, expected, source); + } + if let Some(operation) = Access::named(&name) { + return self.access(operation, arguments, expected, source); + } + if let Some(operation) = CryptoOperation::named(&name) { + return self.crypto(operation, arguments, expected, source); + } + if let Some(operation) = CalendarOperation::named(&name) { + return self.calendar(operation, arguments, expected, source); + } + if let Some(operation) = StandardOperation::named(&name) { + return self.standard(operation, arguments, expected, source); + } + if let Some(operation) = BytesOperation::named(&name) { + return self.bytes(operation, arguments, expected, source); + } + if let Some(operation) = NetOperation::named(&name) { + return self.net(operation, arguments, expected, source); + } + if let Some(operation) = TlsOperation::named(&name) { + return self.tls(operation, arguments, expected, source); + } + if matches!( + name.as_str(), + "dever.system.httpServeLive" | "dever.system.httpServeLiveTls" + ) { + return self.http_live(name.ends_with("Tls"), arguments, expected, source); + } + let websocket = match name.as_str() { + "dever.system.wsAccept" => Some(HttpOperation::WsAccept), + "dever.system.wsConnect" => Some(HttpOperation::WsConnect), + "dever.system.wsOpen" => Some(HttpOperation::WsOpen), + "dever.system.wsSend" => Some(HttpOperation::WsSend), + "dever.system.wsReceive" => Some(HttpOperation::WsReceive), + "dever.system.wsMessages" => Some(HttpOperation::WsMessages), + "dever.system.wsClose" => Some(HttpOperation::WsClose), + _ => None, + }; + if let Some(operation) = websocket { + return self.websocket(operation, arguments, expected, source); + } + let reply = match name.as_str() { + "dever.system.httpRespond" => Some(HttpOperation::Respond), + "dever.system.httpStart" => Some(HttpOperation::Start), + "dever.system.httpWrite" => Some(HttpOperation::Write), + "dever.system.httpFinish" => Some(HttpOperation::Finish), + "dever.system.sseStart" => Some(HttpOperation::SseStart), + "dever.system.sseSend" => Some(HttpOperation::SseSend), + _ => None, + }; + if let Some(operation) = reply { + return self.http_reply(operation, arguments, expected, source); + } + let pool = match name.as_str() { + "dever.system.httpClient" => Some(HttpOperation::Client), + "dever.system.httpRequest" => Some(HttpOperation::Request), + "dever.system.httpOpen" => Some(HttpOperation::Open), + "dever.system.httpOpenStream" => Some(HttpOperation::OpenStream), + "dever.system.httpCloseClient" => Some(HttpOperation::Close), + _ => None, + }; + if let Some(operation) = pool { + return self.http_pool(operation, arguments, expected, source); + } + if matches!( + name.as_str(), + "dever.system.httpSend" | "dever.system.httpServe" | "dever.system.httpServeTls" + ) { + return self.http( + name != "dever.system.httpSend", + name == "dever.system.httpServeTls", + arguments, + expected, + source, + ); + } + if let Some(operation) = IoOperation::named(&name) { + return self.io(operation, arguments, expected, source); + } + if let Some(operation) = JsonOperation::named(&name) { + return self.json(operation, arguments, expected, source); + } + if matches!( + name.as_str(), + "dever.system.idFromText" | "dever.system.idToText" + ) { + if self.definition_names.packages[self.source] != "dever.id" { + return Err(Diagnostic::new( + "C006", + "Id operations belong to the official dever.id package", + source.span, + )); + } + let [argument] = arguments else { + return Err(self.error(source, "Id conversion requires one argument")); + }; + let from_text = name == "dever.system.idFromText"; + let input = if from_text { Type::Text } else { Type::Id }; + let value = Box::new(self.expression(argument, Some(&input))?); + return self.coerce( + Expression { + kind: if from_text { + Node::Id(value) + } else { + Node::IdText(value) + }, + value_type: if from_text { Type::Id } else { Type::Text }, + span: source.span, + }, + expected, + ); + } + Err(self.error( + source, + format!( + "unknown or unsupported function '{name}' with {} inputs", + arguments.len() + ), + )) + } + + pub(super) fn variant( + &mut self, + index: usize, + variant: usize, + arguments: &[ast::Expression], + expected: Option<&Type>, + source: &ast::Expression, + ) -> Result { + let Shape::Choice(variants) = &self.definitions[index].shape else { + unreachable!() + }; + let fields = variants[variant].fields.clone(); + if fields.len() != arguments.len() { + return Err(self.error(source, "choice payload argument count differs")); + } + let values = arguments + .iter() + .zip(&fields) + .map(|(argument, (_, kind))| self.expression(argument, Some(kind))) + .collect::, _>>()?; + self.coerce( + Expression { + kind: Node::Variant(index, variant, values), + value_type: Type::Named(index), + span: source.span, + }, + expected, + ) + } +} diff --git a/rust/src/compiler/check/channel/mod.rs b/rust/src/compiler/check/channel/mod.rs new file mode 100644 index 0000000..ffa8752 --- /dev/null +++ b/rust/src/compiler/check/channel/mod.rs @@ -0,0 +1,74 @@ +//! 通道元素是已知的具体可传递类型;发送移交值,接收按既有规则压平可空层。 + +use super::{Context, names}; +use crate::compiler::model::*; +use crate::syntax::{ast, diagnostic::Diagnostic}; + +impl Context<'_> { + pub(super) fn channel( + &mut self, + element: &ast::TypeRef, + capacity: &ast::Expression, + expected: Option<&Type>, + source: &ast::Expression, + ) -> Result { + let value_type = Type::Channel(Box::new(names::resolve(element, self.definition_names)?)); + names::validate(&value_type, self.definitions, element.span)?; + let capacity = self.expression(capacity, Some(&Type::Int))?; + self.coerce( + Expression { + value_type, + kind: Node::Channel(Box::new(capacity)), + span: source.span, + }, + expected, + ) + } + + pub(super) fn send( + &mut self, + arguments: &[ast::Expression], + expected: Option<&Type>, + source: &ast::Expression, + ) -> Result { + let [channel, value] = arguments else { + return Err(self.error(source, "send requires a Channel and value")); + }; + let channel = self.expression(channel, None)?; + let Type::Channel(element) = &channel.value_type else { + return Err(self.error(source, "send requires a Channel")); + }; + let value = self.expression(value, Some(element))?; + self.coerce( + Expression { + value_type: Type::Unit, + kind: Node::Send(Box::new(channel), Box::new(value)), + span: source.span, + }, + expected, + ) + } + + pub(super) fn receive( + &mut self, + arguments: &[ast::Expression], + expected: Option<&Type>, + source: &ast::Expression, + ) -> Result { + let [channel] = arguments else { + return Err(self.error(source, "receive requires one Channel")); + }; + let channel = self.expression(channel, None)?; + let Type::Channel(element) = &channel.value_type else { + return Err(self.error(source, "receive requires a Channel")); + }; + self.coerce( + Expression { + value_type: element.as_ref().clone().optional(), + kind: Node::Receive(Box::new(channel)), + span: source.span, + }, + expected, + ) + } +} diff --git a/rust/src/compiler/check/clauses/mod.rs b/rust/src/compiler/check/clauses/mod.rs new file mode 100644 index 0000000..cc665ad --- /dev/null +++ b/rust/src/compiler/check/clauses/mod.rs @@ -0,0 +1,229 @@ +//! 解析分句条件,并证明互斥、穷尽和可达。 +//! 分句源顺序不代表优先级;other 是同一输入轴显式条件的补集。 + +mod partition; + +use super::names; +use crate::compiler::model::*; +use crate::syntax::{ast, diagnostic::Diagnostic}; + +pub(super) fn range( + clauses: &[Clause], + clause: &Clause, + position: usize, + kind: &Type, + definitions: &[Definition], + remaining: &mut usize, +) -> Result, Diagnostic> { + let kind = match kind { + Type::Optional(inner) => inner, + kind => kind, + }; + if !matches!(kind, Type::Int | Type::Decimal) { + return Ok(None); + } + match clause.patterns[position].inner() { + Pattern::Number(value) => return Ok(Some(numeric::Range::point(*value))), + Pattern::Range(bounds) => return Ok(numeric::Range::bounds(kind, bounds)), + Pattern::Any => return Ok(Some(numeric::Range::full(kind))), + Pattern::Other => {} + _ => return Ok(None), + } + let column = clauses + .iter() + .map(|clause| clause.patterns[position].inner()) + .collect::>(); + let cells = partition::cells(kind, &column, definitions); + *remaining = remaining + .checked_sub(cells.len().saturating_mul(column.len())) + .ok_or_else(|| { + Diagnostic::new("C015", "range proof exceeds its work limit", clause.span) + })?; + let mut range = None; + for (index, cell) in cells.iter().enumerate() { + let accepted = !column + .iter() + .any(|pattern| partition::matches(pattern, cell)); + if accepted && let partition::Cell::Number(minimum) = cell { + let maximum = match cells.get(index + 1) { + Some(partition::Cell::Number(next)) => { + next.predecessor().expect("ordered numeric cells") + } + _ => numeric::Number::maximum(kind), + }; + let actual = numeric::Range { + minimum: *minimum, + maximum, + }; + range = Some(range.map_or(actual, |previous: numeric::Range| previous.hull(actual))); + } + } + Ok(range) +} + +pub(super) fn resolve( + group: &[ast::Function], + signature: &Signature, + names: &super::package::Types, + definitions: &[Definition], +) -> Result>, Diagnostic> { + group.iter().map(|function| { + function.inputs.iter().zip(&signature.inputs).map(|(input, (_, kind))| { + let present = match kind { + Type::Optional(inner) => inner.as_ref(), + kind => kind, + }; + let mut pattern = match &input.pattern { + ast::Pattern::Handler(_, _) => unreachable!("handler parameters are not clause axes"), + ast::Pattern::Other => Pattern::Other, + ast::Pattern::Literal(ast::Literal::Null) => Pattern::Null, + ast::Pattern::Literal(ast::Literal::Number(value)) => Pattern::Number( + numeric::Number::parse(value, present) + .map_err(|error| Diagnostic::new("C008", error, input.span))? + ), + ast::Pattern::Literal(value) => Pattern::Literal(value.clone()), + ast::Pattern::Typed(reference, bounds) => { + if let Some((index, variant)) = names::variant_type(reference, names, definitions)? { + let Shape::Choice(variants) = &definitions[index].shape else { unreachable!() }; + if !variants[variant].fields.is_empty() || !bounds.is_empty() || reference.nullable { + return Err(Diagnostic::new("C005", "variant pattern requires exactly its payload bindings", input.span)); + } + Pattern::Variant(variant) + } else if bounds.is_empty() { + Pattern::Any + } else { + if !matches!(present, Type::Int | Type::Decimal) || reference.nullable { + return Err(Diagnostic::new("C005", "range patterns require nonnullable Int or Decimal", input.span)); + } + Pattern::Range(bounds.iter().map(|bound| { + Ok(numeric::Bound { + operator: bound.operator.clone(), + value: numeric::Number::parse(&bound.value, present) + .map_err(|error| Diagnostic::new("C008", error, input.span))?, + }) + }).collect::>()?) + } + } + ast::Pattern::Variant(path, bindings) => { + let (index, variant) = names::variant_type(path, names, definitions)? + .ok_or_else(|| Diagnostic::new("C004", "unknown choice variant", input.span))?; + let Shape::Choice(variants) = &definitions[index].shape else { unreachable!() }; + if bindings.len() != variants[variant].fields.len() { + return Err(Diagnostic::new("C005", "variant payload binding count differs", input.span)); + } + Pattern::Variant(variant) + } + }; + let full_nullable = matches!(&input.pattern, ast::Pattern::Typed(reference, _) if reference.nullable); + if matches!(kind, Type::Optional(_)) && !full_nullable && !matches!(pattern, Pattern::Null | Pattern::Other) { + pattern = Pattern::Present(Box::new(pattern)); + } + Ok(pattern) + }).collect() + }).collect() +} + +pub(super) fn validate( + patterns: &[Vec], + signature: &Signature, + definitions: &[Definition], + clauses: &[ast::Function], +) -> Result<(), Diagnostic> { + let partitions = signature + .inputs + .iter() + .enumerate() + .map(|(position, (_, kind))| { + let column = patterns + .iter() + .map(|row| &row[position]) + .collect::>(); + partition::cells(kind, &column, definitions) + }) + .collect::>(); + let mut reached = vec![false; clauses.len()]; + coverage( + 0, + &(0..clauses.len()).collect::>(), + &partitions, + patterns, + clauses, + &mut reached, + )?; + if let Some(index) = reached.iter().position(|value| !value) { + return Err(Diagnostic::new( + "C009", + "unreachable function clause", + clauses[index].span, + )); + } + Ok(()) +} + +fn coverage( + position: usize, + candidates: &[usize], + partitions: &[Vec], + patterns: &[Vec], + clauses: &[ast::Function], + reached: &mut [bool], +) -> Result<(), Diagnostic> { + if candidates.is_empty() { + return Err(Diagnostic::new( + "C009", + "function clauses do not cover every input", + clauses[0].span, + )); + } + if position == partitions.len() { + if candidates.len() != 1 { + return Err(Diagnostic::new( + "C009", + "function clauses overlap", + clauses[candidates[1]].span, + )); + } + reached[candidates[0]] = true; + return Ok(()); + } + for cell in &partitions[position] { + let selected = candidates + .iter() + .copied() + .filter(|index| { + let pattern = &patterns[*index][position]; + if matches!(pattern, Pattern::Other) { + !patterns + .iter() + .any(|row| partition::matches(&row[position], cell)) + } else { + partition::matches(pattern, cell) + } + }) + .collect::>(); + coverage( + position + 1, + &selected, + partitions, + patterns, + clauses, + reached, + )?; + } + Ok(()) +} + +pub(super) fn accepts_variant( + pattern: &Pattern, + variant: usize, + column: impl Iterator, +) -> bool { + let cell = partition::Cell::Variant(variant); + if matches!(pattern, Pattern::Other) { + !column + .into_iter() + .any(|pattern| partition::matches(&pattern, &cell)) + } else { + partition::matches(pattern, &cell) + } +} diff --git a/rust/src/compiler/check/clauses/partition.rs b/rust/src/compiler/check/clauses/partition.rs new file mode 100644 index 0000000..01acc27 --- /dev/null +++ b/rust/src/compiler/check/clauses/partition.rs @@ -0,0 +1,118 @@ +//! 把输入值域划成对当前分句等价的有限单元。 +//! 数值边界使用真实的相邻可表示值,最大值之后不再创建单元。 + +use crate::compiler::model::*; +use crate::syntax::ast::Literal; + +pub(super) enum Cell { + Number(numeric::Number), + Bool(bool), + Text(Option), + Variant(usize), + Null, + Whole, +} + +pub(super) fn cells(kind: &Type, patterns: &[&Pattern], definitions: &[Definition]) -> Vec { + // 所有分句都接受整列时,Bool、Choice、可空类型也只有一个匹配等价类。 + if patterns + .iter() + .all(|pattern| matches!(pattern, Pattern::Any)) + { + return vec![Cell::Whole]; + } + if let Type::Optional(inner) = kind { + let patterns = patterns + .iter() + .map(|pattern| pattern.inner()) + .collect::>(); + let mut result = cells(inner, &patterns, definitions); + result.push(Cell::Null); + return result; + } + match kind { + Type::Bool => vec![Cell::Bool(false), Cell::Bool(true)], + Type::Int | Type::Decimal => { + // 边界与后继划分所有可能条件;Decimal 不经过二进制浮点近似。 + let mut edges = vec![numeric::Number::minimum(kind)]; + for pattern in patterns { + match pattern { + Pattern::Number(value) => { + edges.push(*value); + edges.extend(value.successor()); + } + Pattern::Range(bounds) => { + for bound in bounds { + let edge = if matches!(bound.operator.as_str(), ">" | "<=") { + bound.value.successor() + } else { + Some(bound.value) + }; + edges.extend(edge); + } + } + _ => {} + } + } + edges + .sort_by(|left, right| left.partial_cmp(right).expect("finite values of one type")); + edges.dedup(); + edges.into_iter().map(Cell::Number).collect() + } + Type::Float => { + // whole 还代表 NaN、无穷和所有未列出的值;正负零比较相等。 + let mut values = patterns + .iter() + .filter_map(|pattern| match pattern { + Pattern::Number(value) => Some(*value), + _ => None, + }) + .collect::>(); + values.sort_by(|left, right| left.partial_cmp(right).expect("finite Float literals")); + values.dedup(); + values + .into_iter() + .map(Cell::Number) + .chain([Cell::Whole]) + .collect() + } + Type::Text => { + let mut values = patterns + .iter() + .filter_map(|pattern| match pattern { + Pattern::Literal(Literal::Text(value)) => Some(value.clone()), + _ => None, + }) + .collect::>(); + values.sort(); + values.dedup(); + values + .into_iter() + .map(|value| Cell::Text(Some(value))) + .chain([Cell::Text(None)]) + .collect() + } + Type::Named(index) => match &definitions[*index].shape { + Shape::Choice(variants) => (0..variants.len()).map(Cell::Variant).collect(), + Shape::Record(_) => vec![Cell::Whole], + }, + _ => vec![Cell::Whole], + } +} + +pub(super) fn matches(pattern: &Pattern, cell: &Cell) -> bool { + match (pattern, cell) { + (Pattern::Any, _) => true, + (Pattern::Null, Cell::Null) => true, + (Pattern::Present(_), Cell::Null) => false, + (Pattern::Present(inner), cell) => matches(inner, cell), + (Pattern::Range(bounds), Cell::Number(value)) => { + bounds.iter().all(|bound| bound.matches(*value)) + } + (Pattern::Number(value), Cell::Number(actual)) => value == actual, + (Pattern::Literal(Literal::Bool(value)), Cell::Bool(actual)) => value == actual, + (Pattern::Literal(Literal::Text(value)), Cell::Text(Some(actual))) => value == actual, + (Pattern::Variant(value), Cell::Variant(actual)) => value == actual, + _ => false, + } +} diff --git a/rust/src/compiler/check/collection/mod.rs b/rust/src/compiler/check/collection/mod.rs new file mode 100644 index 0000000..86db392 --- /dev/null +++ b/rust/src/compiler/check/collection/mod.rs @@ -0,0 +1,144 @@ +//! 校验集合元素、静态 handler、上下文和归约状态的类型合同。 +//! reduceUntil 必须返回下一状态和名为 stop 的 Bool。 + +use super::Context; +use crate::compiler::model::*; +use crate::syntax::{ast, diagnostic::Diagnostic}; + +pub(super) fn handler_arity(operation: Operation, arguments: usize) -> Option { + if operation == Operation::ParallelEach { + match arguments { + 3 => Some(1), + 4 => Some(2), + _ => None, + } + } else if operation.reduction() { + (arguments == 3).then_some(2) + } else { + match arguments { + 2 => Some(1), + 3 => Some(2), + _ => None, + } + } +} + +impl Context<'_> { + pub(super) fn collection( + &mut self, + operation: Operation, + arguments: &[ast::Expression], + expected: Option<&Type>, + source: &ast::Expression, + ) -> Result { + let arity = handler_arity(operation, arguments.len()) + .ok_or_else(|| self.error(source, "invalid collection argument count"))?; + let target = &arguments[0]; + let name = target + .path() + .ok_or_else(|| self.error(target, "handler must be a named function"))?; + if self.variables.iter().any(|(variable, _)| variable == &name) { + return Err(self.error(target, "a runtime value cannot be a handler")); + } + let handler = self + .scope + .targets + .get(&target.span.start) + .copied() + .ok_or_else(|| self.error(target, "handler name or input count does not match"))?; + let signature = self.signatures[handler].clone(); + if signature.inputs.len() != arity { + return Err(self.error(target, "handler input count does not match")); + } + let item = &signature.inputs[0].1; + let source_sequence = &arguments[1]; + let context = (matches!( + source_sequence.kind, + ast::ExpressionKind::List(_) | ast::ExpressionKind::Call(_, _) + ) && !self.scope.targets.contains_key(&source_sequence.span.start)) + .then(|| { + let kind = Box::new(item.clone()); + if matches!(&source_sequence.kind, ast::ExpressionKind::Call(target, _) if target.path().as_deref() == Some("stream")) { Type::AsyncStream(kind) } else { Type::List(kind) } + }); + let sequence = self.expression(source_sequence, context.as_ref())?; + let valid = match &sequence.value_type { + Type::List(inner) => inner.as_ref() == item, + Type::Stream(inner) | Type::AsyncStream(inner) => { + inner.as_ref() == item + && (operation.reduction() + || (matches!(operation, Operation::Each | Operation::ParallelEach) + && signature.outputs.is_empty())) + } + Type::Bytes => { + *item == Type::Int + && matches!( + operation, + Operation::Each + | Operation::ParallelEach + | Operation::Reduce + | Operation::ReduceUntil + ) + } + _ => false, + }; + if !valid { + return Err(self.error( + source_sequence, + "sequence type or operation does not match its handler", + )); + } + let parallel = operation == Operation::ParallelEach; + if parallel + && (!super::task::transferable(&signature, self.definitions) + || signature.effects.handler) + { + return Err(Diagnostic::new( + "C013", + "parallel handler inputs and failures must be transferable with known effects", + source.span, + )); + } + let limit = if parallel { + Some(self.expression(&arguments[2], Some(&Type::Int))?) + } else { + None + }; + let extra = arguments + .get(if parallel { 3 } else { 2 }) + .map(|value| self.expression(value, Some(&signature.inputs[1].1))) + .transpose()?; + let value_type = match (operation, signature.outputs.as_slice()) { + (Operation::Each | Operation::ParallelEach, []) => Type::Unit, + (Operation::Each, [(_, value)]) => Type::List(Box::new(value.clone())), + (Operation::Filter, [(_, Type::Bool)]) => sequence.value_type.clone(), + (Operation::Find, [(_, Type::Bool)]) => Type::Optional(Box::new(item.clone())), + (Operation::Sum, [(_, value)]) if value.numeric() => value.clone(), + (Operation::Reduce, [(_, value)]) if *value == signature.inputs[1].1 => value.clone(), + (Operation::ReduceUntil, [(_, value), (stop, Type::Bool)]) + if *value == signature.inputs[1].1 && stop == "stop" => + { + value.clone() + } + _ => { + return Err(self.error( + target, + "handler outputs do not match the collection operation", + )); + } + }; + self.coerce( + Expression { + kind: Node::Collection(Box::new(Collection { + operation, + handler, + sequence, + limit, + extra, + })), + value_type, + span: source.span, + }, + expected, + ) + } +} diff --git a/rust/src/compiler/check/constants.rs b/rust/src/compiler/check/constants.rs new file mode 100644 index 0000000..49acdc5 --- /dev/null +++ b/rust/src/compiler/check/constants.rs @@ -0,0 +1,65 @@ +//! 折叠已经确定类型的常量算术,并提前报告必然发生的故障。 +//! 复用与运行时一致的受检整数和 Decimal 运算,不进行浮点重排。 + +use crate::compiler::model::{Expression, Node, Type}; +use crate::syntax::{ast::Literal, diagnostic::Diagnostic}; +use dever_runtime::value::decimal::Decimal; + +pub(super) fn fold(mut expression: Expression) -> Result { + let Node::Binary(operator, left, right) = &expression.kind else { + return Ok(expression); + }; + let value = match expression.value_type { + Type::Int => { + let (Some(left), Some(right)) = (integer(left), integer(right)) else { + return Ok(expression); + }; + match operator.as_str() { + "+" => left.checked_add(right), + "-" => left.checked_sub(right), + "*" => left.checked_mul(right), + "//" => left.checked_div(right), + "%" => left.checked_rem(right), + _ => unreachable!("checked Int operator"), + } + .map(|value| value.to_string()) + } + Type::Decimal => { + let (Some(left), Some(right)) = (decimal(left), decimal(right)) else { + return Ok(expression); + }; + let mut context = Decimal::context(); + match operator.as_str() { + "+" => left.checked_add(right, &mut context), + "-" => left.checked_sub(right, &mut context), + "*" => left.checked_mul(right, &mut context), + "/" => left.checked_div(right, &mut context), + _ => unreachable!("checked Decimal operator"), + } + .map(|value| value.to_string()) + } + _ => return Ok(expression), + } + .ok_or_else(|| Diagnostic::new("C008", "statically known arithmetic fault", expression.span))?; + expression.kind = Node::Literal(Literal::Number(value)); + Ok(expression) +} + +fn integer(expression: &Expression) -> Option { + match &expression.kind { + Node::Literal(Literal::Number(value)) if expression.value_type == Type::Int => { + value.parse().ok() + } + _ => None, + } +} + +fn decimal(expression: &Expression) -> Option { + match &expression.kind { + Node::Literal(Literal::Number(value)) => { + Some(Decimal::parse(value).expect("checked Decimal literal")) + } + Node::Decimal(value) => integer(value).map(Decimal::from_int), + _ => None, + } +} diff --git a/rust/src/compiler/check/crypto/mod.rs b/rust/src/compiler/check/crypto/mod.rs new file mode 100644 index 0000000..c252986 --- /dev/null +++ b/rust/src/compiler/check/crypto/mod.rs @@ -0,0 +1,66 @@ +//! Secret 只进入既有批准的密码学接收点;参数和名义结果必须匹配官方库。 + +use super::Context; +use crate::compiler::model::*; +use crate::syntax::{ast, diagnostic::Diagnostic}; + +impl Context<'_> { + pub(super) fn crypto( + &mut self, + operation: CryptoOperation, + arguments: &[ast::Expression], + expected: Option<&Type>, + source: &ast::Expression, + ) -> Result { + if self.definition_names.packages[self.source] != "dever.crypto" { + return Err(Diagnostic::new( + "C006", + "crypto intrinsics belong to the official dever.crypto package", + source.span, + )); + } + let inputs = match operation { + CryptoOperation::Token => vec![Type::Int], + CryptoOperation::PasswordHash => vec![Type::Secret], + CryptoOperation::PasswordVerify => vec![Type::Secret, Type::Text], + CryptoOperation::Sha256 => vec![Type::Bytes], + CryptoOperation::HmacSha256 => vec![Type::Secret, Type::Bytes], + CryptoOperation::ConstantTimeEq => vec![Type::Bytes, Type::Bytes], + }; + if inputs.len() != arguments.len() { + return Err(self.error(source, "crypto argument count differs")); + } + let values = arguments + .iter() + .zip(inputs) + .map(|(value, kind)| self.expression(value, Some(&kind))) + .collect::, _>>()?; + let value_type = match operation { + CryptoOperation::Token => self.io_result( + "TokenResult", + &[("Ready", vec![("value", Type::Secret)])], + source, + )?, + CryptoOperation::PasswordHash => self.io_result( + "HashResult", + &[("Ready", vec![("value", Type::Text)])], + source, + )?, + CryptoOperation::PasswordVerify => self.io_result( + "VerifyResult", + &[("Ready", vec![("value", Type::Bool)])], + source, + )?, + CryptoOperation::Sha256 | CryptoOperation::HmacSha256 => Type::Bytes, + CryptoOperation::ConstantTimeEq => Type::Bool, + }; + self.coerce( + Expression { + value_type, + kind: Node::Crypto(operation, values), + span: source.span, + }, + expected, + ) + } +} diff --git a/rust/src/compiler/check/database/condition.rs b/rust/src/compiler/check/database/condition.rs new file mode 100644 index 0000000..c86e57a --- /dev/null +++ b/rust/src/compiler/check/database/condition.rs @@ -0,0 +1,142 @@ +//! 查询函数只在 where 内解释;字段和 SQL 固定,输入按字段类型绑定。 + +use super::*; + +impl super::super::Context<'_> { + pub(super) fn condition( + &mut self, + source: &ast::Expression, + fields: &[(String, Type)], + values: &mut Vec, + bound_fields: &mut Vec, + ) -> Result<(String, String), Diagnostic> { + match &source.kind { + ast::ExpressionKind::Unary(operator, value) if operator == "not" => { + let (sqlite, postgres) = self.condition(value, fields, values, bound_fields)?; + Ok((format!("(NOT {sqlite})"), format!("(NOT {postgres})"))) + } + ast::ExpressionKind::Binary(operator, left, right) + if matches!(operator.as_str(), "and" | "or") => + { + let (left_sqlite, left_postgres) = + self.condition(left, fields, values, bound_fields)?; + let (right_sqlite, right_postgres) = + self.condition(right, fields, values, bound_fields)?; + let operator = operator.to_ascii_uppercase(); + Ok(( + format!("({left_sqlite} {operator} {right_sqlite})"), + format!("({left_postgres} {operator} {right_postgres})"), + )) + } + ast::ExpressionKind::Binary(operator, left, right) + if matches!(operator.as_str(), "==" | "!=" | "<" | "<=" | ">" | ">=") => + { + let position = self.condition_field(left, fields)?; + let (name, kind) = &fields[position]; + values.push(self.expression(right, Some(kind))?); + bound_fields.push(position); + let (sqlite, postgres) = match operator.as_str() { + "==" => ("IS", "IS NOT DISTINCT FROM"), + "!=" => ("IS NOT", "IS DISTINCT FROM"), + other => (other, other), + }; + let parameter = values.len(); + Ok(( + format!("(\"{name}\" {sqlite} ?{parameter})"), + format!("(\"{name}\" {postgres} ${parameter})"), + )) + } + ast::ExpressionKind::Call(target, arguments) => { + let ast::ExpressionKind::Name(operation) = &target.kind else { + return Err(self.error(target, "where requires a static query function")); + }; + let arity = match operation.as_str() { + "in" | "contains" | "startsWith" | "endsWith" => 2, + "between" => 3, + _ => return Err(self.error(target, "unknown Model query function")), + }; + if arguments.len() != arity { + return Err(self.error(source, "wrong number of query function arguments")); + } + let position = self.condition_field(&arguments[0], fields)?; + let (name, kind) = &fields[position]; + let operands = if operation == "in" { + let ast::ExpressionKind::List(items) = &arguments[1].kind else { + return Err(self.error(&arguments[1], "in requires a direct value list")); + }; + if !(1..=100).contains(&items.len()) { + return Err( + self.error(&arguments[1], "in requires between 1 and 100 values") + ); + } + items.as_slice() + } else { + &arguments[1..] + }; + if matches!(operation.as_str(), "contains" | "startsWith" | "endsWith") + && !matches!(kind, Type::Text) + && !matches!(kind, Type::Optional(inner) if **inner == Type::Text) + { + return Err(self.error(&arguments[0], "text query requires a Text field")); + } + let start = values.len() + 1; + for operand in operands { + values.push(self.expression(operand, Some(kind))?); + bound_fields.push(position); + } + let column = format!("\"{name}\""); + let (sqlite, postgres) = match operation.as_str() { + "in" => { + let sqlite = (start..=values.len()) + .map(|index| format!("?{index}")) + .collect::>() + .join(","); + let postgres = (start..=values.len()) + .map(|index| format!("${index}")) + .collect::>() + .join(","); + ( + format!("{column} IN ({sqlite})"), + format!("{column} IN ({postgres})"), + ) + } + "between" => ( + format!("{column} BETWEEN ?{start} AND ?{}", start + 1), + format!("{column} BETWEEN ${start} AND ${}", start + 1), + ), + "contains" => ( + format!("instr(CAST({column} AS BLOB), CAST(?{start} AS BLOB)) > 0"), + format!("POSITION(${start} IN {column}) > 0"), + ), + "startsWith" => ( + format!("instr(CAST({column} AS BLOB), CAST(?{start} AS BLOB)) = 1"), + format!("LEFT({column}, LENGTH(${start})) = ${start}"), + ), + "endsWith" => ( + format!( + "substr(CAST({column} AS BLOB), length(CAST({column} AS BLOB)) - length(CAST(?{start} AS BLOB)) + 1) = CAST(?{start} AS BLOB)" + ), + format!("RIGHT({column}, LENGTH(${start})) = ${start}"), + ), + _ => unreachable!(), + }; + Ok((format!("({sqlite})"), format!("({postgres})"))) + } + _ => Err(self.error(source, "unsupported static Model condition")), + } + } + + fn condition_field( + &self, + source: &ast::Expression, + fields: &[(String, Type)], + ) -> Result { + let ast::ExpressionKind::Name(name) = &source.kind else { + return Err(self.error(source, "condition must compare a Model field")); + }; + fields + .iter() + .position(|(field, _)| field == name) + .ok_or_else(|| self.error(source, "unknown condition field")) + } +} diff --git a/rust/src/compiler/check/database/constant.rs b/rust/src/compiler/check/database/constant.rs new file mode 100644 index 0000000..a021dda --- /dev/null +++ b/rust/src/compiler/check/database/constant.rs @@ -0,0 +1,144 @@ +//! 默认值与 Seed 先转换为受检存储常量,不把非法值留到启动或 SQL 执行阶段。 + +use super::*; +use dever_runtime::{ + time::calendar::{Date, DateTime, Time}, + value::{decimal::Decimal, uuid::Uuid}, + wire::json::Json, +}; +use persistence::{Column, Value}; + +pub(super) fn check( + source: &ast::Expression, + kind: &Type, + column: &Column, + names: &Types, +) -> Result { + let invalid = || { + Diagnostic::new( + "C014", + "invalid constant for persistent column", + source.span, + ) + }; + if let Type::Optional(inner) = kind { + if matches!( + source.kind, + ast::ExpressionKind::Literal(ast::Literal::Null) + ) { + return Ok(Value::Null); + } + return check(source, inner, column, names); + } + if let (Type::Named(index), Some(variants), Some(path)) = (kind, &column.choice, source.path()) + { + let Some((prefix, variant)) = path.rsplit_once('.') else { + return Err(invalid()); + }; + if names.resolve(prefix, source.span.source, source.span)? != Some(*index) + || !variants.iter().any(|(name, _)| name == variant) + { + return Err(invalid()); + } + return Ok(Value::Text(variant.into())); + } + let value = match &source.kind { + ast::ExpressionKind::Literal(value) => value.clone(), + ast::ExpressionKind::Unary(operator, value) if operator == "-" => match &value.kind { + ast::ExpressionKind::Literal(ast::Literal::Number(value)) => { + ast::Literal::Number(format!("-{value}")) + } + _ => return Err(invalid()), + }, + _ => return Err(invalid()), + }; + Ok(match (kind, value) { + (Type::Bool, ast::Literal::Bool(value)) => Value::Integer(i64::from(value)), + (Type::Int | Type::ModelId(_) | Type::Duration, ast::Literal::Number(value)) => { + Value::Integer(value.parse().map_err(|_| invalid())?) + } + (Type::Float, ast::Literal::Number(value)) => { + let value = value.parse::().map_err(|_| invalid())?; + if !value.is_finite() { + return Err(invalid()); + } + Value::Real(value) + } + (Type::Decimal, ast::Literal::Number(value)) => { + let value = Decimal::parse(&value).map_err(|_| invalid())?; + let (precision, scale) = column.decimal.expect("checked Decimal column"); + let value = dec::Decimal128::from_be_bytes(value.to_be_bytes()); + let coefficient = value.coefficient(); + let shift = value.exponent() + scale as i32; + let scaled = if coefficient == 0 { + 0 + } else if shift >= 0 { + coefficient + .checked_mul(10_i128.checked_pow(shift as u32).ok_or_else(invalid)?) + .ok_or_else(invalid)? + } else { + let divisor = 10_i128 + .checked_pow(shift.unsigned_abs()) + .ok_or_else(invalid)?; + if coefficient % divisor != 0 { + return Err(invalid()); + } + coefficient / divisor + }; + let limit = 10_i128.pow(precision); + if scaled <= -limit || scaled >= limit { + return Err(invalid()); + } + Value::Text(format!( + "{:0width$}", + scaled + limit, + width = precision as usize + 1 + )) + } + (Type::Text, ast::Literal::Text(value)) => { + let value = match column.text { + persistence::TextMode::Raw => value, + persistence::TextMode::Trim => value.trim().to_owned(), + persistence::TextMode::Lower => value.trim().to_lowercase(), + persistence::TextMode::Upper => value.trim().to_uppercase(), + }; + if let Some((min, max)) = column.limits + && !(min as usize..=max as usize).contains(&value.chars().count()) + { + return Err(invalid()); + } + Value::Text(value) + } + (Type::Uuid, ast::Literal::Text(value)) => { + Value::Blob(Uuid::parse(&value).ok_or_else(invalid)?.as_bytes().to_vec()) + } + (Type::DateTime, ast::Literal::Text(value)) => { + Value::Integer(DateTime::parse(&value).map_err(|_| invalid())?.millis()) + } + (Type::Date, ast::Literal::Text(value)) => { + Value::Integer(Date::parse(&value).map_err(|_| invalid())?.millis()) + } + (Type::Time, ast::Literal::Text(value)) => { + Value::Integer(Time::parse(&value).map_err(|_| invalid())?.millis()) + } + (Type::DateTime, ast::Literal::Number(value)) => Value::Integer( + DateTime::from_millis(value.parse().map_err(|_| invalid())?) + .map_err(|_| invalid())? + .millis(), + ), + (Type::Date, ast::Literal::Number(value)) => Value::Integer( + Date::from_millis(value.parse().map_err(|_| invalid())?) + .map_err(|_| invalid())? + .millis(), + ), + (Type::Time, ast::Literal::Number(value)) => Value::Integer( + Time::from_millis(value.parse().map_err(|_| invalid())?) + .map_err(|_| invalid())? + .millis(), + ), + (Type::Json, ast::Literal::Text(value)) => { + Value::Text(Json::parse(&value).map_err(|_| invalid())?.as_str().into()) + } + _ => return Err(invalid()), + }) +} diff --git a/rust/src/compiler/check/database/filter.rs b/rust/src/compiler/check/database/filter.rs new file mode 100644 index 0000000..ec0c4be --- /dev/null +++ b/rust/src/compiler/check/database/filter.rs @@ -0,0 +1,185 @@ +//! 查询字段、条件树和排序在编译时确定,右侧值沿普通类型检查进入参数绑定。 + +use super::*; +use crate::compiler::model::persistence::Operation; + +pub(super) struct Filter { + pub condition: String, + pub postgres_condition: String, + pub order: String, + pub order_fields: Vec<(usize, bool)>, + pub values: Vec, + pub fields: Vec, + pub relations: Vec, +} + +impl super::super::Context<'_> { + pub(super) fn model_filter( + &mut self, + arguments: &[ast::Expression], + model: usize, + operation: Operation, + source: &ast::Expression, + ) -> Result { + let fields = self.models[model].fields.clone(); + let entries = match arguments { + [] => &[][..], + [block] => match &block.kind { + ast::ExpressionKind::Map(entries) => entries.as_slice(), + _ => return Err(self.error(block, "query requires a direct field block")), + }, + _ => return Err(self.error(source, "query accepts at most one field block")), + }; + let mut seen = std::collections::HashSet::new(); + let mut filter = String::new(); + let mut postgres_filter = String::new(); + let mut order = String::from("\"id\" DESC"); + let mut order_fields = vec![(0, false)]; + let mut values = Vec::new(); + let mut bound_fields = Vec::new(); + let mut page = None; + let mut size = None; + let mut after = None; + let mut relations = Vec::new(); + for (key, value) in entries { + let ast::ExpressionKind::Name(name) = &key.kind else { + return Err(self.error(key, "query option must be named")); + }; + if !seen.insert(name) { + return Err(self.error(key, "duplicate query option")); + } + match name.as_str() { + "with" + if matches!( + operation, + Operation::First | Operation::List | Operation::Cursor + ) => + { + let ast::ExpressionKind::List(items) = &value.kind else { + return Err(self.error(value, "with requires a static relation list")); + }; + for item in items { + let ast::ExpressionKind::Name(name) = &item.kind else { + return Err( + self.error(item, "with accepts one-level relation names only") + ); + }; + let relation = self.models[model] + .relations + .iter() + .position(|relation| relation.name == *name) + .ok_or_else(|| self.error(item, "unknown Model relation"))?; + if relations.contains(&relation) { + return Err(self.error(item, "duplicate Model relation")); + } + relations.push(relation); + } + } + "where" => { + let (sqlite, postgres) = + self.condition(value, &fields, &mut values, &mut bound_fields)?; + filter = format!(" WHERE {sqlite}"); + postgres_filter = format!(" WHERE {postgres}"); + } + "order" + if matches!( + operation, + Operation::First | Operation::List | Operation::Cursor | Operation::Stream + ) => + { + let ast::ExpressionKind::List(items) = &value.kind else { + return Err(self.error(value, "order requires a static field list")); + }; + let mut columns = Vec::new(); + let mut ordered = std::collections::HashSet::new(); + order_fields.clear(); + for item in items { + let ast::ExpressionKind::Field(base, direction) = &item.kind else { + return Err(self.error(item, "order requires field.asc or field.desc")); + }; + let ast::ExpressionKind::Name(name) = &base.kind else { + return Err(self.error(item, "order requires a Model field")); + }; + if !fields.iter().any(|(field, _)| field == name) + || !matches!(direction.as_str(), "asc" | "desc") + || !ordered.insert(name.as_str()) + { + return Err(self.error(item, "unknown or duplicate ordering field")); + } + columns.push(format!("\"{name}\" {}", direction.to_ascii_uppercase())); + order_fields.push(( + fields.iter().position(|(field, _)| field == name).unwrap(), + direction == "asc", + )); + } + if !ordered.contains("id") { + columns.push("\"id\" DESC".into()); + order_fields.push((0, false)); + } + order = columns.join(","); + } + "page" if operation == Operation::List => page = Some(value), + "size" if matches!(operation, Operation::List | Operation::Cursor) => { + size = Some(value) + } + "after" if operation == Operation::Cursor => after = Some(value), + _ => { + return Err(Diagnostic::new( + "U001", + "unsupported Model query option", + key.span, + )); + } + } + } + if operation == Operation::List { + for (value, default) in [(page, "1"), (size, "20")] { + values.push(match value { + Some(value) => self.expression(value, Some(&Type::Int))?, + None => Expression { + kind: Node::Literal(ast::Literal::Number(default.into())), + value_type: Type::Int, + span: source.span, + }, + }); + } + } + if operation == Operation::Cursor { + if order_fields + .iter() + .any(|(field, _)| matches!(fields[*field].1, Type::Optional(_))) + { + return Err(self.error( + source, + "cursor order does not support nullable Model fields", + )); + } + let record = Type::Optional(Box::new(Type::Named(self.models[model].definition))); + values.push(match after { + Some(value) => self.expression(value, Some(&record))?, + None => Expression { + kind: Node::Literal(ast::Literal::Null), + value_type: record, + span: source.span, + }, + }); + values.push(match size { + Some(value) => self.expression(value, Some(&Type::Int))?, + None => Expression { + kind: Node::Literal(ast::Literal::Number("20".into())), + value_type: Type::Int, + span: source.span, + }, + }); + } + Ok(Filter { + condition: filter, + postgres_condition: postgres_filter, + order, + order_fields, + values, + fields: bound_fields, + relations, + }) + } +} diff --git a/rust/src/compiler/check/database/mod.rs b/rust/src/compiler/check/database/mod.rs new file mode 100644 index 0000000..1d46611 --- /dev/null +++ b/rust/src/compiler/check/database/mod.rs @@ -0,0 +1,359 @@ +//! Model 定义提供静态字段与名义 ID,App 的匿名块直接检查成绑定参数。 + +mod condition; +mod constant; +mod filter; +mod native; +mod options; +mod privacy; +pub(super) use privacy::Privacy; +mod query; +pub(super) mod relation; +mod schema; +mod write; +use super::package::Types; +use crate::{ + compiler::model::{persistence::Model, *}, + syntax::{ast, diagnostic::Diagnostic}, +}; + +pub(super) fn prepare( + declarations: &mut Vec, + names: &mut Types, +) -> Result<(), Diagnostic> { + let base = declarations.len(); + let mut pages = Vec::new(); + for (index, declaration) in declarations.iter_mut().enumerate() { + let identity = &names.packages[declaration.span.source]; + let Some(expected) = package::model_record(identity) else { + continue; + }; + if let ast::TypeBody::Choice(variants) = &declaration.body { + if variants.is_empty() + || variants + .iter() + .any(|variant| variant.error || !variant.fields.is_empty()) + { + return Err(Diagnostic::new( + "C014", + "Model choices require non-error variants without payloads", + declaration.span, + )); + } + continue; + } + if declaration.name != expected { + continue; + } + let ast::TypeBody::Record(fields) = &mut declaration.body else { + return Err(Diagnostic::new( + "U001", + "Model choices are not implemented yet", + declaration.span, + )); + }; + if fields + .iter() + .any(|field| matches!(field.name.as_str(), "id" | "created_at")) + { + return Err(Diagnostic::new( + "C014", + "id and created_at are generated Model fields", + declaration.span, + )); + } + if !names.insert(declaration.span.source, "id".into(), index, true) { + return Err(Diagnostic::new( + "C002", + "duplicate Model source", + declaration.span, + )); + } + for (position, (name, kind)) in [("id", "id"), ("created_at", "DateTime")] + .into_iter() + .enumerate() + { + fields.insert( + position, + ast::Field { + name: name.into(), + kind: ast::TypeRef { + name: kind.into(), + arguments: vec![], + storage: vec![], + nullable: false, + span: declaration.span, + }, + bounds: vec![], + private: false, + unique: false, + index: false, + rest: Default::default(), + persistence: Default::default(), + span: declaration.span, + }, + ); + } + names.insert( + declaration.span.source, + "Page".into(), + base + pages.len(), + true, + ); + let kind = |name: &str| ast::TypeRef { + name: name.into(), + arguments: vec![], + storage: vec![], + nullable: false, + span: declaration.span, + }; + let mut fields = [ + ("items", kind("List")), + ("page", kind("Int")), + ("size", kind("Int")), + ("total", kind("Int")), + ("pages", kind("Int")), + ] + .into_iter() + .map(|(name, kind)| ast::Field { + name: name.into(), + kind, + bounds: vec![], + private: false, + unique: false, + index: false, + rest: Default::default(), + persistence: Default::default(), + span: declaration.span, + }) + .collect::>(); + fields[0].kind.arguments.push(kind(&declaration.name)); + pages.push(ast::TypeDeclaration { + name: "Page".into(), + global: false, + setting: false, + public: true, + body: ast::TypeBody::Record(fields), + span: declaration.span, + }); + names.insert( + declaration.span.source, + "Cursor".into(), + base + pages.len(), + true, + ); + let mut item = kind("List"); + item.arguments.push(kind(&declaration.name)); + let mut next = kind(&declaration.name); + next.nullable = true; + let fields = [("items", item), ("next", next), ("has_more", kind("Bool"))] + .into_iter() + .map(|(name, kind)| ast::Field { + name: name.into(), + kind, + bounds: vec![], + private: false, + unique: false, + index: false, + rest: Default::default(), + persistence: Default::default(), + span: declaration.span, + }) + .collect(); + pages.push(ast::TypeDeclaration { + name: "Cursor".into(), + global: false, + setting: false, + public: true, + body: ast::TypeBody::Record(fields), + span: declaration.span, + }); + } + declarations.extend(pages); + relation::prepare(declarations, names); + Ok(()) +} + +pub(super) fn check( + definitions: &[Definition], + declarations: &[ast::TypeDeclaration], + names: &Types, + bindings: &[Option], + persistence: &[ast::Persistence], +) -> Result, Diagnostic> { + let mut models = Vec::new(); + for (index, definition) in definitions.iter().enumerate() { + let identity = &names.packages[definition.source]; + let Some((_, "model")) = package::role(identity) else { + continue; + }; + if names.resolve("id", definition.source, declarations[index].span)? != Some(index) { + continue; + } + let Shape::Record(fields) = &definition.shape else { + continue; + }; + let ast::TypeBody::Record(source_fields) = &declarations[index].body else { + unreachable!() + }; + let mut columns = Vec::new(); + for (field, (_, kind)) in source_fields.iter().zip(fields) { + let decimal = if field.kind.name == "Decimal" { + match field.kind.storage.as_slice() { + [precision, scale] if (1..=34).contains(precision) && scale <= precision => { + Some((*precision, *scale)) + } + _ => { + return Err(Diagnostic::new( + "C014", + "Decimal storage requires precision 1..34 and scale 0..precision", + field.kind.span, + )); + } + } + } else { + None + }; + let limits = match field.kind.storage.as_slice() { + _ if decimal.is_some() => None, + [] => None, + [max] if *max > 0 => Some((0, *max)), + [min, max] if min <= max && *max > 0 => Some((*min, *max)), + _ => { + return Err(Diagnostic::new( + "C014", + "invalid storage length bounds", + field.kind.span, + )); + } + }; + let base = if let Type::Optional(inner) = kind { + inner.as_ref() + } else { + kind + }; + let choice = if let Type::Named(index) = base { + match &declarations[*index].body { + ast::TypeBody::Choice(variants) + if variants + .iter() + .all(|variant| !variant.error && variant.fields.is_empty()) => + { + Some( + variants + .iter() + .map(|variant| { + ( + variant.name.clone(), + variant + .label + .clone() + .unwrap_or_else(|| variant.name.clone()), + ) + }) + .collect(), + ) + } + _ => None, + } + } else { + None + }; + if field.persistence.generated + && (*kind != Type::Uuid || field.persistence.default.is_some()) + { + return Err(Diagnostic::new( + "C014", + "generated requires non-null Uuid without a default", + field.span, + )); + } + if field.persistence.text.is_some() && *base != Type::Text { + return Err(Diagnostic::new( + "C014", + "raw, lower and upper require a Text Model field", + field.span, + )); + } + let mut column = persistence::Column { + text: field.persistence.text.unwrap_or_default(), + limits, + decimal, + generated: field.persistence.generated, + default: None, + previous: field.persistence.previous.clone(), + choice, + unique: field.unique, + index: field.index, + }; + if let Some(value) = &field.persistence.default { + column.default = Some(constant::check(value, kind, &column, names)?); + } + columns.push(column); + } + let supported = |kind: &Type| { + matches!( + kind, + Type::Bool + | Type::Int + | Type::Float + | Type::Text + | Type::Bytes + | Type::ModelId(_) + | Type::DateTime + | Type::Decimal + | Type::Uuid + | Type::Date + | Type::Time + | Type::Duration + | Type::Json + ) + }; + if fields.iter().zip(&columns).any(|((_, kind), column)| { + column.choice.is_none() + && !supported(if let Type::Optional(inner) = kind { + inner + } else { + kind + }) + }) || definition.ranges.iter().any(Option::is_some) + { + return Err(Diagnostic::new( + "U001", + "this persistent field type or constraint is not implemented yet", + crate::syntax::source::Span { + source: definition.source, + start: 0, + end: 0, + }, + )); + } + let span = crate::syntax::source::Span { + source: definition.source, + start: 0, + end: 0, + }; + let page = names.resolve("Page", definition.source, span)?.unwrap(); + let cursor = names.resolve("Cursor", definition.source, span)?.unwrap(); + models.push(Model { + global: declarations[index].global, + definition: index, + page, + cursor, + source: definition.source, + table: identity.replace('.', "_"), + component: identity.split('.').next().unwrap().into(), + fields: fields.clone(), + columns, + indexes: vec![], + seeds: vec![], + migrations: vec![], + sql: vec![], + relations: vec![], + database: bindings[definition.source].clone(), + }); + } + schema::check(&mut models, declarations, persistence, names)?; + native::check(&mut models, definitions, declarations, persistence, names)?; + Ok(models) +} diff --git a/rust/src/compiler/check/database/native/mod.rs b/rust/src/compiler/check/database/native/mod.rs new file mode 100644 index 0000000..505b3b1 --- /dev/null +++ b/rust/src/compiler/check/database/native/mod.rs @@ -0,0 +1,311 @@ +//! SQL 合同在编译期确定返回记录、标量编码和只读证明;执行沿用 Model 的连接。 + +mod sql; +use super::*; +use crate::compiler::{ + check::names, + model::persistence::{Column, NativeSql, Operation, Query, Write}, +}; + +pub(super) fn check( + models: &mut [Model], + definitions: &[Definition], + sources: &[ast::TypeDeclaration], + declarations: &[ast::Persistence], + names: &Types, +) -> Result<(), Diagnostic> { + let mut used = std::collections::HashSet::new(); + let tables = models + .iter() + .map(|model| model.table.clone()) + .collect::>(); + for model in models.iter_mut() { + for declaration in declarations + .iter() + .filter(|declaration| declaration.span.source == model.source) + { + let ast::PersistenceKind::Sql(value) = &declaration.kind else { + continue; + }; + let span = declaration.span; + let invalid = |message| Diagnostic::new("C014", message, span); + names::function(&value.name, span)?; + if Operation::named(&value.name).is_some() + || model.sql.iter().any(|sql| sql.name == value.name) + { + return Err(invalid("duplicate or reserved Model SQL operation")); + } + let [output] = value.outputs.as_slice() else { + return Err(invalid("SQL requires one record output")); + }; + field_contract(output)?; + let output = names::resolve(&output.kind, names)?; + let row = match &output { + Type::Optional(inner) | Type::List(inner) => &**inner, + other => other, + }; + let Type::Named(record) = row else { + return Err(invalid( + "SQL output requires a concrete record, optional record or List", + )); + }; + let result = &definitions[*record]; + let Shape::Record(fields) = &result.shape else { + return Err(invalid("SQL output requires a record")); + }; + let same_model = *record == model.definition; + let own_record = result.source == model.source; + let app_view = package::role(&names.packages[result.source]) + == package::role(&names.packages[model.source]).map(|(domain, _)| (domain, "app")); + if !same_model && !own_record && !app_view { + return Err(invalid( + "SQL result must belong to its Model or the same domain App", + )); + } + if !same_model + && (result.ranges.iter().any(Option::is_some) || !result.private_fields.is_empty()) + { + return Err(invalid( + "SQL result records require public unconstrained scalar fields", + )); + } + used.insert(*record); + let mut inputs = Vec::new(); + let mut input_columns = Vec::new(); + let mut seen = std::collections::HashSet::new(); + for input in &value.inputs { + field_contract(input)?; + names::identifier(&input.name, false, input.span)?; + if !seen.insert(&input.name) { + return Err(invalid("duplicate SQL parameter")); + } + let kind = names::resolve(&input.kind, names)?; + input_columns.push(column(&kind, &input.kind, sources)?); + inputs.push(kind); + } + let ast::TypeBody::Record(source_fields) = &sources[*record].body else { + unreachable!() + }; + if !same_model { + for field in source_fields { + field_contract(field)?; + } + } + let columns = if same_model { + model.columns.clone() + } else { + fields + .iter() + .zip(source_fields) + .map(|((_, kind), source)| column(kind, &source.kind, sources)) + .collect::, _>>()? + }; + let (sqlite, mut dependencies) = + sql::check(&value.sqlite, false, inputs.len(), model, span, &tables)?; + let (postgres, pg_dependencies) = + sql::check(&value.postgres, true, inputs.len(), model, span, &tables)?; + dependencies.extend(pg_dependencies); + dependencies.sort_unstable(); + dependencies.dedup(); + let record = *record; + model.sql.push(NativeSql { + dependencies, + name: value.name.clone(), + inputs, + input_columns, + output, + record, + columns, + sqlite: value.sqlite.clone(), + postgres: value.postgres.clone(), + readonly: sqlite && postgres, + }); + } + } + for (index, declaration) in sources.iter().enumerate() { + if matches!( + package::role(&names.packages[declaration.span.source]), + Some((_, "model")) + ) && matches!(declaration.body, ast::TypeBody::Record(_)) + && !matches!(declaration.name.as_str(), "Page" | "Cursor") + && !models.iter().any(|model| model.definition == index) + && !used.contains(&index) + { + return Err(Diagnostic::new( + "C014", + "additional Model records must be SQL result records", + declaration.span, + )); + } + } + Ok(()) +} + +fn field_contract(field: &ast::Field) -> Result<(), Diagnostic> { + if field.private + || field.unique + || field.index + || !field.bounds.is_empty() + || !field.rest.is_empty() + || !field.persistence.is_empty() + { + Err(Diagnostic::new( + "C014", + "SQL parameters and outputs cannot have field constraints or storage modifiers", + field.span, + )) + } else { + Ok(()) + } +} + +fn column( + kind: &Type, + source: &ast::TypeRef, + declarations: &[ast::TypeDeclaration], +) -> Result { + let kind = if let Type::Optional(inner) = kind { + &**inner + } else { + kind + }; + let choice = if let Type::Named(index) = kind { + match &declarations[*index].body { + ast::TypeBody::Choice(variants) + if variants + .iter() + .all(|value| !value.error && value.fields.is_empty()) => + { + Some( + variants + .iter() + .map(|value| { + ( + value.name.clone(), + value.label.clone().unwrap_or_else(|| value.name.clone()), + ) + }) + .collect(), + ) + } + _ => None, + } + } else { + None + }; + if choice.is_none() + && !matches!( + kind, + Type::Bool + | Type::Int + | Type::Float + | Type::Decimal + | Type::Text + | Type::Bytes + | Type::Uuid + | Type::DateTime + | Type::Date + | Type::Time + | Type::Duration + | Type::Json + | Type::ModelId(_) + ) + { + return Err(Diagnostic::new( + "C014", + "SQL requires supported storage scalars", + source.span, + )); + } + let decimal = if *kind == Type::Decimal { + match source.storage.as_slice() { + [] => None, + [precision, scale] if (1..=34).contains(precision) && scale <= precision => { + Some((*precision, *scale)) + } + _ => { + return Err(Diagnostic::new( + "C014", + "invalid SQL Decimal precision or scale", + source.span, + )); + } + } + } else if !source.storage.is_empty() { + return Err(Diagnostic::new( + "C014", + "SQL scalar parameters and result fields cannot have storage bounds", + source.span, + )); + } else { + None + }; + Ok(Column { + text: persistence::TextMode::Raw, + limits: None, + decimal, + generated: false, + default: None, + previous: None, + choice, + unique: false, + index: false, + }) +} + +impl crate::compiler::check::Context<'_> { + pub(in crate::compiler::check::database) fn native_sql( + &mut self, + model: usize, + name: &str, + arguments: &[ast::Expression], + expected: Option<&Type>, + source: &ast::Expression, + ) -> Result { + let index = self.models[model] + .sql + .iter() + .position(|sql| sql.name == name) + .ok_or_else(|| self.error(source, "unknown Model operation"))?; + let sql = &self.models[model].sql[index]; + let inputs = sql.inputs.clone(); + let output = sql.output.clone(); + let readonly = sql.readonly; + let dependencies = sql.dependencies.clone(); + if inputs.len() != arguments.len() { + return Err(self.error(source, "SQL argument count differs from its declaration")); + } + let values = arguments + .iter() + .zip(&inputs) + .map(|(argument, kind)| self.expression(argument, Some(kind))) + .collect::, _>>()?; + let error = self + .definition_names + .resolve("dever.database.Error", self.source, source.span)? + .ok_or_else(|| self.error(source, "database standard error is missing"))?; + self.coerce( + Expression { + kind: Node::Database( + Query { + dependencies, + model, + operation: Operation::Sql { index, readonly }, + fields: vec![], + write: Write::None, + error, + filter: String::new(), + postgres_filter: String::new(), + order: String::new(), + order_fields: vec![], + relations: vec![], + }, + values, + ), + value_type: output, + span: source.span, + }, + expected, + ) + } +} diff --git a/rust/src/compiler/check/database/native/sql.rs b/rust/src/compiler/check/database/native/sql.rs new file mode 100644 index 0000000..b0c3991 --- /dev/null +++ b/rust/src/compiler/check/database/native/sql.rs @@ -0,0 +1,343 @@ +//! 仅证明有限的自有表投影只读;无法证明的 SQL 保留写效果,不凭 SELECT 前缀放行 GET。 + +use crate::{ + compiler::model::persistence::Model, + syntax::{diagnostic::Diagnostic, source::Span}, +}; +use std::collections::BTreeSet; + +struct Token { + text: String, + quoted: bool, +} + +impl Token { + fn plain(text: impl Into) -> Self { + Self { + text: text.into(), + quoted: false, + } + } + fn keyword(&self) -> &str { + if self.quoted { "" } else { &self.text } + } +} + +pub(super) fn check( + sql: &str, + postgres: bool, + parameters: usize, + model: &Model, + span: Span, + tables: &[String], +) -> Result<(bool, Vec), Diagnostic> { + let invalid = || { + Diagnostic::new( + "C014", + "SQL requires one parameterized SELECT/WITH statement without internal tables", + span, + ) + }; + let bytes = sql.as_bytes(); + let mut cursor = 0; + let mut tokens = Vec::new(); + let mut bindings = BTreeSet::new(); + let mut ended = false; + while cursor < bytes.len() { + let byte = bytes[cursor]; + if byte.is_ascii_whitespace() { + cursor += 1; + continue; + } + if bytes[cursor..].starts_with(b"--") { + cursor += 2; + while cursor < bytes.len() && bytes[cursor] != b'\n' { + cursor += 1 + } + continue; + } + if bytes[cursor..].starts_with(b"/*") { + cursor += 2; + let mut depth = 1; + while cursor < bytes.len() && depth > 0 { + if postgres && bytes[cursor..].starts_with(b"/*") { + depth += 1; + cursor += 2 + } else if bytes[cursor..].starts_with(b"*/") { + depth -= 1; + cursor += 2 + } else { + cursor += 1 + } + } + if depth != 0 { + return Err(invalid()); + } + continue; + } + if ended { + return Err(invalid()); + } + if byte == b';' { + ended = true; + cursor += 1; + continue; + } + if matches!(byte, b'\'' | b'"' | b'`' | b'[') { + let close = if byte == b'[' { b']' } else { byte }; + cursor += 1; + let start = cursor; + while cursor < bytes.len() { + if bytes[cursor] == close { + if byte != b'[' && bytes.get(cursor + 1) == Some(&close) { + cursor += 2; + continue; + } + break; + } + cursor += 1; + } + if cursor == bytes.len() { + return Err(invalid()); + } + let word = &sql[start..cursor]; + if byte != b'\'' && reserved(word) { + return Err(invalid()); + } + tokens.push(Token { + text: if byte == b'\'' { + "".into() + } else { + word.to_ascii_lowercase() + }, + quoted: byte != b'\'', + }); + cursor += 1; + continue; + } + if matches!(byte, b'?' | b'$') { + if byte != if postgres { b'$' } else { b'?' } { + return Err(invalid()); + } + cursor += 1; + let start = cursor; + while cursor < bytes.len() && bytes[cursor].is_ascii_digit() { + cursor += 1 + } + let index = sql[start..cursor].parse::().map_err(|_| invalid())?; + if index == 0 || index > parameters { + return Err(invalid()); + } + bindings.insert(index); + tokens.push(Token::plain("")); + continue; + } + if byte.is_ascii_alphabetic() || byte == b'_' { + let start = cursor; + cursor += 1; + while cursor < bytes.len() + && (bytes[cursor].is_ascii_alphanumeric() || bytes[cursor] == b'_') + { + cursor += 1 + } + let word = sql[start..cursor].to_ascii_lowercase(); + if reserved(&word) || (word == "u" && bytes.get(cursor) == Some(&b'&')) { + return Err(invalid()); + } + tokens.push(Token::plain(word)); + continue; + } + if byte == b'@' + || (byte == b':' + && bytes.get(cursor + 1).is_some_and(u8::is_ascii_alphabetic) + && (cursor == 0 || bytes[cursor - 1] != b':')) + { + return Err(invalid()); + } + tokens.push(Token::plain(char::from(byte).to_string())); + cursor += 1; + } + if !matches!(tokens.first().map(Token::keyword), Some("select" | "with")) + || bindings.len() != parameters + { + return Err(invalid()); + } + let ctes = tokens + .windows(3) + .filter(|words| words[1].keyword() == "as" && words[2].keyword() == "(") + .map(|words| words[0].text.as_str()) + .collect::>(); + table_references(&tokens, &ctes, tables, span)?; + let dependencies = tables + .iter() + .enumerate() + .filter_map(|(index, name)| { + tokens + .iter() + .any(|token| token.text == *name) + .then_some(index) + }) + .collect(); + Ok((readonly(&tokens, model), dependencies)) +} + +// 每层 SELECT 分别跟踪 FROM;逗号 JOIN、嵌套查询也必须提供完整静态依赖。 +fn table_references( + tokens: &[Token], + ctes: &BTreeSet<&str>, + tables: &[String], + span: Span, +) -> Result<(), Diagnostic> { + let invalid = || { + Diagnostic::new( + "C014", + "SQL table must reference a declared Model or local CTE without schema qualification", + span, + ) + }; + let mut sources = vec![false]; + for (position, token) in tokens.iter().enumerate() { + let source = sources.last_mut().unwrap(); + let table = matches!(token.keyword(), "from" | "join" | "into" | "update") + || (*source && token.keyword() == ","); + match token.keyword() { + "from" | "join" => *source = true, + "where" | "group" | "having" | "order" | "limit" | "offset" | "union" | "except" + | "intersect" | "returning" => *source = false, + "(" => sources.push(false), + ")" => { + if sources.len() == 1 { + return Err(invalid()); + } + sources.pop(); + } + _ => (), + } + if table { + let name = tokens.get(position + 1).ok_or_else(invalid)?; + if name.keyword() == "(" { + continue; + } + if (!ctes.contains(name.text.as_str()) && !tables.contains(&name.text)) + || tokens + .get(position + 2) + .is_some_and(|token| matches!(token.keyword(), "." | "(")) + { + return Err(invalid()); + } + } + } + if sources.len() != 1 { + return Err(invalid()); + } + Ok(()) +} + +fn reserved(name: &str) -> bool { + let name = name.to_ascii_lowercase(); + name.starts_with("_dever_") + || name.starts_with("sqlite_") + || matches!( + name.as_str(), + "pg_catalog" | "pg_toast" | "information_schema" + ) +} + +fn readonly(tokens: &[Token], model: &Model) -> bool { + let mut cursor = 0; + let eat = |cursor: &mut usize, expected: &str| { + if tokens + .get(*cursor) + .is_some_and(|value| value.keyword() == expected) + { + *cursor += 1; + true + } else { + false + } + }; + let field = |cursor: &mut usize| { + if tokens + .get(*cursor) + .is_some_and(|value| model.fields.iter().any(|(name, _)| *name == value.text)) + { + *cursor += 1; + true + } else { + false + } + }; + if !eat(&mut cursor, "select") { + return false; + } + loop { + // 字段别名、COUNT(*) 和参数化分页不增加 SQL 副作用;不放行任意函数。 + let projection = field(&mut cursor) + || (eat(&mut cursor, "count") + && eat(&mut cursor, "(") + && eat(&mut cursor, "*") + && eat(&mut cursor, ")")); + if !projection { + return false; + } + if eat(&mut cursor, "as") { + let Some(alias) = tokens.get(cursor) else { + return false; + }; + if !alias.quoted + && !alias.text.bytes().enumerate().all(|(index, byte)| { + byte == b'_' + || byte.is_ascii_alphabetic() + || (index > 0 && byte.is_ascii_digit()) + }) + { + return false; + } + cursor += 1; + } + if !eat(&mut cursor, ",") { + break; + } + } + if !eat(&mut cursor, "from") + || tokens + .get(cursor) + .is_none_or(|token| token.text != model.table) + { + return false; + } + cursor += 1; + if eat(&mut cursor, "where") { + loop { + if !field(&mut cursor) || !eat(&mut cursor, "=") || !eat(&mut cursor, "") { + return false; + } + if !eat(&mut cursor, "and") { + break; + } + } + } + if eat(&mut cursor, "order") { + if !eat(&mut cursor, "by") { + return false; + } + loop { + if !field(&mut cursor) { + return false; + } + if !eat(&mut cursor, "asc") { + eat(&mut cursor, "desc"); + } + if !eat(&mut cursor, ",") { + break; + } + } + } + if eat(&mut cursor, "limit") && !eat(&mut cursor, "") { + return false; + } + if eat(&mut cursor, "offset") && !eat(&mut cursor, "") { + return false; + } + cursor == tokens.len() +} diff --git a/rust/src/compiler/check/database/options.rs b/rust/src/compiler/check/database/options.rs new file mode 100644 index 0000000..d64eac1 --- /dev/null +++ b/rust/src/compiler/check/database/options.rs @@ -0,0 +1,69 @@ +//! Model 选项在编译期展开为有序 Map,不查询数据库或动态检查变体。 + +use crate::{ + compiler::{check::Context, model::*}, + syntax::{ast, diagnostic::Diagnostic}, +}; + +impl Context<'_> { + pub(in crate::compiler::check) fn model_options( + &self, + source: &ast::Expression, + ) -> Result, Diagnostic> { + let Some(path) = source.path() else { + return Ok(None); + }; + let Some(name) = path.strip_suffix(".options") else { + return Ok(None); + }; + let Some(index) = self + .definition_names + .resolve(name, self.source, source.span)? + else { + return Ok(None); + }; + let definition = &self.definitions[index]; + if !matches!( + package::role(&self.definition_names.packages[definition.source]), + Some((_, "model")) + ) { + return Ok(None); + } + let Shape::Choice(variants) = &definition.shape else { + return Ok(None); + }; + if definition.related { + return Ok(None); + } + let text = |value: String| Expression { + kind: Node::Literal(ast::Literal::Text(value)), + value_type: Type::Text, + span: source.span, + }; + Ok(Some(Expression { + kind: Node::Map( + variants + .iter() + .enumerate() + .map(|(position, variant)| { + ( + Expression { + kind: Node::Variant(index, position, vec![]), + value_type: Type::Named(index), + span: source.span, + }, + text( + variant + .label + .clone() + .unwrap_or_else(|| variant.name.clone()), + ), + ) + }) + .collect(), + ), + value_type: Type::Map(Box::new(Type::Named(index)), Box::new(Type::Text)), + span: source.span, + })) + } +} diff --git a/rust/src/compiler/check/database/privacy.rs b/rust/src/compiler/check/database/privacy.rs new file mode 100644 index 0000000..1abf401 --- /dev/null +++ b/rust/src/compiler/check/database/privacy.rs @@ -0,0 +1,107 @@ +//! 只把私有 Model 数据标为敏感;普通 record 的 private 字段仍是合法封装。 +//! 反向类型边传播一次,Related 环不会导致递归或漏检,运行程序不增加检查。 + +use crate::{ + compiler::model::{Definition, Shape, Signature, Type, persistence::Model}, + syntax::{diagnostic::Diagnostic, source::Span}, +}; + +pub(in crate::compiler::check) struct Privacy(Vec); + +impl Privacy { + pub fn new(definitions: &[Definition], models: &[Model]) -> Self { + let mut parents = vec![Vec::new(); definitions.len()]; + for (index, definition) in definitions.iter().enumerate() { + let mut field = |kind: &Type| visit(kind, &mut |child| parents[child].push(index)); + match &definition.shape { + Shape::Record(fields) => { + for (_, kind) in fields { + field(kind); + } + } + Shape::Choice(variants) => { + for variant in variants { + for (_, kind) in &variant.fields { + field(kind); + } + } + } + } + } + let mut private = vec![false; definitions.len()]; + let mut pending = Vec::new(); + for model in models { + if !definitions[model.definition].private_fields.is_empty() { + private[model.definition] = true; + pending.push(model.definition); + } + } + while let Some(index) = pending.pop() { + for &parent in &parents[index] { + if !private[parent] { + private[parent] = true; + pending.push(parent); + } + } + } + Self(private) + } + + pub fn signature( + &self, + signature: &Signature, + definitions: &[Definition], + span: Span, + ) -> Result<(), Diagnostic> { + for (_, kind) in signature.inputs.iter().chain(&signature.outputs) { + self.check(kind, span)?; + } + if signature + .outputs + .iter() + .any(|(_, kind)| kind.contains_secret(definitions)) + { + return Err(Diagnostic::new( + "C006", + "App outputs cannot contain Secret", + span, + )); + } + Ok(()) + } + + pub fn check(&self, kind: &Type, span: Span) -> Result<(), Diagnostic> { + let mut private = false; + visit(kind, &mut |index| private |= self.0[index]); + if private { + Err(Diagnostic::new( + "C006", + "private Model snapshots cannot cross App contracts; expose a View", + span, + )) + } else { + Ok(()) + } + } +} + +fn visit(kind: &Type, named: &mut impl FnMut(usize)) { + match kind { + Type::Named(index) => named(*index), + Type::List(inner) + | Type::Optional(inner) + | Type::Stream(inner) + | Type::AsyncStream(inner) + | Type::Channel(inner) => visit(inner, named), + Type::Map(key, value) | Type::MapEntry(key, value) => { + visit(key, named); + visit(value, named); + } + Type::Outputs(fields) => { + for (_, kind) in fields { + visit(kind, named); + } + } + _ => {} + } +} diff --git a/rust/src/compiler/check/database/query.rs b/rust/src/compiler/check/database/query.rs new file mode 100644 index 0000000..61708a9 --- /dev/null +++ b/rust/src/compiler/check/database/query.rs @@ -0,0 +1,192 @@ +use super::*; +use crate::compiler::model::persistence::{Operation, Query, Write}; + +impl super::super::Context<'_> { + pub(in crate::compiler::check) fn database( + &mut self, + name: &str, + arguments: &[ast::Expression], + expected: Option<&Type>, + source: &ast::Expression, + ) -> Result, Diagnostic> { + let Some((prefix, operation)) = name.rsplit_once('.') else { + return Ok(None); + }; + if prefix != "model" && !prefix.starts_with("model.") { + return Ok(None); + } + if !matches!( + package::role(&self.definition_names.packages[self.source]), + Some((_, "app")) + ) { + return Err(Diagnostic::new( + "C006", + "Model operations belong to their owning App", + source.span, + )); + } + let definition = self + .definition_names + .resolve(&format!("{prefix}.id"), self.source, source.span)? + .ok_or_else(|| self.error(source, "unknown Model"))?; + let model = self + .models + .iter() + .position(|model| model.definition == definition) + .unwrap(); + let operation = match Operation::named(operation) { + Some(operation) => operation, + None => { + return self + .native_sql(model, operation, arguments, expected, source) + .map(Some); + } + }; + let mut values = Vec::new(); + let mut selected = Vec::new(); + let mut filter = String::new(); + let mut postgres_filter = String::new(); + let mut order = String::from("\"id\" DESC"); + let mut order_fields = vec![(0, false)]; + let mut write = Write::None; + let mut relations = Vec::new(); + let conditional = matches!(operation, Operation::Update | Operation::Delete) + && arguments + .first() + .is_some_and(|value| matches!(value.kind, ast::ExpressionKind::Map(_))); + let output = if conditional { + let expected = if operation == Operation::Update { 2 } else { 1 }; + if arguments.len() != expected { + return Err(self.error( + source, + "conditional write requires a query and update fields when updating", + )); + } + let checked = self.model_filter(&arguments[..1], model, operation, source)?; + if checked.condition.is_empty() { + return Err(self.error(source, "conditional write requires where")); + } + filter = checked.condition; + postgres_filter = checked.postgres_condition; + values = checked.values; + selected = checked.fields; + write = Write::Conditional { + filter_count: values.len(), + }; + if operation == Operation::Update { + let mut updated = Vec::new(); + self.model_input(&arguments[1], model, &mut updated, &mut values)?; + if updated.is_empty() { + return Err(self.error(&arguments[1], "update requires at least one field")); + } + selected.extend(updated); + } + Type::Int + } else { + match operation { + Operation::Sql { .. } => unreachable!("SQL is checked separately"), + Operation::Create | Operation::Update => { + let block = if operation == Operation::Update { + let [id, block] = arguments else { + return Err(self.error(source, "update requires id and field block")); + }; + values.push(self.expression(id, Some(&Type::ModelId(definition)))?); + block + } else { + let [block] = arguments else { + return Err(self.error(source, "create requires one field block")); + }; + block + }; + self.model_input(block, model, &mut selected, &mut values)?; + if operation == Operation::Create { + self.complete_model_input(block, model, &mut selected, &mut values)?; + } else if selected.is_empty() { + return Err(self.error(block, "update requires at least one field")); + } + Type::Named(definition) + } + Operation::CreateMany | Operation::Upsert => { + write = self.model_write( + arguments, + model, + operation, + source, + &mut selected, + &mut values, + )?; + if operation == Operation::CreateMany { + Type::Int + } else { + Type::Named(definition) + } + } + Operation::Get | Operation::Delete => { + let [id] = arguments else { + return Err(self.error(source, "Model operation requires one nominal id")); + }; + values.push(self.expression(id, Some(&Type::ModelId(definition)))?); + if operation == Operation::Get { + Type::Named(definition) + } else { + Type::Int + } + } + Operation::Count + | Operation::Exists + | Operation::First + | Operation::List + | Operation::Stream + | Operation::Cursor => { + let checked = self.model_filter(arguments, model, operation, source)?; + filter = checked.condition; + postgres_filter = checked.postgres_condition; + order = checked.order; + order_fields = checked.order_fields; + selected = checked.fields; + values = checked.values; + relations = checked.relations; + match operation { + Operation::Count => Type::Int, + Operation::Exists => Type::Bool, + Operation::First => Type::Optional(Box::new(Type::Named(definition))), + Operation::Cursor => Type::Named(self.models[model].cursor), + Operation::Stream => Type::AsyncStream(Box::new(Type::Named(definition))), + _ => Type::Named(self.models[model].page), + } + } + } + }; + let error = self + .definition_names + .resolve("dever.database.Error", self.source, source.span)? + .ok_or_else(|| self.error(source, "database standard error is missing"))?; + self.coerce( + Expression { + kind: Node::Database( + Query { + dependencies: relations + .iter() + .map(|index| self.models[model].relations[*index].model) + .collect(), + model, + operation, + fields: selected, + write, + error, + filter, + postgres_filter, + order, + order_fields, + relations, + }, + values, + ), + value_type: output, + span: source.span, + }, + expected, + ) + .map(Some) + } +} diff --git a/rust/src/compiler/check/database/relation/mod.rs b/rust/src/compiler/check/database/relation/mod.rs new file mode 100644 index 0000000..c5d59ff --- /dev/null +++ b/rust/src/compiler/check/database/relation/mod.rs @@ -0,0 +1,228 @@ +//! Related 是编译器专用的两态具体 choice;只装箱载荷以允许自关联,不改变 Model 标量存储。 + +use super::*; +use crate::compiler::model::persistence::Relation; + +pub(super) fn prepare(declarations: &mut Vec, names: &mut Types) { + let models = declarations + .iter() + .enumerate() + .filter(|(_, declaration)| { + matches!(declaration.body, ast::TypeBody::Record(_)) + && package::model_record(&names.packages[declaration.span.source]).as_deref() + == Some(declaration.name.as_str()) + }) + .map(|(index, declaration)| (index, declaration.clone())) + .collect::>(); + for (model, declaration) in models { + for container in ["", "?", "List"] { + let mut value = ast::TypeRef { + name: declaration.name.clone(), + arguments: vec![], + storage: vec![], + nullable: container == "?", + span: declaration.span, + }; + let mut kind = Type::Named(model); + if container == "?" { + kind = Type::Optional(Box::new(kind)); + } + if container == "List" { + kind = Type::List(Box::new(kind)); + value = ast::TypeRef { + name: "List".into(), + arguments: vec![value], + storage: vec![], + nullable: false, + span: declaration.span, + }; + } + let index = declarations.len(); + names.related.insert(kind, index); + let name = format!("Related{index}"); + names.insert( + declaration.span.source, + format!("Related<{index}>"), + index, + true, + ); + declarations.push(ast::TypeDeclaration { + name, + global: false, + setting: false, + public: true, + span: declaration.span, + body: ast::TypeBody::Choice(vec![ + ast::Variant { + name: "Unloaded".into(), + label: None, + fields: vec![], + error: false, + span: declaration.span, + }, + ast::Variant { + name: "Loaded".into(), + label: None, + fields: vec![ast::Field { + name: "value".into(), + kind: value, + bounds: vec![], + private: false, + unique: false, + index: false, + rest: Default::default(), + persistence: Default::default(), + span: declaration.span, + }], + error: false, + span: declaration.span, + }, + ]), + }); + } + } +} + +pub(in crate::compiler::check) fn check( + models: &mut [Model], + definitions: &mut [Definition], + declarations: &[ast::Persistence], + names: &Types, +) -> Result<(), Diagnostic> { + for index in 0..models.len() { + let mut relations = Vec::new(); + let span = crate::syntax::source::Span { + source: models[index].source, + start: 0, + end: 0, + }; + for (field, (name, kind)) in models[index].fields.iter().enumerate().skip(2) { + let (kind, optional) = match kind { + Type::Optional(inner) => (&**inner, true), + kind => (kind, false), + }; + let Type::ModelId(target) = kind else { + continue; + }; + let Some(name) = name.strip_suffix("_id") else { + continue; + }; + let target = models + .iter() + .position(|model| model.definition == *target) + .ok_or_else(|| { + Diagnostic::new("C014", "relation target must be a persistent Model", span) + })?; + let payload = if optional { + Type::Optional(Box::new(Type::Named(models[target].definition))) + } else { + Type::Named(models[target].definition) + }; + relations.push(Relation { + name: name.into(), + model: target, + field, + many: false, + optional, + definition: names.related[&payload], + }); + } + for declaration in declarations + .iter() + .filter(|declaration| declaration.span.source == models[index].source) + { + let ast::PersistenceKind::Relation { name, target } = &declaration.kind else { + continue; + }; + super::super::names::identifier(name, false, declaration.span)?; + let (package, field) = target.rsplit_once('.').ok_or_else(|| { + Diagnostic::new( + "C014", + "relation requires a Model foreign-key field", + declaration.span, + ) + })?; + let definition = names + .resolve( + &format!("{package}.id"), + models[index].source, + declaration.span, + )? + .ok_or_else(|| { + Diagnostic::new("C014", "unknown relation Model", declaration.span) + })?; + let target = models + .iter() + .position(|model| model.definition == definition) + .ok_or_else(|| { + Diagnostic::new( + "C014", + "relation target must be a persistent Model", + declaration.span, + ) + })?; + let field = models[target] + .fields + .iter() + .position(|(name, _)| name == field) + .ok_or_else(|| { + Diagnostic::new("C014", "unknown relation field", declaration.span) + })?; + let kind = &models[target].fields[field].1; + let kind = if let Type::Optional(inner) = kind { + &**inner + } else { + kind + }; + if *kind != Type::ModelId(models[index].definition) || field < 2 { + return Err(Diagnostic::new( + "C014", + "reverse relation must reference this Model id", + declaration.span, + )); + } + relations.push(Relation { + name: name.clone(), + model: target, + field, + many: true, + optional: false, + definition: names.related[&Type::List(Box::new(Type::Named(definition)))], + }); + } + let mut seen = models[index] + .fields + .iter() + .map(|(name, _)| name.clone()) + .collect::>(); + for relation in &relations { + if !seen.insert(relation.name.clone()) { + return Err(Diagnostic::new( + "C014", + "relation name collides with a Model field or relation", + span, + )); + } + if let (Some(left), Some(right)) = + (&models[index].database, &models[relation.model].database) + && left != right + { + return Err(Diagnostic::new( + "C014", + "relation cannot cross explicit database bindings", + span, + )); + } + } + let definition = &mut definitions[models[index].definition]; + let Shape::Record(fields) = &mut definition.shape else { + unreachable!() + }; + for relation in &relations { + fields.push((relation.name.clone(), Type::Named(relation.definition))); + definition.ranges.push(None); + } + models[index].relations = relations; + } + Ok(()) +} diff --git a/rust/src/compiler/check/database/schema/migration.rs b/rust/src/compiler/check/database/schema/migration.rs new file mode 100644 index 0000000..9268bab --- /dev/null +++ b/rust/src/compiler/check/database/schema/migration.rs @@ -0,0 +1,103 @@ +//! 命名迁移按源码顺序保留操作;SQL 与参数共同进入不可变历史签名。 + +use super::*; + +pub(super) fn check( + source: &ast::Migration, + span: crate::syntax::source::Span, + model: &Model, +) -> Result { + let invalid = |message| Diagnostic::new("C014", message, span); + crate::compiler::check::names::identifier(&source.name, false, span)?; + let mut result = persistence::Migration { + name: source.name.clone(), + drops: vec![], + before: vec![], + after: vec![], + }; + for step in &source.steps { + match step { + ast::MigrationStep::Drop(name) => { + crate::compiler::check::names::identifier(name, false, span)?; + if matches!(name.as_str(), "id" | "created_at") + || model.fields.iter().any(|(field, _)| field == name) + || result.drops.contains(name) + || model + .columns + .iter() + .any(|column| column.previous.as_ref() == Some(name)) + { + return Err(invalid( + "migration drop conflicts with target fields or rename", + )); + } + result.drops.push(name.clone()); + } + ast::MigrationStep::Data { + before, + sqlite, + postgres, + parameters, + } => { + let parameters = parameters + .iter() + .map(|value| parameter(value, span)) + .collect::, _>>()?; + super::sql::check(sqlite, false, parameters.len(), span)?; + super::sql::check(postgres, true, parameters.len(), span)?; + let statement = persistence::Statement { + sqlite: sqlite.clone(), + postgres: postgres.clone(), + parameters, + }; + if *before { + result.before.push(statement); + } else { + result.after.push(statement); + } + } + } + } + if source.steps.is_empty() { + return Err(invalid("migration cannot be empty")); + } + Ok(result) +} + +fn parameter( + source: &ast::Expression, + span: crate::syntax::source::Span, +) -> Result { + let invalid = || { + Diagnostic::new( + "C014", + "migration parameters require Bool, Int, Text, null or exponent Float literals", + span, + ) + }; + let literal = match &source.kind { + ast::ExpressionKind::Literal(value) => value.clone(), + ast::ExpressionKind::Unary(operator, value) if operator == "-" => match &value.kind { + ast::ExpressionKind::Literal(ast::Literal::Number(number)) => { + ast::Literal::Number(format!("-{number}")) + } + _ => return Err(invalid()), + }, + _ => return Err(invalid()), + }; + Ok(match literal { + ast::Literal::Null => persistence::Value::Null, + ast::Literal::Bool(value) => persistence::Value::Integer(i64::from(value)), + ast::Literal::Text(value) => persistence::Value::Text(value), + ast::Literal::Number(value) if value.contains(['e', 'E']) => { + let value = value.parse::().map_err(|_| invalid())?; + if !value.is_finite() { + return Err(invalid()); + } + persistence::Value::Real(value) + } + ast::Literal::Number(value) => { + persistence::Value::Integer(value.parse().map_err(|_| invalid())?) + } + }) +} diff --git a/rust/src/compiler/check/database/schema/mod.rs b/rust/src/compiler/check/database/schema/mod.rs new file mode 100644 index 0000000..e509366 --- /dev/null +++ b/rust/src/compiler/check/database/schema/mod.rs @@ -0,0 +1,116 @@ +//! Model 的 schema 计划完全静态;重命名、索引和唯一键在访问存储前确定。 + +mod migration; +mod seed; +mod sql; +use super::*; + +pub(super) fn check( + models: &mut [Model], + sources: &[ast::TypeDeclaration], + declarations: &[ast::Persistence], + names: &Types, +) -> Result<(), Diagnostic> { + for model in models.iter_mut() { + let ast::TypeBody::Record(fields) = &sources[model.definition].body else { + unreachable!() + }; + let span = sources[model.definition].span; + let invalid = |message| Diagnostic::new("C014", message, span); + let mut previous = std::collections::HashSet::new(); + for column in &model.columns { + if let Some(name) = &column.previous { + crate::compiler::check::names::identifier(name, false, span)?; + if matches!(name.as_str(), "id" | "created_at") + || model.fields.iter().any(|(field, _)| field == name) + || !previous.insert(name) + { + return Err(invalid("ambiguous Model field rename")); + } + } + } + let source = declarations + .iter() + .filter(|declaration| declaration.span.source == model.source) + .collect::>(); + for declaration in &source { + if let ast::PersistenceKind::Index { unique, fields } = &declaration.kind { + let mut positions = Vec::new(); + for name in fields { + let position = model + .fields + .iter() + .position(|(field, _)| field == name) + .ok_or_else(|| invalid("unknown index field"))?; + if positions.contains(&position) { + return Err(invalid("duplicate index field")); + } + positions.push(position); + } + if model + .indexes + .iter() + .any(|index| index.fields == positions && index.unique == *unique) + { + return Err(invalid("duplicate Model index")); + } + model.indexes.push(persistence::Index { + fields: positions, + unique: *unique, + }); + } + } + // owner 与 search 的组合在 schema 中建立普通索引,保持请求筛选可用索引。 + if let (Some(owner), Some(search)) = ( + fields.iter().position(|field| field.rest.owner.is_some()), + fields.iter().position(|field| field.rest.search.is_some()), + ) { + let positions = vec![owner, search]; + if !model.indexes.iter().any(|index| index.fields == positions) { + model.indexes.push(persistence::Index { + fields: positions, + unique: false, + }); + } + } + let mut seeded = false; + for declaration in source { + match &declaration.kind { + ast::PersistenceKind::Index { .. } | ast::PersistenceKind::Sql(_) => (), + ast::PersistenceKind::Relation { .. } => (), + ast::PersistenceKind::Seed(rows) => { + if seeded { + return Err(invalid("only one Seed block is allowed per Model")); + } + seeded = true; + model.seeds = seed::check(rows, model, names)?; + } + ast::PersistenceKind::Migration(value) => { + if model + .migrations + .iter() + .any(|migration| migration.name == value.name) + { + return Err(invalid("duplicate migration name")); + } + model + .migrations + .push(migration::check(value, declaration.span, model)?); + } + } + } + } + for declaration in declarations { + if !models + .iter() + .any(|model| model.source == declaration.span.source) + { + return Err(Diagnostic::new( + "C014", + "storage declaration requires a Model record", + declaration.span, + )); + } + } + Ok(()) +} diff --git a/rust/src/compiler/check/database/schema/seed.rs b/rust/src/compiler/check/database/schema/seed.rs new file mode 100644 index 0000000..cd7959b --- /dev/null +++ b/rust/src/compiler/check/database/schema/seed.rs @@ -0,0 +1,74 @@ +//! Seed 只接受静态字段值,并选择完整非空唯一键判断记录是否已存在。 + +use super::*; + +pub(super) fn check( + rows: &[ast::Expression], + model: &Model, + names: &Types, +) -> Result, Diagnostic> { + rows.iter() + .map(|row| { + let invalid = |message| Diagnostic::new("C014", message, row.span); + let ast::ExpressionKind::Map(entries) = &row.kind else { + return Err(invalid("Seed row requires a direct field block")); + }; + let mut values = Vec::new(); + for (key, value) in entries { + let ast::ExpressionKind::Name(name) = &key.kind else { + return Err(invalid("Seed fields must be statically named")); + }; + let Some(position) = model.fields.iter().position(|(field, _)| field == name) + else { + return Err(invalid("unknown Seed field")); + }; + if position < 2 || model.columns[position].generated { + return Err(invalid("Seed cannot assign generated fields")); + } + if values.iter().any(|(field, _)| *field == position) { + return Err(invalid("duplicate Seed field")); + } + values.push(( + position, + super::super::constant::check( + value, + &model.fields[position].1, + &model.columns[position], + names, + )?, + )); + } + for (position, (_, kind)) in model.fields.iter().enumerate().skip(2) { + if !values.iter().any(|(field, _)| *field == position) + && !model.columns[position].generated + && model.columns[position].default.is_none() + && !matches!(kind, Type::Optional(_)) + { + return Err(invalid("Seed omits a required field")); + } + } + let key = model + .columns + .iter() + .enumerate() + .filter(|(_, column)| column.unique) + .map(|(position, _)| vec![position]) + .chain( + model + .indexes + .iter() + .filter(|index| index.unique) + .map(|index| index.fields.clone()), + ) + .find(|fields| { + fields.iter().all(|field| { + values.iter().any(|(position, value)| { + position == field && !matches!(value, persistence::Value::Null) + }) + }) + }) + .ok_or_else(|| invalid("Seed requires one complete non-null unique key"))?; + Ok(persistence::Seed { values, key }) + }) + .collect() +} diff --git a/rust/src/compiler/check/database/schema/sql.rs b/rust/src/compiler/check/database/schema/sql.rs new file mode 100644 index 0000000..02cbcbd --- /dev/null +++ b/rust/src/compiler/check/database/schema/sql.rs @@ -0,0 +1,196 @@ +//! 迁移 SQL 只允许一条参数化 DML;引号和注释中的分隔符不改变语句边界。 + +use crate::syntax::{diagnostic::Diagnostic, source::Span}; +use std::collections::BTreeSet; + +pub(super) fn check( + sql: &str, + postgres: bool, + parameters: usize, + span: Span, +) -> Result<(), Diagnostic> { + let invalid = || { + Diagnostic::new( + "C014", + "migration requires one INSERT/UPDATE/DELETE with complete numbered bindings and no internal tables", + span, + ) + }; + let bytes = sql.as_bytes(); + let mut cursor = 0; + let mut first = true; + let mut ended = false; + let mut target = false; + let mut bindings = BTreeSet::new(); + while cursor < bytes.len() { + let byte = bytes[cursor]; + if byte.is_ascii_whitespace() { + cursor += 1; + continue; + } + if bytes[cursor..].starts_with(b"--") { + cursor += 2; + while cursor < bytes.len() && bytes[cursor] != b'\n' { + cursor += 1; + } + continue; + } + if bytes[cursor..].starts_with(b"/*") { + cursor += 2; + let mut depth = 1; + while cursor < bytes.len() && depth > 0 { + if postgres && bytes[cursor..].starts_with(b"/*") { + depth += 1; + cursor += 2; + } else if bytes[cursor..].starts_with(b"*/") { + depth -= 1; + cursor += 2; + } else { + cursor += 1; + } + } + if depth != 0 { + return Err(invalid()); + } + continue; + } + if ended { + return Err(invalid()); + } + if byte == b';' { + if first { + return Err(invalid()); + } + ended = true; + cursor += 1; + continue; + } + if matches!(byte, b'\'' | b'"' | b'`' | b'[') { + if first { + return Err(invalid()); + } + let quote = if byte == b'[' { b']' } else { byte }; + cursor += 1; + let start = cursor; + let mut closed = false; + while cursor < bytes.len() { + if bytes[cursor] == quote { + if byte != b'[' && bytes.get(cursor + 1) == Some("e) { + cursor += 2; + continue; + } + closed = true; + break; + } + cursor += 1; + } + if !closed || ((byte != b'\'' || target) && reserved(&sql[start..cursor])) { + return Err(invalid()); + } + cursor += 1; + target = target + && bytes[cursor..] + .iter() + .find(|byte| !byte.is_ascii_whitespace()) + == Some(&b'.'); + continue; + } + if byte == b'$' && postgres && !bytes.get(cursor + 1).is_some_and(u8::is_ascii_digit) { + if first { + return Err(invalid()); + } + let start = cursor; + cursor += 1; + while cursor < bytes.len() + && (bytes[cursor].is_ascii_alphanumeric() || bytes[cursor] == b'_') + { + cursor += 1; + } + if bytes.get(cursor) != Some(&b'$') { + return Err(invalid()); + } + cursor += 1; + let delimiter = &sql[start..cursor]; + let Some(end) = sql[cursor..].find(delimiter) else { + return Err(invalid()); + }; + cursor += end + delimiter.len(); + continue; + } + if matches!(byte, b'?' | b'$') { + if first || byte != if postgres { b'$' } else { b'?' } { + return Err(invalid()); + } + cursor += 1; + let start = cursor; + while cursor < bytes.len() && bytes[cursor].is_ascii_digit() { + cursor += 1; + } + let binding = sql[start..cursor].parse::().map_err(|_| invalid())?; + if binding == 0 || binding > parameters { + return Err(invalid()); + } + bindings.insert(binding); + continue; + } + if byte.is_ascii_alphabetic() || byte == b'_' { + let start = cursor; + cursor += 1; + while cursor < bytes.len() + && (bytes[cursor].is_ascii_alphanumeric() || bytes[cursor] == b'_') + { + cursor += 1; + } + let word = &sql[start..cursor]; + if first { + if !["INSERT", "UPDATE", "DELETE"] + .iter() + .any(|name| word.eq_ignore_ascii_case(name)) + { + return Err(invalid()); + } + first = false; + } + if reserved(word) + || (word.eq_ignore_ascii_case("u") && bytes.get(cursor) == Some(&b'&')) + { + return Err(invalid()); + } + target = (target + && bytes[cursor..] + .iter() + .find(|byte| !byte.is_ascii_whitespace()) + == Some(&b'.')) + || ["UPDATE", "INTO", "FROM", "JOIN"] + .iter() + .any(|name| word.eq_ignore_ascii_case(name)); + continue; + } + if first + || matches!(byte, b'@') + || (byte == b':' + && bytes.get(cursor + 1).is_some_and(u8::is_ascii_alphabetic) + && (cursor == 0 || bytes[cursor - 1] != b':')) + { + return Err(invalid()); + } + if byte != b'.' { + target = false; + } + cursor += 1; + } + if first || bindings.len() != parameters { + return Err(invalid()); + } + Ok(()) +} + +fn reserved(name: &str) -> bool { + let name = name.to_ascii_lowercase(); + name.starts_with("_dever_") + || name.starts_with("sqlite_") + || matches!( + name.as_str(), + "pg_catalog" | "pg_toast" | "information_schema" + ) +} diff --git a/rust/src/compiler/check/database/write.rs b/rust/src/compiler/check/database/write.rs new file mode 100644 index 0000000..4c01c57 --- /dev/null +++ b/rust/src/compiler/check/database/write.rs @@ -0,0 +1,129 @@ +//! 创建、批量创建和 upsert 共用字段检查,不引入可在运行时保存的写入对象。 + +use super::*; +use crate::compiler::model::persistence::{Operation, Write}; + +impl super::super::Context<'_> { + pub(super) fn model_input( + &mut self, + block: &ast::Expression, + model: usize, + selected: &mut Vec, + values: &mut Vec, + ) -> Result<(), Diagnostic> { + let ast::ExpressionKind::Map(entries) = &block.kind else { + return Err(self.error(block, "Model input must be a direct field block")); + }; + for (key, value) in entries { + let ast::ExpressionKind::Name(name) = &key.kind else { + return Err(self.error(key, "Model field must be statically named")); + }; + let position = self.models[model] + .fields + .iter() + .position(|(field, _)| field == name) + .filter(|position| *position >= 2) + .ok_or_else(|| self.error(key, "unknown or generated Model field"))?; + if self.models[model].columns[position].generated { + return Err(self.error(key, "generated Model fields are read-only")); + } + if selected.contains(&position) { + return Err(self.error(key, "duplicate Model field")); + } + let kind = self.models[model].fields[position].1.clone(); + values.push(self.expression(value, Some(&kind))?); + selected.push(position); + } + Ok(()) + } + + pub(super) fn complete_model_input( + &self, + block: &ast::Expression, + model: usize, + selected: &mut Vec, + values: &mut Vec, + ) -> Result<(), Diagnostic> { + for (position, (_, kind)) in self.models[model].fields.iter().enumerate().skip(2) { + let column = &self.models[model].columns[position]; + if selected.contains(&position) || column.generated || column.default.is_some() { + continue; + } + if !matches!(kind, Type::Optional(_)) { + return Err(self.error(block, "missing required Model field")); + } + selected.push(position); + values.push(Expression { + kind: Node::Literal(ast::Literal::Null), + value_type: kind.clone(), + span: block.span, + }); + } + Ok(()) + } + + pub(super) fn model_write( + &mut self, + arguments: &[ast::Expression], + model: usize, + operation: Operation, + source: &ast::Expression, + selected: &mut Vec, + values: &mut Vec, + ) -> Result { + if operation == Operation::CreateMany { + let [list] = arguments else { + return Err(self.error( + source, + "createMany requires one direct list of field blocks", + )); + }; + let ast::ExpressionKind::List(rows) = &list.kind else { + return Err(self.error(list, "createMany requires one direct list of field blocks")); + }; + let mut ends = Vec::new(); + for row in rows { + let mut fields = Vec::new(); + self.model_input(row, model, &mut fields, values)?; + self.complete_model_input(row, model, &mut fields, values)?; + selected.extend(fields); + ends.push(values.len()); + } + return Ok(Write::Many { ends }); + } + let [key, create, update] = arguments else { + return Err(self.error( + source, + "upsert requires unique key, create and update field blocks", + )); + }; + self.model_input(key, model, selected, values)?; + let metadata = &self.models[model]; + let unique = selected.len() == 1 && metadata.columns[selected[0]].unique + || metadata.indexes.iter().any(|index| { + index.unique + && index.fields.len() == selected.len() + && index.fields.iter().all(|field| selected.contains(field)) + }); + if selected.is_empty() || !unique { + return Err(self.error(key, "upsert key must match one complete unique index")); + } + if values + .iter() + .any(|value| matches!(value.kind, Node::Literal(ast::Literal::Null))) + { + return Err(self.error(key, "upsert key cannot contain null")); + } + let key_count = selected.len(); + self.model_input(create, model, selected, values)?; + self.complete_model_input(create, model, selected, values)?; + let insert_count = selected.len(); + let mut updated = Vec::new(); + self.model_input(update, model, &mut updated, values)?; + selected.extend(updated); + Ok(Write::Upsert { + key_count, + insert_count, + }) + } +} diff --git a/rust/src/compiler/check/effects/mod.rs b/rust/src/compiler/check/effects/mod.rs new file mode 100644 index 0000000..5e8b142 --- /dev/null +++ b/rust/src/compiler/check/effects/mod.rs @@ -0,0 +1,351 @@ +//! 沿具体调用依赖传播效果,包含 result 和集合内的静态 handler。 +//! 未调用模板的原型保留未知 handler 效果;具体纯函数绑定仍可使用。 + +use crate::compiler::model::*; +use crate::syntax::diagnostic::Diagnostic; + +pub(super) fn check( + function: &mut Function, + signatures: &[Signature], + packages: &[String], +) -> Result<(), Diagnostic> { + let mut effects = body(function, signatures, false); + if effects.suspend { + effects = body(function, signatures, true); + } + if function.signature.pure + && (Effects { + configuration: false, + ..effects + }) != Effects::default() + { + return Err(Diagnostic::new( + "C013", + "pure function invokes I/O or an unrestricted handler", + function.clauses[0].span, + )); + } + if effects.suspend && (effects.blocking || effects.handler) { + return Err(Diagnostic::new( + "C013", + "suspending function contains inline blocking or unrestricted handler effects; use blocking for a known synchronous boundary", + function.clauses[0].span, + )); + } + if function.signature.transaction && (!effects.database || effects.transaction_unsafe()) { + return Err(Diagnostic::new( + "C014", + "transaction requires database work without external or concurrent operations", + function.clauses[0].span, + )); + } + if effects.unmediated_io + && matches!( + package::role(&packages[function.signature.source]), + Some((_, "app" | "domain")) + ) + { + return Err(Diagnostic::new( + "C013", + "App and Domain external I/O requires a Port boundary", + function.clauses[0].span, + )); + } + if effects.request + && matches!( + package::role(&packages[function.signature.source]), + Some((_, "domain" | "port" | "adapter")) + ) + { + return Err(Diagnostic::new( + "C014", + "request context belongs to App", + function.clauses[0].span, + )); + } + if function.signature.transaction { + effects.unscoped_write = false; + } + function.signature.effects = effects; + Ok(()) +} + +fn body(function: &Function, signatures: &[Signature], suspending: bool) -> Effects { + let mut effects = Effects::default(); + for clause in &function.clauses { + for statement in &clause.body { + let value = match statement { + Statement::Assign { value, .. } + | Statement::Action(value) + | Statement::Fail(value) => value, + }; + effects.include(expression(value, signatures, suspending)); + } + } + effects +} + +pub(super) fn expression( + value: &Expression, + signatures: &[Signature], + suspending: bool, +) -> Effects { + let mut effects = Effects::default(); + match &value.kind { + Node::Literal(_) | Node::Variable(_) => {} + Node::Assert(values) => { + effects.output = true; + effects.include(arguments(values, signatures, suspending)); + } + Node::TestClock(value) => { + effects.timer = true; + effects.include(expression(value, signatures, suspending)); + } + Node::TestDrain { limit, handlers } => { + effects.include(expression(limit, signatures, suspending)); + if !handlers.is_empty() { + effects.database = true; + effects.schedule = true; + effects.suspend = true; + effects.configuration = handlers + .iter() + .any(|index| signatures[*index].effects.configuration); + } + } + Node::Close(value) => { + if matches!(value.value_type, Type::Channel(_)) { + effects.channel = true; + effects.suspend = true; + } else if matches!(value.value_type, Type::AsyncStream(_)) { + effects.async_stream = true; + } else { + effects.stream = true; + } + effects.include(expression(value, signatures, suspending)); + } + Node::Stream(source) | Node::Ticks(source) => { + effects.async_stream = true; + effects.timer = matches!(value.kind, Node::Ticks(_)); + effects.include(expression(source, signatures, suspending)); + } + Node::Channel(capacity) => { + effects.channel = true; + effects.include(expression(capacity, signatures, suspending)); + } + Node::Send(channel, value) => { + effects.channel = true; + effects.suspend = true; + effects.include(expression(channel, signatures, suspending)); + effects.include(expression(value, signatures, suspending)); + } + Node::Receive(channel) => { + effects.channel = true; + effects.suspend = true; + effects.include(expression(channel, signatures, suspending)); + } + Node::Call(index, values) => { + effects.include(signatures[*index].effects); + effects.include(arguments(values, signatures, suspending)); + } + Node::Database(query, values) => { + effects.database = true; + effects.async_stream |= query.operation == persistence::Operation::Stream; + effects.database_write = query.operation.writes(); + effects.unscoped_write = query.operation.writes(); + effects.suspend = true; + effects.schedule = true; + effects.include(arguments(values, signatures, suspending)); + } + Node::Auth(call, values) => { + effects.request = true; + effects.identity = call.operation.identity(); + effects.authentication = call.operation.package() == "dever.auth"; + effects.include(arguments(values, signatures, suspending)); + } + Node::Authorization(call, values) => { + effects.request = true; + effects.identity = true; + effects.authentication = true; + effects.authorization = true; + effects.authorization_write = call.operation.writes(); + effects.database = true; + effects.database_write = call.operation.writes(); + effects.unscoped_write = call.operation.writes(); + effects.suspend = true; + effects.schedule = true; + effects.include(arguments(values, signatures, suspending)); + } + Node::Enqueue(_, values) => { + effects.enqueue = true; + effects.database = true; + effects.database_write = true; + effects.unscoped_write = true; + effects.suspend = true; + effects.schedule = true; + effects.include(arguments(values, signatures, suspending)); + } + Node::Crypto(operation, values) => { + effects.include(operation.effects()); + effects.include(arguments(values, signatures, suspending)); + } + Node::Calendar(operation, values) => { + effects.clock = operation.clock(); + effects.include(arguments(values, signatures, suspending)); + } + Node::Net(operation, values) => { + effects.include(operation.effects()); + effects.include(arguments(values, signatures, suspending)); + } + Node::Http(operation, values) => { + effects.schedule = matches!( + operation, + HttpOperation::Client + | HttpOperation::WsAccept + | HttpOperation::WsConnect + | HttpOperation::WsOpen + ); + if let HttpOperation::Serve(handler, _) | HttpOperation::ServeLive(handler, _) = + operation + { + effects.include(signatures[*handler].effects); + effects.handler = false; + effects.schedule = true; + } + effects.network = true; + effects.unmediated_io = true; + effects.suspend = !matches!(operation, HttpOperation::WsMessages); + effects.include(arguments(values, signatures, suspending)); + } + Node::Io(operation, values) => { + effects.include(operation.effects()); + effects.include(arguments(values, signatures, suspending)); + } + Node::Upload(operation, values) => { + effects.include(operation.effects()); + effects.include(arguments(values, signatures, suspending)); + } + Node::Json(_, values) => effects.include(arguments(values, signatures, suspending)), + Node::Schedule(operation, target, values) => { + effects = signatures[*target].effects; + effects.concurrent |= *operation == Schedule::Run; + effects.blocking = false; + effects.handler = false; + effects.suspend = true; + effects.schedule = true; + effects.include(arguments(values, signatures, suspending)); + } + Node::Join { task, .. } => { + effects.include(expression(task, signatures, suspending)); + effects.suspend = true; + effects.schedule = true; + } + Node::Group(limit) => { + effects.include(expression(limit, signatures, suspending)); + effects.suspend = true; + effects.schedule = true; + } + Node::GroupRun { + group, + target, + arguments: values, + } => { + effects = signatures[*target].effects; + effects.concurrent = true; + effects.blocking = false; + effects.handler = false; + effects.suspend = true; + effects.schedule = true; + effects.include(expression(group, signatures, suspending)); + effects.include(arguments(values, signatures, suspending)); + } + Node::Timeout { + task, + millis, + fallback, + } => { + effects.include(expression(task, signatures, suspending)); + effects.include(expression(millis, signatures, suspending)); + effects.include(signatures[*fallback].effects); + effects.suspend = true; + effects.schedule = true; + effects.timer = true; + } + Node::Race(tasks) => { + effects.include(arguments(tasks, signatures, suspending)); + effects.suspend = true; + effects.schedule = true; + } + Node::Timer(timer, value) => { + effects.include(expression(value, signatures, suspending)); + effects.timer = true; + effects.suspend |= *timer == Timer::Delay; + effects.blocking |= *timer == Timer::Sleep; + } + Node::List(values) + | Node::Variant(_, _, values) + | Node::Access(_, values) + | Node::Standard(_, values) + | Node::Tls(_, values) + | Node::Bytes(_, values) => effects.include(arguments(values, signatures, suspending)), + Node::Record(_, values) => { + for (_, value) in values { + effects.include(expression(value, signatures, suspending)); + } + } + Node::Map(values) => { + for (key, value) in values { + effects.include(expression(key, signatures, suspending)); + effects.include(expression(value, signatures, suspending)); + } + } + Node::Field(value, _) + | Node::TestSecret(value) + | Node::Unary(_, value) + | Node::Some(value) + | Node::Decimal(value) + | Node::Id(value) + | Node::IdText(value) + | Node::Length(value) + | Node::Sum(value) => effects.include(expression(value, signatures, suspending)), + Node::Binary(_, left, right) => { + effects.include(expression(left, signatures, suspending)); + effects.include(expression(right, signatures, suspending)); + } + Node::Capture(capture) => { + effects.include(expression(&capture.call, signatures, suspending)); + } + Node::Collection(collection) => { + let handler = signatures[collection.handler].effects; + effects.include(handler); + if collection.operation == Operation::ParallelEach { + effects.parallel = true; + effects.suspend |= + suspending || matches!(collection.sequence.value_type, Type::AsyncStream(_)); + effects.schedule |= effects.suspend; + effects.blocking = !effects.suspend; + } else { + effects.async_stream |= + matches!(collection.sequence.value_type, Type::AsyncStream(_)); + effects.suspend |= effects.async_stream; + effects.blocking |= matches!(collection.sequence.value_type, Type::Stream(_)); + } + effects.stream |= matches!(collection.sequence.value_type, Type::Stream(_)); + effects.include(expression(&collection.sequence, signatures, suspending)); + if let Some(limit) = &collection.limit { + effects.include(expression(limit, signatures, suspending)); + } + if let Some(extra) = &collection.extra { + effects.include(expression(extra, signatures, suspending)); + } + } + } + effects +} + +fn arguments(values: &[Expression], signatures: &[Signature], suspending: bool) -> Effects { + let mut effects = Effects::default(); + for value in values { + effects.include(expression(value, signatures, suspending)); + } + effects +} diff --git a/rust/src/compiler/check/expressions.rs b/rust/src/compiler/check/expressions.rs new file mode 100644 index 0000000..14ad5b8 --- /dev/null +++ b/rust/src/compiler/check/expressions.rs @@ -0,0 +1,352 @@ +//! 把语法表达式解析为具体类型表达式,保留原始位置和求值顺序。 +//! 只允许合同明确规定的转换,例如 Int 到 Decimal 和非空值到可空值。 + +use super::Context; +use crate::compiler::model::*; +use crate::syntax::{ + ast::{self, ExpressionKind as Syntax, Literal}, + diagnostic::Diagnostic, +}; + +impl Context<'_> { + pub(super) fn expression( + &mut self, + source: &ast::Expression, + expected: Option<&Type>, + ) -> Result { + if let Some(value) = self.model_options(source)? { + return self.coerce(value, expected); + } + if let Some(path) = source.path() + && let Some((index, variant)) = super::names::variant( + &path, + self.source, + self.definition_names, + self.definitions, + source.span, + )? + { + return self.variant(index, variant, &[], expected, source); + } + let value = match &source.kind { + Syntax::Literal(literal) => self.literal(literal, expected, source)?, + Syntax::Name(name) => { + let index = self + .variables + .iter() + .position(|(candidate, _)| candidate == name) + .ok_or_else(|| self.error(source, format!("unknown value '{name}'")))?; + if !self.initialized[index] { + return Err(self.error(source, format!("'{name}' is not initialized"))); + } + Expression { + kind: Node::Variable(index), + value_type: self.variables[index].1.clone(), + span: source.span, + } + } + Syntax::Field(base, name) => { + let base = self.expression(base, None)?; + let fields = base.value_type.fields(self.definitions).ok_or_else(|| { + self.error( + source, + "field access requires a record, MapEntry or named outputs", + ) + })?; + let index = fields + .iter() + .position(|(field, _)| *field == name) + .ok_or_else(|| self.error(source, format!("unknown field '{name}'")))?; + let value_type = fields[index].1.clone(); + if let Type::Named(record) = &base.value_type { + let definition = &self.definitions[*record]; + if definition.private_fields.contains(&index) { + self.definition_names.private_field( + self.source, + definition.source, + source.span, + )?; + } + } + Expression { + kind: Node::Field(Box::new(base), index), + value_type, + span: source.span, + } + } + Syntax::Channel(element, capacity) => { + return self.channel(element, capacity, expected, source); + } + Syntax::Call(target, arguments) => { + return self.call(target, arguments, expected, source); + } + Syntax::Unary(operator, operand) => { + if operator == "-" + && let Syntax::Literal(Literal::Number(number)) = &operand.kind + { + return self.expression( + &ast::Expression { + kind: Syntax::Literal(Literal::Number(format!("-{number}"))), + span: source.span, + depth: 1, + }, + expected, + ); + } + let operand = self.expression(operand, expected)?; + if (operator == "not" && operand.value_type != Type::Bool) + || (operator == "-" && !operand.value_type.numeric()) + { + return Err(self.error(source, "invalid unary operand")); + } + Expression { + value_type: operand.value_type.clone(), + kind: Node::Unary(operator.clone(), Box::new(operand)), + span: source.span, + } + } + Syntax::Binary(operator, left, right) => { + self.binary(operator, left, right, expected, source)? + } + Syntax::List(items) => { + let inner = match present_type(expected) { + Some(Type::List(inner)) => Some(inner.as_ref()), + _ => None, + }; + let mut values = Vec::new(); + let mut element = inner.cloned(); + for item in items { + let value = self.expression(item, element.as_ref())?; + if element.is_none() { + element = Some(value.value_type.clone()); + } + values.push(value); + } + let element = element + .ok_or_else(|| self.error(source, "empty List requires a type context"))?; + Expression { + kind: Node::List(values), + value_type: Type::List(Box::new(element)), + span: source.span, + } + } + Syntax::Record(name, fields) => { + let index = self + .definition_names + .resolve(name, self.source, source.span)? + .ok_or_else(|| self.error(source, format!("unknown record '{name}'")))?; + let Shape::Record(declared) = &self.definitions[index].shape else { + return Err(self.error(source, "record construction requires a record type")); + }; + let definition = &self.definitions[index]; + if matches!( + package::role(&self.definition_names.packages[definition.source]), + Some((_, "model")) + ) { + return Err(Diagnostic::new( + "C014", + "Model snapshots are produced by ORM operations", + source.span, + )); + } + if !definition.private_fields.is_empty() { + self.definition_names.private_field( + self.source, + definition.source, + source.span, + )?; + } + let declared = declared.clone(); + if fields.len() != declared.len() { + return Err(self.error( + source, + "record construction must supply every field exactly once", + )); + } + let mut values = Vec::new(); + let mut supplied = std::collections::HashSet::new(); + // 保存源码字段顺序;声明中的字段位置只用于最终存放结果。 + for (name, expression) in fields { + let position = declared + .iter() + .position(|(field, _)| field == name) + .ok_or_else(|| self.error(source, format!("unknown field '{name}'")))?; + if !supplied.insert(position) { + return Err(self.error(source, format!("duplicate field '{name}'"))); + } + values.push(( + position, + self.expression(expression, Some(&declared[position].1))?, + )); + } + Expression { + kind: Node::Record(index, values), + value_type: Type::Named(index), + span: source.span, + } + } + Syntax::Map(entries) => self.map(entries, expected, source)?, + }; + self.coerce(super::constants::fold(value)?, expected) + } + + fn literal( + &self, + literal: &Literal, + expected: Option<&Type>, + source: &ast::Expression, + ) -> Result { + let value_type = match literal { + Literal::Bool(_) => Type::Bool, + Literal::Text(_) => Type::Text, + Literal::Null => match expected { + Some(Type::Optional(inner)) => Type::Optional(inner.clone()), + _ => return Err(self.error(source, "null requires a nullable type context")), + }, + Literal::Number(value) => { + if value.contains(['e', 'E']) + || (value.contains('.') && present_type(expected) == Some(&Type::Float)) + { + if !value.parse::().is_ok_and(f64::is_finite) { + return Err(self.error(source, "Float literal is not representable")); + } + Type::Float + } else if value.contains('.') { + dever_runtime::value::decimal::Decimal::parse(value) + .map_err(|error| self.error(source, error))?; + Type::Decimal + } else { + value + .parse::() + .map_err(|_| self.error(source, "Int literal is not representable"))?; + Type::Int + } + } + }; + Ok(Expression { + kind: Node::Literal(literal.clone()), + value_type, + span: source.span, + }) + } + + fn binary( + &mut self, + operator: &str, + left: &ast::Expression, + right: &ast::Expression, + expected: Option<&Type>, + source: &ast::Expression, + ) -> Result { + let context = (present_type(expected) == Some(&Type::Float)).then_some(&Type::Float); + let (mut left, mut right) = if matches!(operator, "==" | "!=") + && matches!(left.kind, Syntax::Literal(Literal::Null)) + { + let right = self.expression(right, context)?; + (self.expression(left, Some(&right.value_type))?, right) + } else { + let left = self.expression(left, context)?; + let context = if matches!(operator, "==" | "!=") + && matches!(left.value_type, Type::Optional(_)) + { + Some(&left.value_type) + } else { + context + }; + let right = self.expression(right, context)?; + (left, right) + }; + if matches!(operator, "==" | "!=") + && matches!(right.value_type, Type::Optional(_)) + && !matches!(left.value_type, Type::Optional(_)) + { + left = self.coerce(left, Some(&right.value_type))?; + } + if matches!( + (&left.value_type, &right.value_type), + (Type::Int, Type::Decimal) | (Type::Decimal, Type::Int) + ) || (operator == "/" && left.value_type == Type::Int && right.value_type == Type::Int) + { + left = self.coerce(left, Some(&Type::Decimal))?; + right = self.coerce(right, Some(&Type::Decimal))?; + } + if left.value_type != right.value_type { + return Err(self.error(source, "binary operand types differ")); + } + let kind = &left.value_type; + let value_type = match operator { + "and" | "or" if *kind == Type::Bool => Type::Bool, + "==" | "!=" if kind.comparable(self.definitions) => Type::Bool, + "<" | "<=" | ">" | ">=" if kind.numeric() => Type::Bool, + "+" if *kind == Type::Text => Type::Text, + "+" | "-" | "*" | "/" if kind.numeric() => kind.clone(), + "//" | "%" if *kind == Type::Int => Type::Int, + _ => { + return Err(self.error( + source, + format!("operator '{operator}' does not accept {kind:?}"), + )); + } + }; + Ok(Expression { + kind: Node::Binary(operator.into(), Box::new(left), Box::new(right)), + value_type, + span: source.span, + }) + } + + pub(super) fn coerce( + &self, + value: Expression, + expected: Option<&Type>, + ) -> Result { + let Some(expected) = expected else { + return Ok(value); + }; + if &value.value_type == expected { + return Ok(value); + } + let span = value.span; + if *expected == Type::Decimal && value.value_type == Type::Int { + // Int 常量在编译期提升;先按 Int 取值,避免把整数 -0 变成 Decimal 负零。 + let kind = match &value.kind { + Node::Literal(Literal::Number(number)) => Node::Literal(Literal::Number( + number + .parse::() + .expect("checked Int literal") + .to_string(), + )), + _ => Node::Decimal(Box::new(value)), + }; + return Ok(Expression { + kind, + value_type: Type::Decimal, + span, + }); + } + if let Type::Optional(inner) = expected { + let value = self.coerce(value, Some(inner))?; + return Ok(Expression { + kind: Node::Some(Box::new(value)), + value_type: expected.clone(), + span, + }); + } + Err(Diagnostic::new( + "C005", + format!("expected {expected:?}, got {:?}", value.value_type), + span, + )) + } + + pub(super) fn error(&self, source: &ast::Expression, message: impl Into) -> Diagnostic { + Diagnostic::new("C005", message, source.span) + } +} + +fn present_type(expected: Option<&Type>) -> Option<&Type> { + match expected { + Some(Type::Optional(inner)) => Some(inner), + expected => expected, + } +} diff --git a/rust/src/compiler/check/failure/capture.rs b/rust/src/compiler/check/failure/capture.rs new file mode 100644 index 0000000..ec2727d --- /dev/null +++ b/rust/src/compiler/check/failure/capture.rs @@ -0,0 +1,167 @@ +//! 为 result(call) 选择唯一且名义身份匹配的 choice。 +//! 仅载荷形状相同不足以匹配业务错误;数值 Fault 不进入捕获集合。 + +use crate::compiler::{check::Context, model::*}; +use crate::syntax::{ast, diagnostic::Diagnostic}; +use std::collections::BTreeMap; + +impl Context<'_> { + pub(crate) fn capture( + &mut self, + call: &ast::Expression, + expected: Option<&Type>, + source: &ast::Expression, + ) -> Result { + let target = match expected { + Some(Type::Optional(inner)) => Some(inner.as_ref()), + expected => expected, + }; + // 泛型解码直接使用结果 Choice 的成功字段作为类型上下文,不生成包装函数。 + let decoded = if matches!(&call.kind, ast::ExpressionKind::Call(target, _) + if target.path().as_deref() == Some("dever.json.decode")) + { + let Some(Type::Named(index)) = target else { + return Err(self.error( + source, + "result(dever.json.decode(...)) requires an explicit result choice", + )); + }; + let Shape::Choice(variants) = &self.definitions[*index].shape else { + return Err(self.error(source, "JSON decode result requires a choice")); + }; + let mut successes = variants.iter().filter(|variant| !variant.error); + let (Some(success), None) = (successes.next(), successes.next()) else { + return Err(self.error( + source, + "JSON decode result requires exactly one success variant", + )); + }; + let [(_, kind)] = success.fields.as_slice() else { + return Err(self.error(source, "JSON decode success variant requires one value")); + }; + Some(kind.clone()) + } else { + None + }; + let value = self.expression(call, decoded.as_ref())?; + let decoded_failure; + let (origin, failures) = match &value.kind { + Node::Call(function, _) => { + let signature = &self.signatures[*function]; + (signature.source, signature.failures.as_slice()) + } + Node::Json(JsonOperation::Decode { error }, _) => { + decoded_failure = [Failure { + definition: *error, + variant: 1, + }]; + (self.definitions[*error].source, decoded_failure.as_slice()) + } + _ => { + return Err(self.error( + source, + "result requires a function call or typed JSON decode", + )); + } + }; + let mut candidates = Vec::new(); + for (index, definition) in self.definitions.iter().enumerate() { + if definition.source != self.source && !self.definition_names.exported(index) { + continue; + } + if target.is_none() + && definition.source != self.source + && definition.source != origin + && !failures.iter().any(|failure| failure.definition == index) + { + continue; + } + if target.is_some_and(|kind| *kind != Type::Named(index)) { + continue; + } + let Shape::Choice(variants) = &definition.shape else { + continue; + }; + let successes = variants + .iter() + .enumerate() + .filter(|(_, variant)| !variant.error) + .collect::>(); + let [(success, variant)] = successes.as_slice() else { + continue; + }; + let outputs_match = match &value.value_type { + Type::Unit => variant.fields.is_empty(), + Type::Outputs(outputs) => variant + .fields + .iter() + .map(|(_, kind)| kind) + .eq(outputs.iter().map(|(_, kind)| kind)), + kind => matches!(variant.fields.as_slice(), [(_, field)] if field == kind), + }; + if !outputs_match { + continue; + } + if let Some(targets) = targets(index, variants, failures, self.definitions) { + candidates.push((index, *success, targets)); + } + } + if candidates.len() != 1 { + return Err(Diagnostic::new( + "C005", + "result needs one unambiguous choice with matching success outputs and all declared errors", + source.span, + )); + } + let (definition, success, targets) = candidates.pop().unwrap(); + let capture = Capture { + call: value, + definition, + success, + targets, + }; + self.coerce( + Expression { + kind: Node::Capture(Box::new(capture)), + value_type: Type::Named(definition), + span: source.span, + }, + expected, + ) + } +} + +fn targets( + index: usize, + variants: &[Variant], + actual: &[Failure], + definitions: &[Definition], +) -> Option> { + let mut mapping = BTreeMap::new(); + for (position, variant) in variants.iter().enumerate().filter(|(_, value)| value.error) { + let direct = Failure { + definition: index, + variant: position, + }; + if actual.contains(&direct) { + mapping.insert(direct, (position, false)); + } + if let [(_, Type::Named(inner))] = variant.fields.as_slice() + && let Shape::Choice(_) = &definitions[*inner].shape + { + for failure in actual.iter().filter(|failure| failure.definition == *inner) { + if mapping.insert(*failure, (position, true)).is_some() { + return None; + } + } + } + } + actual + .iter() + .map(|failure| { + mapping + .get(failure) + .map(|(variant, wrapped)| (*failure, *variant, *wrapped)) + }) + .collect() +} diff --git a/rust/src/compiler/check/failure/collection.rs b/rust/src/compiler/check/failure/collection.rs new file mode 100644 index 0000000..4361103 --- /dev/null +++ b/rust/src/compiler/check/failure/collection.rs @@ -0,0 +1,194 @@ +//! 推导完整遍历、提前停止及归约中的失败处理义务。 +//! 未知长度归约合并初始与迭代摘要直到稳定,保留空输入的可能性。 + +use super::{State, flow}; +use crate::compiler::model::*; +use crate::syntax::diagnostic::Diagnostic; +use std::collections::{BTreeMap, BTreeSet}; + +impl State<'_> { + pub(super) fn collection(&mut self, collection: &Collection) -> Result { + if collection + .sequence + .value_type + .database_stream(self.definitions) + && let Some(definition) = self.database_error + { + let Shape::Choice(variants) = &self.definitions[definition].shape else { + unreachable!() + }; + self.failures + .extend((0..variants.len()).map(|variant| Failure { + definition, + variant, + })); + } + let sequence = self.expression(&collection.sequence)?; + if let Some(limit) = &collection.limit { + self.expression(limit)?; + } + let extra = collection + .extra + .as_ref() + .map(|value| self.expression(value)) + .transpose()?; + let signature = &self.signatures[collection.handler]; + self.failures.extend(&signature.failures); + let before = self.pending.clone(); + // Stream 的失败元素尚未产生;只在实际消费合同中引入义务,关闭不等于丢弃已读错误。 + let shape = + if let Type::Stream(item) | Type::AsyncStream(item) = &collection.sequence.value_type { + match &sequence.sequence { + // 转发流可能没有现存错误 token,但未来拉取仍会产生错误义务。 + Some(Sequence::Unknown(value)) if value.tokens.is_empty() => { + Sequence::Unknown(Box::new(self.unknown(item, 0)?)) + } + Some(sequence) => sequence.clone(), + None => Sequence::Unknown(Box::new(self.unknown(item, 0)?)), + } + } else { + sequence + .sequence + .clone() + .unwrap_or_else(|| Sequence::Unknown(Box::default())) + }; + let mut produced = Vec::new(); + let mut state = extra.clone().unwrap_or_default(); + let mut fresh = BTreeMap::new(); + match &shape { + Sequence::Known(items) => { + for item in items { + self.budget(0)?; + let value = + self.apply_handler(collection, item, &state, &mut BTreeMap::new())?; + if collection.operation.reduction() { + state = value; + produced.push(state.clone()); + } else { + produced.push(value); + } + } + } + Sequence::Unknown(item) => { + if collection.operation.reduction() { + // 初始状态参与合并,因为未知集合可以完全不执行 handler。 + loop { + self.budget(0)?; + let value = self.apply_handler(collection, item, &state, &mut fresh)?; + let previous = state.clone(); + state.merge(&value); + if previous == state { + break; + } + } + produced.push(state.clone()); + } else { + produced.push(self.apply_handler(collection, item, &state, &mut fresh)?); + } + } + } + let empty = matches!(&shape, Sequence::Known(items) if items.is_empty()); + let nonempty = matches!(&shape, Sequence::Known(items) if !items.is_empty()); + let partial = matches!( + collection.operation, + Operation::Find | Operation::ReduceUntil + ); + self.pending = before; + // 提前结束只保证访问首项,不能用选中的输出消费剩余元素的义务。 + if partial { + if let Sequence::Known(items) = &shape + && let Some(first) = items.first() + { + self.pending.retain(|token| !first.tokens.contains(token)); + } + } else { + self.pending + .retain(|token| !sequence.tokens.contains(token)); + } + if let Some(extra) = &extra + && (nonempty || collection.operation.reduction()) + { + self.pending.retain(|token| !extra.tokens.contains(token)); + } + let mut output = match collection.operation { + Operation::ParallelEach => Flow::default(), + Operation::Each if signature.outputs.is_empty() => Flow::default(), + Operation::Each => Flow::sequence(match shape { + Sequence::Known(_) => Sequence::Known(produced), + Sequence::Unknown(_) => Sequence::Unknown(Box::new(produced.pop().unwrap())), + }), + Operation::Filter if empty => Flow::sequence(Sequence::Known(Vec::new())), + Operation::Filter => Flow::sequence(Sequence::Unknown(Box::new(shape.element()))), + Operation::Find => { + let item = if empty { + return Ok(Flow::variants([(0, Flow::default())])); + } else if matches!(&shape, Sequence::Known(items) if items.len() == 1) { + shape.element() + } else { + self.unknown(&signature.inputs[0].1, 0)? + }; + Flow::variants([(0, Flow::default()), (1, item)]) + } + Operation::Sum => Flow::default(), + Operation::Reduce => state, + Operation::ReduceUntil => { + if let Some((first, rest)) = produced.split_first() { + let mut output = first.clone(); + for value in rest { + output.merge(value); + } + output + } else { + state + } + } + }; + let retained: BTreeSet<_> = self.pending.intersection(&output.tokens).copied().collect(); + if !retained.is_empty() { + let mut mapping = BTreeMap::new(); + for token in &output.tokens { + let target = if retained.contains(token) { + self.token()? + } else { + *token + }; + mapping.insert(*token, BTreeSet::from([target])); + } + output = self.instantiate(&output, &mut mapping, 0)?; + } + self.pending.extend(&output.tokens); + self.closed_streams.extend(sequence.streams); + self.refresh_streams(&mut output); + Ok(output) + } + + fn apply_handler( + &mut self, + collection: &Collection, + item: &Flow, + extra: &Flow, + fresh: &mut BTreeMap>, + ) -> Result { + let signature = &self.signatures[collection.handler]; + let mut mapping = fresh.clone(); + for template in &signature.input_flows { + for token in &template.tokens { + mapping.remove(token); + } + } + for (template, actual) in signature.input_flows.iter().zip([item, extra]) { + flow::bind(template, actual, &mut mapping); + } + let output = if let Some(value) = &signature.returns { + self.instantiate(value, &mut mapping, 0)? + } else { + Flow::default() + }; + *fresh = mapping; + if collection.operation == Operation::ReduceUntil { + Ok(output.fields.get(&0).cloned().unwrap_or_default()) + } else { + Ok(output) + } + } +} diff --git a/rust/src/compiler/check/failure/expression.rs b/rust/src/compiler/check/failure/expression.rs new file mode 100644 index 0000000..ddd0cc4 --- /dev/null +++ b/rust/src/compiler/check/failure/expression.rs @@ -0,0 +1,371 @@ +//! 按表达式求值规则传播失败摘要。 +//! 可能被短路跳过的处理不能解除已有义务;捕获调用的参数失败仍向外传播。 + +use super::State; +use crate::compiler::model::*; +use crate::syntax::{ast::Literal, diagnostic::Diagnostic}; +use std::collections::BTreeMap; + +impl State<'_> { + pub(super) fn expression(&mut self, expression: &Expression) -> Result { + self.budget(0)?; + match &expression.kind { + Node::Enqueue(call, arguments) => { + for argument in arguments { + self.expression(argument)?; + } + let Shape::Choice(variants) = &self.definitions[call.error].shape else { + unreachable!() + }; + self.failures + .extend((0..variants.len()).map(|variant| Failure { + definition: call.error, + variant, + })); + self.unknown(&expression.value_type, 0) + } + Node::Authorization(call, arguments) => { + for argument in arguments { + self.expression(argument)?; + } + for definition in [call.api_error, call.database_error] { + let Shape::Choice(variants) = &self.definitions[definition].shape else { + unreachable!() + }; + self.failures + .extend((0..variants.len()).map(|variant| Failure { + definition, + variant, + })); + } + self.unknown(&expression.value_type, 0) + } + Node::Auth(call, arguments) => { + for argument in arguments { + self.expression(argument)?; + } + if call.operation != auth::Operation::Site { + let Shape::Choice(variants) = &self.definitions[call.error].shape else { + unreachable!() + }; + self.failures.extend(variants.iter().enumerate().filter_map( + |(variant, value)| { + matches!( + value.name.as_str(), + "Invalid" | "Unauthorized" | "Forbidden" + ) + .then_some(Failure { + definition: call.error, + variant, + }) + }, + )); + } + self.unknown(&expression.value_type, 0) + } + Node::Json(JsonOperation::Decode { error }, arguments) => { + self.expression(&arguments[0])?; + self.failures.insert(Failure { + definition: *error, + variant: 1, + }); + self.unknown(&expression.value_type, 0) + } + Node::Database(query, arguments) => { + for argument in arguments { + self.expression(argument)?; + } + let Shape::Choice(variants) = &self.definitions[query.error].shape else { + unreachable!() + }; + self.failures + .extend((0..variants.len()).map(|variant| Failure { + definition: query.error, + variant, + })); + self.unknown(&expression.value_type, 0) + } + Node::Literal(Literal::Null) => Ok(Flow::variants([(0, Flow::default())])), + Node::Literal(_) => Ok(Flow::default()), + Node::Variable(index) => { + let mut value = self.values[*index].clone(); + self.refresh_streams(&mut value); + Ok(value) + } + Node::Field(value, index) => Ok(self + .expression(value)? + .fields + .get(index) + .cloned() + .unwrap_or_default()), + Node::Some(value) => Ok(Flow::variants([(1, self.expression(value)?)])), + Node::List(values) => Ok(Flow::sequence(Sequence::Known( + values + .iter() + .map(|value| self.expression(value)) + .collect::>()?, + ))), + Node::Map(entries) => { + let mut values = Vec::new(); + for (key, value) in entries { + values.push(Flow::fields([ + (0, self.expression(key)?), + (1, self.expression(value)?), + ])); + } + Ok(Flow::sequence(Sequence::Known(values))) + } + Node::Access(operation, arguments) => { + self.access(*operation, arguments, &expression.value_type) + } + Node::Net(operation, arguments) => { + for argument in arguments { + self.expression(argument)?; + } + match operation { + NetOperation::Connections => self.lazy_stream(), + NetOperation::Chunks => { + let Type::Named(index) = expression.value_type else { + unreachable!("checked stream result"); + }; + Ok(Flow::variants([ + (0, Flow::fields([(0, self.lazy_stream()?)])), + (1, self.unknown_variant(index, 1, 0)?), + ])) + } + _ => self.unknown(&expression.value_type, 0), + } + } + Node::Http(HttpOperation::Open | HttpOperation::OpenStream, arguments) => { + for argument in arguments { + let value = self.expression(argument)?; + // 上传源的失败由 HTTP 打开结果或响应流交付,不能留下重复义务。 + self.pending.retain(|token| !value.tokens.contains(token)); + } + let Type::Named(index) = expression.value_type else { + unreachable!("checked HTTP stream result"); + }; + // 下载尚未拉取;沿用 TCP 流的延迟失败证明,关闭不会伪造已读错误。 + Ok(Flow::variants([ + ( + 0, + Flow::fields([(0, Flow::fields([(2, self.lazy_stream()?)]))]), + ), + (1, self.unknown_variant(index, 1, 0)?), + ])) + } + Node::Crypto(_, arguments) + | Node::Assert(arguments) + | Node::Calendar(_, arguments) + | Node::Standard(_, arguments) + | Node::Bytes(_, arguments) + | Node::Http(_, arguments) + | Node::Tls(_, arguments) + | Node::Io(_, arguments) + | Node::Upload(_, arguments) + | Node::Json(_, arguments) => { + for argument in arguments { + self.expression(argument)?; + } + self.unknown(&expression.value_type, 0) + } + Node::Variant(index, variant, values) => { + let mut fields = Vec::new(); + for (index, value) in values.iter().enumerate() { + fields.push((index, self.expression(value)?)); + } + let value = Flow::fields(fields); + Ok(Flow::variants([( + *variant, + self.variant(*index, *variant, value)?, + )])) + } + Node::Record(_, values) => { + let mut fields = Vec::new(); + for (index, value) in values { + fields.push((*index, self.expression(value)?)); + } + Ok(Flow::fields(fields)) + } + Node::Close(value) => { + let value = self.expression(value)?; + self.closed_streams.extend(value.streams); + Ok(Flow::default()) + } + Node::Unary(_, value) + | Node::TestSecret(value) + | Node::TestClock(value) + | Node::TestDrain { limit: value, .. } + | Node::Decimal(value) + | Node::Id(value) + | Node::IdText(value) + | Node::Length(value) + | Node::Sum(value) => { + self.expression(value)?; + Ok(Flow::default()) + } + Node::Binary(operator, left, right) => { + self.expression(left)?; + if matches!(operator.as_str(), "and" | "or") { + // 右侧仍需通过检查,但可能不执行,处理义务必须保留该路径。 + if let Node::Literal(Literal::Bool(value)) = left.kind { + if value == (operator == "and") { + self.expression(right)?; + } else { + let pending = self.pending.clone(); + let closed = self.closed_streams.clone(); + self.expression(right)?; + self.pending = pending; + self.closed_streams = closed; + } + } else { + let pending = self.pending.clone(); + self.expression(right)?; + self.pending.extend(pending); + } + } else { + self.expression(right)?; + } + Ok(Flow::default()) + } + Node::Call(index, arguments) => { + let signature = &self.signatures[*index]; + let mut mapping = self.arguments(arguments, signature)?; + self.failures.extend(&signature.failures); + if let Some(value) = &signature.returns { + self.instantiate(value, &mut mapping, 0) + } else { + Ok(Flow::default()) + } + } + Node::Schedule(operation, index, arguments) => { + self.schedule(*operation, *index, arguments) + } + Node::Join { task, stop } => self.join(task, *stop), + Node::Timeout { + task, + millis, + fallback, + } => self.timeout(task, millis, *fallback), + Node::Race(tasks) => self.race(tasks), + Node::Group(limit) => self.group(limit), + Node::Stream(value) => { + let mut value = self.expression(value)?; + value.streams.insert(self.symbol()?); + Ok(value) + } + Node::Ticks(millis) => { + self.expression(millis)?; + Ok(Flow { + streams: [self.symbol()?].into(), + ..Flow::default() + }) + } + Node::Channel(capacity) => { + self.expression(capacity)?; + Ok(Flow::default()) + } + Node::Send(channel, value) => { + self.expression(channel)?; + let value = self.expression(value)?; + self.pending.retain(|token| !value.tokens.contains(token)); + Ok(Flow::default()) + } + Node::Receive(channel) => { + self.expression(channel)?; + self.unknown(&expression.value_type, 0) + } + Node::GroupRun { + group, + target, + arguments, + } => self.group_run(group, *target, arguments), + Node::Timer(_, millis) => { + self.expression(millis)?; + self.unknown(&expression.value_type, 0) + } + Node::Capture(capture) => self.capture(capture), + Node::Collection(collection) => self.collection(collection), + } + } + + fn capture(&mut self, capture: &Capture) -> Result { + let mut possibilities = BTreeMap::::new(); + match &capture.call.kind { + Node::Call(function, arguments) => { + let signature = &self.signatures[*function]; + let mut mapping = self.arguments(arguments, signature)?; + if let Some(value) = &signature.returns { + let value = self.instantiate(value, &mut mapping, 0)?; + let fields = if signature.outputs.len() == 1 { + Flow::fields([(0, value)]) + } else { + value + }; + possibilities.insert(capture.success, fields); + } + } + Node::Json(JsonOperation::Decode { .. }, arguments) => { + self.expression(&arguments[0])?; + let value = self.unknown(&capture.call.value_type, 0)?; + possibilities.insert(capture.success, Flow::fields([(0, value)])); + } + _ => unreachable!("checked capture call"), + } + for (failure, target, wrapped) in &capture.targets { + let source = self.unknown_variant(failure.definition, failure.variant, 0)?; + let value = if *wrapped { + self.variant( + capture.definition, + *target, + Flow::fields([(0, Flow::variants([(failure.variant, source)]))]), + )? + } else { + source + }; + if let Some(previous) = possibilities.get_mut(target) { + previous.merge(&value); + } else { + possibilities.insert(*target, value); + } + } + Ok(Flow::variants(possibilities)) + } + + pub(super) fn fail(&mut self, value: &Expression) -> Result<(), Diagnostic> { + let flow = self.expression(value)?; + let Type::Named(index) = value.value_type else { + return Err(Diagnostic::new( + "C005", + "fail requires an error choice variant", + value.span, + )); + }; + let Shape::Choice(variants) = &self.definitions[index].shape else { + return Err(Diagnostic::new( + "C005", + "fail requires an error choice variant", + value.span, + )); + }; + let possible = if flow.variants.is_empty() { + (0..variants.len()).collect::>() + } else { + flow.variants.keys().copied().collect() + }; + for variant in possible { + if !variants[variant].error { + return Err(Diagnostic::new( + "C005", + "fail can receive a non-error variant", + value.span, + )); + } + self.failures.insert(Failure { + definition: index, + variant, + }); + } + Ok(()) + } +} diff --git a/rust/src/compiler/check/failure/flow.rs b/rust/src/compiler/check/failure/flow.rs new file mode 100644 index 0000000..4e81dec --- /dev/null +++ b/rust/src/compiler/check/failure/flow.rs @@ -0,0 +1,247 @@ +//! 构造、绑定和实例化嵌套值的失败摘要。 +//! 函数摘要中的义务编号在调用处重新映射,避免不同调用互相抵消错误。 + +use super::State; +use crate::compiler::model::*; +use crate::syntax::diagnostic::Diagnostic; +use std::collections::{BTreeMap, BTreeSet}; + +pub(super) fn tracked_types(definitions: &[Definition]) -> Vec { + fn tracked(kind: &Type, definitions: &[Definition], values: &mut [Option]) -> bool { + match kind { + Type::List(_) | Type::Map(_, _) | Type::AsyncStream(_) => true, + Type::Optional(inner) => tracked(inner, definitions, values), + Type::MapEntry(key, value) => { + tracked(key, definitions, values) || tracked(value, definitions, values) + } + Type::Named(index) => { + if let Some(value) = values[*index] { + return value; + } + let value = !definitions[*index].related + && match &definitions[*index].shape { + Shape::Record(fields) => fields + .iter() + .any(|(_, kind)| tracked(kind, definitions, values)), + Shape::Choice(variants) => variants.iter().any(|variant| { + variant.error + || variant + .fields + .iter() + .any(|(_, kind)| tracked(kind, definitions, values)) + }), + }; + values[*index] = Some(value); + value + } + _ => false, + } + } + // 类型检查已排除值类型环;只为含集合、流或错误的记录建立结构摘要。 + let mut values = vec![None; definitions.len()]; + for index in 0..definitions.len() { + tracked(&Type::Named(index), definitions, &mut values); + } + values.into_iter().map(Option::unwrap).collect() +} + +impl State<'_> { + pub(super) fn unknown(&mut self, kind: &Type, depth: usize) -> Result { + self.budget(depth)?; + if let Type::List(inner) | Type::AsyncStream(inner) = kind { + let mut value = + Flow::sequence(Sequence::Unknown(Box::new(self.unknown(inner, depth + 1)?))); + if matches!(kind, Type::AsyncStream(_)) { + value.streams.insert(self.symbol()?); + } + return Ok(value); + } + if let Type::Map(key, value) = kind { + let entry = Type::MapEntry(key.clone(), value.clone()); + return Ok(Flow::sequence(Sequence::Unknown(Box::new( + self.unknown(&entry, depth + 1)?, + )))); + } + // 没有业务错误的记录仍可能含集合或流,字段摘要不能因此丢弃。 + match kind { + Type::Named(index) if !self.tracked_types[*index] => Ok(Flow::default()), + Type::Named(index) => match &self.definitions[*index].shape { + Shape::Record(fields) => { + let mut values = Vec::new(); + for (index, (_, kind)) in fields.iter().enumerate() { + values.push((index, self.unknown(kind, depth + 1)?)); + } + Ok(Flow::fields(values)) + } + Shape::Choice(variants) => { + let mut values = Vec::new(); + for variant in 0..variants.len() { + values.push((variant, self.unknown_variant(*index, variant, depth + 1)?)); + } + Ok(Flow::variants(values)) + } + }, + Type::Optional(inner) => Ok(Flow::variants([ + (0, Flow::default()), + (1, self.unknown(inner, depth + 1)?), + ])), + Type::MapEntry(key, value) => Ok(Flow::fields([ + (0, self.unknown(key, depth + 1)?), + (1, self.unknown(value, depth + 1)?), + ])), + _ => Ok(Flow::default()), + } + } + + pub(super) fn unknown_variant( + &mut self, + index: usize, + variant: usize, + depth: usize, + ) -> Result { + self.budget(depth)?; + let Shape::Choice(variants) = &self.definitions[index].shape else { + unreachable!() + }; + let mut fields = Vec::new(); + for (field, (_, kind)) in variants[variant].fields.iter().enumerate() { + fields.push((field, self.unknown(kind, depth + 1)?)); + } + self.variant(index, variant, Flow::fields(fields)) + } + + pub(super) fn variant( + &mut self, + index: usize, + variant: usize, + mut fields: Flow, + ) -> Result { + let Shape::Choice(variants) = &self.definitions[index].shape else { + unreachable!() + }; + if variants[variant].error { + fields.tokens.insert(self.token()?); + fields.guaranteed = true; + } + Ok(fields) + } + + pub(super) fn instantiate( + &mut self, + template: &Flow, + mapping: &mut BTreeMap>, + depth: usize, + ) -> Result { + self.budget(depth)?; + // 子摘要按调用处重新映射,不先深拷贝再逐项覆盖。 + let mut value = Flow { + guaranteed: template.guaranteed, + task: template.task.clone(), + ..Flow::default() + }; + for resource in &template.streams { + if !mapping.contains_key(resource) { + mapping.insert(*resource, BTreeSet::from([self.symbol()?])); + } + value.streams.extend(&mapping[resource]); + } + let mut tokens = BTreeSet::new(); + for token in &template.tokens { + if !mapping.contains_key(token) { + mapping.insert(*token, BTreeSet::from([self.token()?])); + } + tokens.extend(&mapping[token]); + } + self.pending.extend(&tokens); + value.tokens = tokens; + for (index, field) in &template.fields { + value + .fields + .insert(*index, self.instantiate(field, mapping, depth + 1)?); + } + for (index, variant) in &template.variants { + value + .variants + .insert(*index, self.instantiate(variant, mapping, depth + 1)?); + } + value.sequence = + match &template.sequence { + Some(Sequence::Known(values)) => Some(Sequence::Known( + values + .iter() + .map(|value| self.instantiate(value, mapping, depth + 1)) + .collect::>()?, + )), + Some(Sequence::Unknown(value)) => Some(Sequence::Unknown(Box::new( + self.instantiate(value, mapping, depth + 1)?, + ))), + None => None, + }; + self.refresh_streams(&mut value); + Ok(value) + } +} + +pub(super) fn bind(template: &Flow, actual: &Flow, mapping: &mut BTreeMap>) { + for resource in &template.streams { + mapping + .entry(*resource) + .or_default() + .extend(&actual.streams); + } + for token in &template.tokens { + mapping.entry(*token).or_default(); + } + let mut nested: BTreeSet<_> = template + .fields + .values() + .chain(template.variants.values()) + .flat_map(|value| value.tokens.iter().copied()) + .collect(); + if let Some(sequence) = &template.sequence { + nested.extend(sequence.element().tokens); + } + for token in template.tokens.difference(&nested) { + mapping.entry(*token).or_default().extend(&actual.tokens); + } + for (index, field) in &template.fields { + bind( + field, + actual.fields.get(index).unwrap_or(&Flow::default()), + mapping, + ); + } + for (index, variant) in &template.variants { + bind( + variant, + actual.variants.get(index).unwrap_or(&Flow::default()), + mapping, + ); + } + if let Some(sequence) = &template.sequence { + let actual = actual.sequence.as_ref(); + match (sequence, actual) { + (Sequence::Known(values), Some(Sequence::Known(actual))) + if values.len() == actual.len() => + { + for (value, actual) in values.iter().zip(actual) { + bind(value, actual, mapping); + } + } + _ => bind( + &sequence.element(), + &actual.map(Sequence::element).unwrap_or_default(), + mapping, + ), + } + } +} + +pub(super) fn assign(target: &mut Flow, fields: &[usize], value: Flow) { + let Some((field, remaining)) = fields.split_first() else { + *target = value; + return; + }; + assign(target.fields.entry(*field).or_default(), remaining, value); + *target = Flow::fields(std::mem::take(&mut target.fields)); +} diff --git a/rust/src/compiler/check/failure/mod.rs b/rust/src/compiler/check/failure/mod.rs new file mode 100644 index 0000000..cc387c9 --- /dev/null +++ b/rust/src/compiler/check/failure/mod.rs @@ -0,0 +1,256 @@ +//! 逐分句追踪业务失败的处理义务,并推导函数失败集合。 +//! 只有返回、传播或显式恢复才能解除义务;分析结果不改变运行时错误身份。 + +mod capture; +mod collection; +mod expression; +mod flow; +mod storage; +mod stream; +mod task; + +use crate::compiler::model::*; +use crate::syntax::{diagnostic::Diagnostic, source::Span}; +use std::collections::{BTreeMap, BTreeSet}; + +pub(super) fn check( + function: &mut Function, + definitions: &[Definition], + signatures: &[Signature], + database_error: Option, +) -> Result<(), Diagnostic> { + let tracked_types = flow::tracked_types(definitions); + let mut next = 0; + let mut failures = BTreeSet::new(); + let mut returns: Option = None; + let mut inputs: Vec> = vec![None; function.signature.inputs.len()]; + for clause in &function.clauses { + let mut state = State { + database_error, + definitions, + signatures, + tracked_types: &tracked_types, + values: vec![Flow::default(); clause.variables.len()], + pending: BTreeSet::new(), + closed_streams: BTreeSet::new(), + failures: BTreeSet::new(), + next, + work: 0, + span: clause.span, + }; + for (position, (_, kind)) in function.signature.inputs.iter().enumerate() { + let pattern = &clause.patterns[position]; + let value = match (&clause.variables[position].1, pattern) { + (_, Pattern::Null) => Flow::default(), + (Type::Named(index), _) + if matches!(definitions[*index].shape, Shape::Choice(_)) => + { + let Shape::Choice(variants) = &definitions[*index].shape else { + unreachable!() + }; + let mut possibilities = Vec::new(); + for variant in 0..variants.len() { + if super::clauses::accepts_variant( + pattern, + variant, + function + .clauses + .iter() + .map(|clause| clause.patterns[position].clone()), + ) { + possibilities + .push((variant, state.unknown_variant(*index, variant, 0)?)); + } + } + Flow::variants(possibilities) + } + (kind, _) => state.unknown(kind, 0)?, + }; + let input = if matches!(kind, Type::Optional(_)) + && matches!(pattern, Pattern::Present(_) | Pattern::Other) + { + Flow::variants([(1, value.clone())]) + } else if matches!(pattern, Pattern::Null) { + Flow::variants([(0, Flow::default())]) + } else { + value.clone() + }; + if let Some(previous) = &mut inputs[position] { + previous.merge(&input); + } else { + inputs[position] = Some(input); + } + state.values[position] = value; + } + for binding in &clause.bindings { + let Pattern::Variant(variant) = clause.patterns[binding.input].inner() else { + unreachable!() + }; + state.values[binding.variable] = state.values[binding.input] + .variants + .get(variant) + .and_then(|value| value.fields.get(&binding.field)) + .cloned() + .unwrap_or_default(); + } + for statement in &clause.body { + match statement { + Statement::Assign { + variable, + fields, + value, + } => { + let value = state.expression(value)?; + flow::assign(&mut state.values[*variable], fields, value); + } + Statement::Action(value) => { + state.expression(value)?; + } + Statement::Fail(value) => state.fail(value)?, + } + } + if !clause.terminates { + let mut output = match clause.outputs.as_slice() { + [] => Flow::default(), + [index] => state.values[*index].clone(), + outputs => Flow::fields( + outputs + .iter() + .enumerate() + .map(|(field, index)| (field, state.values[*index].clone())), + ), + }; + state.refresh_streams(&mut output); + if function.signature.recovery.is_none() + && !output.guaranteed + && !state.pending.is_subset(&output.tokens) + { + return Err(Diagnostic::new( + "C012", + "failure is not handled: return, propagate, or declare an explicit recovery policy", + clause.span, + )); + } + if let Some(previous) = &mut returns { + previous.merge(&output); + } else { + returns = Some(output); + } + } + next = state.next; + failures.extend(state.failures); + } + function.signature.failures = failures.into_iter().collect(); + function.signature.input_flows = inputs.into_iter().map(Option::unwrap).collect(); + function.signature.returns = returns; + Ok(()) +} + +struct State<'program> { + database_error: Option, + definitions: &'program [Definition], + signatures: &'program [Signature], + tracked_types: &'program [bool], + values: Vec, + /// 尚未由当前分句处理或转移出去的错误义务。 + pending: BTreeSet, + closed_streams: BTreeSet, + failures: BTreeSet, + next: usize, + work: usize, + span: Span, +} + +pub(super) fn prototype( + signature: &mut Signature, + definitions: &[Definition], + span: Span, +) -> Result<(), Diagnostic> { + let tracked_types = flow::tracked_types(definitions); + let mut state = State { + database_error: None, + definitions, + signatures: &[], + tracked_types: &tracked_types, + values: Vec::new(), + pending: BTreeSet::new(), + closed_streams: BTreeSet::new(), + failures: BTreeSet::new(), + next: 0, + work: 0, + span, + }; + signature.input_flows = signature + .inputs + .iter() + .map(|(_, kind)| state.unknown(kind, 0)) + .collect::>()?; + signature.returns = Some(match signature.outputs.as_slice() { + [] => Flow::default(), + [(_, kind)] => state.unknown(kind, 0)?, + fields => Flow::fields( + fields + .iter() + .enumerate() + .map(|(index, (_, kind))| Ok((index, state.unknown(kind, 0)?))) + .collect::, Diagnostic>>()?, + ), + }); + Ok(()) +} + +impl State<'_> { + fn symbol(&mut self) -> Result { + self.budget(0)?; + let symbol = self.next; + self.next += 1; + Ok(symbol) + } + + fn token(&mut self) -> Result { + let token = self.symbol()?; + self.pending.insert(token); + Ok(token) + } + + fn budget(&mut self, depth: usize) -> Result<(), Diagnostic> { + // 达到分析边界就报告失败,不能把未完成的证明当作合同通过。 + self.work += 1; + if self.work > 16_384 || depth > 128 { + return Err(Diagnostic::new( + "C015", + "failure analysis exceeds its structural limit", + self.span, + )); + } + Ok(()) + } + + fn arguments( + &mut self, + arguments: &[Expression], + signature: &Signature, + ) -> Result>, Diagnostic> { + self.call_inputs(arguments, signature, true) + } + + fn call_inputs( + &mut self, + arguments: &[Expression], + signature: &Signature, + consume: bool, + ) -> Result>, Diagnostic> { + let mut mapping = BTreeMap::new(); + let values = arguments + .iter() + .map(|argument| self.expression(argument)) + .collect::, _>>()?; + for (value, template) in values.iter().zip(&signature.input_flows) { + flow::bind(template, value, &mut mapping); + if consume { + self.pending.retain(|token| !value.tokens.contains(token)); + } + } + Ok(mapping) + } +} diff --git a/rust/src/compiler/check/failure/storage.rs b/rust/src/compiler/check/failure/storage.rs new file mode 100644 index 0000000..6e053cd --- /dev/null +++ b/rust/src/compiler/check/failure/storage.rs @@ -0,0 +1,82 @@ +//! 追踪 List/Map 存取对失败义务的影响。 +//! 读取、覆盖或删除部分元素不等于处理整个容器中的业务错误。 + +use super::State; +use crate::compiler::model::*; +use crate::syntax::diagnostic::Diagnostic; + +impl State<'_> { + pub(super) fn access( + &mut self, + operation: Access, + arguments: &[Expression], + kind: &Type, + ) -> Result { + let mut values = arguments + .iter() + .map(|argument| self.expression(argument)) + .collect::, _>>()?; + let container = std::mem::take(&mut values[0]); + match operation { + Access::Contains => Ok(Flow::default()), + Access::Entries => Ok(container), + Access::Append => { + let value = std::mem::take(&mut values[1]); + let guaranteed = value.guaranteed || container.guaranteed; + let sequence = match container.sequence.expect("List flow") { + Sequence::Known(mut items) => { + items.push(value); + Sequence::Known(items) + } + Sequence::Unknown(mut item) => { + item.merge(&value); + Sequence::Unknown(item) + } + }; + let mut output = Flow::sequence(sequence); + output.guaranteed = guaranteed; + Ok(output) + } + Access::First => { + if let Some(Sequence::Known(items)) = container.sequence { + if let Some(item) = items.into_iter().next() { + let Type::List(inner) = &arguments[0].value_type else { + unreachable!() + }; + return Ok(if matches!(inner.as_ref(), Type::Optional(_)) { + item + } else { + Flow::variants([(1, item)]) + }); + } + Ok(Flow::variants([(0, Flow::default())])) + } else { + self.unknown(kind, 0) + } + } + Access::Get => self.unknown(kind, 0), + Access::Remove => { + if matches!(&container.sequence, Some(Sequence::Known(items)) if items.is_empty()) { + Ok(container) + } else { + self.unknown(kind, 0) + } + } + Access::Put => { + let entry = Flow::fields([ + (0, std::mem::take(&mut values[1])), + (1, std::mem::take(&mut values[2])), + ]); + if matches!(&container.sequence, Some(Sequence::Known(items)) if items.is_empty()) { + return Ok(Flow::sequence(Sequence::Known(vec![entry]))); + } + let mut output = self.unknown(kind, 0)?; + let mut item = output.sequence.take().expect("Map flow").element(); + item.merge(&entry); + output = Flow::sequence(Sequence::Unknown(Box::new(item))); + output.guaranteed = entry.guaranteed; + Ok(output) + } + } + } +} diff --git a/rust/src/compiler/check/failure/stream.rs b/rust/src/compiler/check/failure/stream.rs new file mode 100644 index 0000000..2df85e6 --- /dev/null +++ b/rust/src/compiler/check/failure/stream.rs @@ -0,0 +1,57 @@ +//! 异步流别名只需在错误证明中跟踪:关闭的源不能再兑现处理原元素的承诺。 +//! 这些编号和集合不会进入生成程序;普通 List 复制仍保留独立可消费的值。 + +use super::State; +use crate::compiler::model::{Flow, Sequence}; +use crate::syntax::diagnostic::Diagnostic; +use std::collections::BTreeSet; + +impl State<'_> { + pub(super) fn lazy_stream(&mut self) -> Result { + Ok(Flow { + streams: [self.symbol()?].into(), + ..Flow::default() + }) + } + + pub(super) fn refresh_streams(&self, value: &mut Flow) -> bool { + if !value.streams.is_disjoint(&self.closed_streams) { + value.tokens.clear(); + value.guaranteed = false; + value.sequence = Some(Sequence::Known(Vec::new())); + return true; + } + let mut nested = BTreeSet::new(); + let mut changed = false; + for child in value.fields.values_mut().chain(value.variants.values_mut()) { + nested.extend(&child.tokens); + changed |= self.refresh_streams(child); + } + if let Some(sequence) = &mut value.sequence { + nested.extend(sequence.element().tokens); + match sequence { + Sequence::Known(items) => { + for item in items { + changed |= self.refresh_streams(item); + } + } + Sequence::Unknown(item) => changed |= self.refresh_streams(item), + } + } + if changed { + value.tokens.retain(|token| !nested.contains(token)); + value.tokens.extend( + value + .fields + .values() + .chain(value.variants.values()) + .flat_map(|child| child.tokens.iter().copied()), + ); + if let Some(sequence) = &value.sequence { + value.tokens.extend(sequence.element().tokens); + } + value.guaranteed = false; + } + changed + } +} diff --git a/rust/src/compiler/check/failure/task.rs b/rust/src/compiler/check/failure/task.rs new file mode 100644 index 0000000..91ed9ef --- /dev/null +++ b/rust/src/compiler/check/failure/task.rs @@ -0,0 +1,156 @@ +//! 提交只保存处理承诺;正常 wait 才完成输入处理并引入输出错误义务。 +//! stop 可能在工作开始前取消,不能把已存在的错误当成已经被子任务处理。 + +use super::State; +use crate::compiler::model::{failure::Deferred, *}; +use crate::syntax::diagnostic::Diagnostic; + +impl State<'_> { + pub(super) fn schedule( + &mut self, + operation: Schedule, + target: usize, + arguments: &[Expression], + ) -> Result { + let signature = &self.signatures[target]; + let mut mapping = self.call_inputs(arguments, signature, operation != Schedule::Run)?; + let before = self.pending.clone(); + let inputs = mapping.values().flatten().copied().collect(); + let output = if let Some(value) = &signature.returns { + self.instantiate(value, &mut mapping, 0)? + } else { + Flow::default() + }; + if operation == Schedule::Run { + self.pending = before; + Ok(Flow { + task: Some(Box::new(Deferred { + inputs, + output, + failures: signature.failures.iter().copied().collect(), + })), + ..Flow::default() + }) + } else { + self.failures.extend(&signature.failures); + Ok(output) + } + } + + pub(super) fn join(&mut self, task: &Expression, stop: bool) -> Result { + let value = self.expression(task)?; + let deferred = value.task.expect("checked affine handle"); + self.failures.extend(&deferred.failures); + if !stop { + self.pending + .retain(|token| !deferred.inputs.contains(token)); + } + self.pending.extend(&deferred.output.tokens); + Ok(deferred.output) + } + + pub(super) fn timeout( + &mut self, + task: &Expression, + millis: &Expression, + fallback: usize, + ) -> Result { + let flow = self.expression(task)?; + self.expression(millis)?; + let deferred = flow.task.expect("Task originates from run"); + let Type::Task(target) = task.value_type else { + unreachable!("checked Task"); + }; + self.failures.extend(&self.signatures[target].failures); + let signature = &self.signatures[fallback]; + self.failures.extend(&signature.failures); + let fallback = signature + .returns + .as_ref() + .map(|value| self.instantiate(value, &mut Default::default(), 0)) + .transpose()?; + if fallback.is_none() { + self.pending + .retain(|token| !deferred.inputs.contains(token)); + } + let mut output = self.signatures[target] + .returns + .as_ref() + .map(|_| deferred.output); + if let Some(fallback) = fallback { + if let Some(output) = &mut output { + output.merge(&fallback); + } else { + output = Some(fallback); + } + } + let output = output.unwrap_or_default(); + self.pending.extend(&output.tokens); + Ok(output) + } + + pub(super) fn race(&mut self, tasks: &[Expression]) -> Result { + let mut output: Option = None; + let mut handled: Option> = None; + for task in tasks { + let value = self.expression(task)?; + let deferred = value.task.expect("Task originates from run"); + let Type::Task(target) = task.value_type else { + unreachable!("checked Task"); + }; + self.failures.extend(&self.signatures[target].failures); + if self.signatures[target].returns.is_some() { + // 只有每个可能胜出的任务都处理的输入,才能在正常返回时解除义务。 + if let Some(handled) = &mut handled { + handled.retain(|token| deferred.inputs.contains(token)); + } else { + handled = Some(deferred.inputs); + } + if let Some(output) = &mut output { + output.merge(&deferred.output); + } else { + output = Some(deferred.output); + } + } + } + if let Some(handled) = handled { + self.pending.retain(|token| !handled.contains(token)); + } + let output = output.unwrap_or_default(); + self.pending.extend(&output.tokens); + Ok(output) + } + + pub(super) fn group(&mut self, limit: &Expression) -> Result { + self.expression(limit)?; + Ok(Flow { + task: Some(Box::new(Deferred { + inputs: Default::default(), + output: Flow::default(), + failures: Default::default(), + })), + ..Flow::default() + }) + } + + pub(super) fn group_run( + &mut self, + group: &Expression, + target: usize, + arguments: &[Expression], + ) -> Result { + let Node::Variable(index) = group.kind else { + unreachable!("checked local Group"); + }; + let pending = self + .schedule(Schedule::Run, target, arguments)? + .task + .expect("checked run"); + let group = self.values[index].task.as_mut().expect("checked Group"); + group.inputs.extend(pending.inputs); + group.failures.extend(pending.failures); + // 提交时可能观察到已有子任务的失败;正常 wait 才兑现所有输入处理承诺。 + self.failures.extend(&group.failures); + Ok(Flow::default()) + } +} diff --git a/rust/src/compiler/check/handler/adapter.rs b/rust/src/compiler/check/handler/adapter.rs new file mode 100644 index 0000000..988cce7 --- /dev/null +++ b/rust/src/compiler/check/handler/adapter.rs @@ -0,0 +1,171 @@ +//! 把选择器与只读配置访问加入已有特化和依赖图,不建立第二套调用分析。 + +use super::super::{application::port::Ports, failure}; +use super::contract::Template; +use crate::compiler::model::{ + adapter::{Adapters, Generated}, + *, +}; +use crate::syntax::{diagnostic::Diagnostic, source::Span}; +use std::collections::HashMap; + +type Targets = HashMap<(usize, String, usize), usize>; +type Settings = HashMap<(usize, String), usize>; + +pub(super) fn templates( + templates: &mut Vec