☰
Himalaya 3.0 `--json` 输出键全面切换 camelCase:一次针对脚本消费方的破坏性契约变更解读
2026/10/4 9:50:25 网站建设 项目流程
  • CLI

【免费下载链接】himalaya

CLI to manage emails

项目地址:https://gitcode.com/gh_mirrors/hi/himalaya
点击查看免费下载

导读

本文基于 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 profilesrc/gmail/profile/get.rs、src/msgraph/profile/get.rs
Gmail historysrc/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
ManageSievesrc/sieve/get.rs、src/sieve/list.rs、src/sieve/capability.rs
IMAPsrc/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 纯粹是装饰。

如果将来想软化这次破坏,提案列出了两条替代路径:

  1. 在 printer 里孪生键:同时序列化两种拼写。代价是 payload 体积翻倍,且发布的 schema 必须为同一个值描述两个名字;
  2. 接受大版本的破坏:把键重命名当作 major release 中允许可见的变化。

最终决策是接受破坏——大版本正是键重命名可以光明正大出现的地方。这也意味着从今天到 3.0,messages-total等旧键不会获得任何双拼写兼容期。


六、落地任务清单与验收标准

配套的 tasks.md 给出了完整的执行清单(当前处于Held until the 3.0 cycle opens状态):

  1. 给 src/json_schema.rs 注册的每一个类型加上#[serde(rename_all = "camelCase")],移除它替换掉的 26 处 kebab-case 重命名;
  2. 保留 provider 透传 rename(@odata.nextLink、nextPageToken)以及 io-gmail / io-msgraph 资源之上的透明 newtype 不动;
  3. 保留 src/config.rs 的 kebab-case——TOML 键不是--json键;
  4. 把 42 个尚未带*Output后缀的注册类型重命名,参考GmailProfileOutput;
  5. 更新命名了键拼写的文档注释,从GmailProfileOutput开始;
  6. 重新生成 JSON Schema,并检查没有任何键还保留连字符;
  7. 在 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

项目地址:https://gitcode.com/gh_mirrors/hi/himalaya
点击查看免费下载

相关推荐

上一篇:解决MudBlazor中Dialog嵌套Menu失效问题:Provider加载顺序深度解析
下一篇:解决FanControl.CorsairLink插件风扇控制卡重复问题的3个实用方案

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询