- CLI
【免费下载链接】himalaya
CLI to manage emails
导读
本文基于 Himalaya 仓库中的变更提案 cairn/changes/json-keys-are-camel-case/proposal.md 及其配套的 delta.md 与 tasks.md,完整解读 Himalaya 计划在 3.0 版本中把--json输出对象键从 kebab-case / snake_case 混用统一为 camelCase 的破坏性变更:包括动机、影响面、明确不动的地方、以及 alias 陷阱。读完本文,你将理解这次变更为什么必须放在大版本、哪些输出类型会被改写、哪些拼写会被刻意保留,以及作为脚本或下游 JSON Schema 消费者需要如何准备迁移。
一、为什么--json键必须改为 camelCase
1. Pimalaya 家族已经统一约定
Himalaya 是 Pimalaya 家族的四个产品之一。该家族早已把--json的对象键标准化为 camelCase,但 Himalaya 由于历史原因无法立即跟进——这个提案就是消除这项差异的正式计划。
2. 上游线上格式本来就讲 camelCase
提案中的第一个理由是:camelCase 正是这些命令所包装的线上格式已经使用的语言。
- JMAP 对象按 RFC 8620 本身就是 camelCase;
- Microsoft Graph 的响应是 camelCase;
- Himalaya 对接的每一个 Google API(Gmail REST)也是 camelCase。
也就是说,一个 Gmail 或 Graph 响应在流入 Himalaya 的输出类型时,会无缘无故地跨过一次大小写边界——而这只是因为 serde 的默认行为(Rust 字段名messages_total默认序列化为 snake_casemessages_total),没有其他理由。
3. 脚本消费方的点访问痛点
第二个理由是--json存在的意义——被脚本消费。而无论是 jq 还是 JavaScript,都无法对包含连字符的键使用点访问:
- jq 中
.messages-total必须写成."messages-total"; - JavaScript 中必须写成
obj["messages-total"]; - 更危险的是,脚本一旦忘记加引号,失败是静默的(返回
null而非报错),而不是响亮地暴露问题。
而messagesTotal在两种语言中都可以直接点访问、语义一致。camelCase 因此是脚本消费场景下唯一让键"可点访问"的命名方案。
二、现状:一个 CLI 里并存两套命名约定
提案明确指出,Himalaya 今天输出的并不是一种约定,而是两种。
1. 26 个类型带着#[serde(rename_all = "kebab-case")]
以下类型(按提案原文列举,均已在本仓库源码中核实)显式声明了 kebab-case 序列化:
| 分类 | 文件 |
|---|---|
| Gmail / Graph profile | src/gmail/profile/get.rs、src/msgraph/profile/get.rs |
| Gmail history | src/gmail/history/list.rs(含GmailHistoryListOutput、GmailHistoryRecordOutput、GmailHistoryMessageOutput、GmailHistoryLabelOutput) |
| 三个 Gmail get 渲染器 | src/gmail/messages/get.rs、src/gmail/drafts/get.rs、src/gmail/threads/get.rs |
| 五个 Gmail settings 单例 | src/gmail/settings/pop/get.rs、src/gmail/settings/autoforwarding/get.rs、src/gmail/settings/language/get.rs、src/gmail/settings/vacation/get.rs、src/gmail/settings/imap/get.rs |
| ManageSieve | src/sieve/get.rs、src/sieve/list.rs、src/sieve/capability.rs |
| IMAP | src/imap/id.rs(ServerIdTable) |
| 共享消息删除 | src/shared/message/delete.rs(DeleteReport与DeleteAction) |
| 共享邮件领域类型 | src/email/envelope.rs(Envelope)、src/email/mailbox.rs(Mailbox)、src/email/flag.rs、src/email/address.rs |
2. 其余类型不带注解,走 serde 默认的 snake_case
未带rename_all的类型直接按 Rust 字段名输出 snake_case。于是同一个 CLI 中出现了典型的"精神分裂"输出:
- src/shared/output.rs 中
Paginated<T>的分页字段next_page输出为next_page(snake_case); - 而
gmail profile get的messages_total字段因为 kebab-case 注解输出为messages-total。
两者并排出现在同一个 Himalaya CLI 的--json输出里,对消费方毫无规律可循。
3. 版本契约:为什么必须等 3.0
Himalaya 当前版本为 2.1.0(见 Cargo.toml)。--json的键是已发布给用户的公开契约,改变它们是破坏性变更,因此必须留到 3.0 大版本发布。这也是整个提案的时间线前提。
三、3.0 到底改什么
1. 只动输出类型,不碰其他
变更范围被严格限定在输出类型:即交给printer.out的类型,以及注册在 src/json_schema.rs 中的类型。具体做法是:
- 每个输出类型加上
#[serde(rename_all = "camelCase")]; - 移除这些类型上现有的 kebab-case 重命名。
2. 参考类型:GmailProfileOutput
提案指定的参考类型是 src/gmail/profile/get.rs 中的GmailProfileOutput。它的载荷将发生如下变化:
{email, messages-total, threads-total, history-id} ↓ {email, messagesTotal, threadsTotal, historyId}注意该类型当前的文档注释写的是旧拼写({email, messages-total, threads-total, history-id}),在 3.0 中,命名旧拼写的文档注释也要随之更新——这是任务清单里明确列出的一项(Update the doc comments naming a key spelling, starting with GmailProfileOutput)。
3. 另一半清理:统一*Output后缀
注册在src/json_schema.rs中的类型共有61 个,其中:
- 19 个已经带
*Output后缀; - 42 个没有(如
Envelopes、Mailboxes、MessagesTable、DeleteReport、SieveScripts等)。
把它们统一重命名为*Output是同一清理的另一半,必须放在同一个大版本中完成——因为两件事改动的是同一批声明。参考命名同样是GmailProfileOutput。
4. 源码中的注册表实况
src/json_schema.rs 的schemas()函数以himalaya-<命令路径>为键注册每个命令的--jsonpayload 类型:
- 共享 API:
himalaya-mailbox-list→Mailboxes、himalaya-envelope-list/himalaya-envelope-search→Envelopes、himalaya-message-delete→DeleteReport等; - 协议专属:
himalaya-imap-id→ServerIdTable、himalaya-sieve-get→SieveScriptOutput、himalaya-gmail-profile-get→GmailProfileOutput、himalaya-msgraph-messages-get→MsgraphMessageGetOutput等; - 分页列表使用
Paginated<T>包装,因此next_page字段也会被 schema 描述出来。
这与 cairn/spec/commands.md 中"--json切换每个命令到 JSON,数据通过 printer 输出到 stdout"的规范相互印证。
四、三处刻意不动的地方
提案特别强调,以下三件事故意保留原样,不是遗漏,而是边界。
1. 配置类型保持 kebab-case
src/config.rs 中的Config及大量子类型(如 EnvelopeConfig、MailboxConfig)使用#[serde(rename_all = "kebab-case")](多数还带deny_unknown_fields)从TOML反序列化。
原因很直接:TOML 中连字符键是家族约定,且没有任何 jq 表达式会接触到配置。这条规则只约束 printer 发出的内容,从不约束 loader 读取的内容——"输出端的事,与输入端无关"。
2. Provider 透传字段保留自己的拼写
携带线上原始名称的字段会保留其显式的#[serde(rename = "...")]:
@odata.nextLink是 Microsoft Graph 的原名;nextPageToken是 Gmail 的原名。
这两者都不是能从 Rust 字段名推导出来的拼写。对持有这类字段的类型应用rename_all不会触碰它们(rename优先于rename_all),这恰好是正确的行为——并且提案明确警告:未来也不应有人去"修正"它们。这些 rename 位于 io-gmail / io-msgraph 依赖 crate 的资源类型中,Himalaya 仓库内没有对应字面量。
3. io-gmail / io-msgraph 之上的透明 newtype 不归 Himalaya 改
MsgraphMessageGetOutput(src/msgraph/messages/get.rs)以及 Gmail settings 的 newtype,是#[serde(transparent)]的透明包装,序列化的是 io- crate 声明的资源——而这些资源本身就已经是 provider 的 camelCase。它们不需要任何属性,也不得被强制套上属性。
这与 cairn/spec/commands.md 中"get输出通过透明 newtype 原样发出list已序列化的后端资源"的规范一致:一行list读到的东西,在get里形状必须完全相同。
4. 但 Himalaya 自己的分页字段不是透传
Paginated::next_page(src/shared/output.rs)是Himalaya 自己发明的字段,用于包装 provider 的游标,而不是透传。因此它和任何其他输出字段一样,在 3.0 变为nextPage。
next_page → nextPage (Himalaya 自有分页字段,随大部队改) nextPageToken → nextPageToken (Gmail 透传字段,不动)五、别名陷阱:为什么serde(alias)救不了过渡期
一个容易踩的坑:#[serde(alias = "...")]是仅用于反序列化的属性。它教Deserialize接受第二种拼写,但对Serialize完全没有效果——因此它无法让一个输出类型在过渡期同时发出messages-total和messagesTotal。而输出类型只序列化、从不反序列化,所以给输出类型加 alias 纯粹是装饰。
如果将来想软化这次破坏,提案列出了两条替代路径:
- 在 printer 里孪生键:同时序列化两种拼写。代价是 payload 体积翻倍,且发布的 schema 必须为同一个值描述两个名字;
- 接受大版本的破坏:把键重命名当作 major release 中允许可见的变化。
最终决策是接受破坏——大版本正是键重命名可以光明正大出现的地方。这也意味着从今天到 3.0,messages-total等旧键不会获得任何双拼写兼容期。
六、落地任务清单与验收标准
配套的 tasks.md 给出了完整的执行清单(当前处于Held until the 3.0 cycle opens状态):
- 给 src/json_schema.rs 注册的每一个类型加上
#[serde(rename_all = "camelCase")],移除它替换掉的 26 处 kebab-case 重命名; - 保留 provider 透传 rename(
@odata.nextLink、nextPageToken)以及 io-gmail / io-msgraph 资源之上的透明 newtype 不动; - 保留 src/config.rs 的 kebab-case——TOML 键不是
--json键; - 把 42 个尚未带
*Output后缀的注册类型重命名,参考GmailProfileOutput; - 更新命名了键拼写的文档注释,从
GmailProfileOutput开始; - 重新生成 JSON Schema,并检查没有任何键还保留连字符;
- 在 CHANGELOG 的 3.0 段落、
Changed分类下,按破坏性变更记录。
对应的验收需求写入 delta.md:每个交给 printer 的输出类型 SHALL 以 camelCase 序列化其键;携带 provider 拼写的字段 SHALL 保留显式#[serde(rename)];配置类型 SHALL 保持 kebab-case。该 delta 将折叠进 cairn/spec/commands.md,前提是 3.0 真正落地了重命名。
七、影响面与迁移提示(Out of scope)
提案最后划清了本次变更的边界:
- 没有 CHANGELOG 条目:在 3.0 落地重命名之前,对用户没有任何可见变化;
- JSON Schema 文件随键改变:
json-schema命令(src/cli.rs)写出的 schema 文件会同步更新,任何钉死在旧 schema 上的下游消费者需要在同一时间重新生成。
对脚本开发者而言,迁移要点可以概括为三句话:
- 3.0 之前继续使用
."messages-total"、obj["messages-total"]这类带引号访问; - 3.0 发布后,所有 Himalaya 自有的输出键都变成可点访问的 camelCase(
messagesTotal、nextPage等); - 永远不要把
@odata.nextLink、nextPageToken这类 provider 透传键改成 camelCase——它们不是 Himalaya 的命名,改名反而会破坏与上游 API 的对应关系。
- CLI
【免费下载链接】himalaya
CLI to manage emails
相关推荐
OpCore Simplify 新手指南:如何 10 分钟生成 OpenCore EFI 的完整教程(附避坑清单)
OpCore Simplify 新手指南:如何 10 分钟生成 OpenCore EFI 的完整教程(附避坑清单) 在黑苹果的圈子里,最劝退新手的从来不是下载系
CLIRobot Framework 3.0 Alpha 1 版本全解读:Python 3 支持、统一 robot 启动脚本与破坏性变更全景
Robot Framework 3.0 Alpha 1 版本全解读:Python 3 支持、统一 robot 启动脚本与破坏性变更全景 导读 Robot Fra
测试RPA接口测试`vault` CLI 的 `--json` 输出规范全解析:面向脚本、编辑器与 CI 的稳定 Schema 契约
vault CLI 的 json 输出规范全解析:面向脚本、编辑器与 CI 的稳定 Schema 契约 导读 vault 是 StaffML 题库工程(vaul
教育教程人工智能机器学习
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考