Skip to content

Commit b431f9f

Browse files
committed
refactor: add json schema and improve JSON output
1 parent 5234bdf commit b431f9f

31 files changed

Lines changed: 633 additions & 52 deletions

‎CHANGELOG.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66

77
## [Unreleased]
88

9+
### Added
10+
11+
- Added `imap.alpn`, `jmap.alpn`, `caldav.alpn` and `carddav.alpn`, the ALPN identifiers offered during the TLS handshake.
12+
13+
Unset takes the protocol default the backend already used, `["imap"]` over IMAP and `["http/1.1"]` over JMAP and WebDAV, so nothing moves for a configuration that does not name it. An empty list skips ALPN negotiation, for a server whose TLS terminator rejects the handshake carrying one, and a non-empty list replaces the default. Only rustls reads it, native-tls having no ALPN.
14+
15+
- Added `carillon json-schema`, generating the JSON Schema of a command's `--json` output.
16+
17+
It writes one schema to stdout, or one file per command into `--dir`. `check` and `configure` are the two commands that print data.
18+
919
### Changed
1020

21+
- Expanded the leading tilde and the shell variables in `imap.tls.cert`, `jmap.tls.cert`, `caldav.tls.cert` and `carddav.tls.cert`.
22+
23+
`cert = "~/certs/example.pem"` used to be read as the literal relative path `./~/certs/example.pem`, so the certificate was never found. It is now expanded when the file is read, as every other path in the configuration already was.
24+
25+
1126
- **BREAKING**: renamed `completions` and `manuals` to `completion` and `manual`, the plural staying as a hidden alias.
1227
- Spawned a credential command once per checked account, rather than once per backend naming it.
1328

‎Cargo.lock‎

Lines changed: 58 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,10 @@ notify-rust = { version = "4", default-features = false, features = ["d"] }
5252
pimalaya-cli = { version = "0.2", default-features = false, features = ["terminal", "prompt", "spinner", "wizard"] }
5353
pimalaya-config = { version = "0.2", default-features = false, features = ["toml", "secret"] }
5454
pimalaya-stream = { version = "0.3", default-features = false, features = ["std"] }
55+
schemars = "1"
5556
secrecy = { version = "0.10", features = ["serde"] }
5657
serde = { version = "1", features = ["derive"] }
58+
serde_json = "1"
5759
shellexpand = "3.1"
5860
subst = "0.3"
5961
url = { version = "2.2", features = ["serde"] }

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ Force a backend on an account declaring several:
131131
carillon -b jmap watch
132132
```
133133

134-
Every command and every flag is documented behind `--help`. Man pages and shell completions are generated by `carillon manual --dir <DIR>` and `carillon completion --dir <DIR>`.
134+
Every command and every flag is documented behind `--help`. Man pages, shell completions and the JSON Schemas of the `--json` output are generated by `carillon manual --dir <DIR>`, `carillon completion --dir <DIR>` and `carillon json-schema --dir <DIR>`.
135135

136136
Logs go to stderr, so they can be redirected to a file while the command output stays on stdout:
137137

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
cairn: delta
3+
change: alpn-under-each-backend
4+
---
5+
6+
## ADDED Requirements
7+
8+
### Requirement: The TLS handshake is configured under its backend
9+
Every backend speaking TLS SHALL carry a `tls` table and an `alpn` key of its own, and the runtime TLS handle SHALL be built through one conversion taking that list, so a connection cannot be opened without saying what it negotiates. `alpn` SHALL be a list of ALPN identifiers: unset SHALL take the default the backend's client crate owns (`["imap"]` over IMAP, `["http/1.1"]` over JMAP and WebDAV), an empty list SHALL skip ALPN negotiation, and a non-empty list SHALL replace the default. Only rustls SHALL read it, native-tls having no ALPN. Every path in a `tls` table SHALL be expanded when the file is read, so a leading tilde or a shell variable names the same thing there as everywhere else.
10+
11+
#### Scenario: A server that refuses the handshake carrying an ALPN
12+
- **GIVEN** a CalDAV server whose TLS terminator rejects a `ClientHello` offering `http/1.1`
13+
- **WHEN** the account sets `caldav.alpn = []`
14+
- **THEN** the connection is opened with no ALPN extension at all
15+
16+
#### Scenario: A configuration naming no ALPN
17+
- **GIVEN** an account carrying no `alpn` key
18+
- **WHEN** it is watched
19+
- **THEN** the backend offers the default its client crate owns, unchanged from before the key existed, and nothing is written back into a generated document
20+
21+
#### Scenario: A certificate under the home directory
22+
- **GIVEN** `imap.tls.cert = "~/certs/example.pem"`
23+
- **WHEN** the configuration is read
24+
- **THEN** the path is expanded against the home directory rather than read as a relative `./~/certs/example.pem`
25+
26+
## MODIFIED Requirements
27+
28+
## REMOVED Requirements
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
cairn: change
3+
id: alpn-under-each-backend
4+
status: landed
5+
created: 2026-08-29
6+
---
7+
8+
# Give every TLS backend its own ALPN key
9+
10+
## Why
11+
12+
Three call sites built a `Tls` out of a `TlsConfig` and then patched the ALPN list back into it, because `From<TlsConfig> for Tls` hardcoded an empty one. Nothing forced a fourth call site to do the same, so the next connection added would have negotiated no ALPN and nobody would have noticed until a server that needs one refused it.
13+
14+
The three lists were also literals in the source, which is the wrong place: `vec!["imap"]` and `vec!["http/1.1"]` are exactly what a configuration key should be able to override. A server whose TLS terminator rejects a handshake carrying `http/1.1` has no way to say so, and the rest of the family (himalaya, himalaya-tui, cardamum, neverest) already exposes that key.
15+
16+
While reading the same table, `tls.cert` turned out to be the one path in the configuration that is not expanded when the file is read, so `cert = "~/certs/example.pem"` names a relative `./~/certs` directory that does not exist.
17+
18+
## What
19+
20+
- `From<TlsConfig> for Tls` becomes `TlsConfig::into_tls(self, alpn)`, the signature himalaya, himalaya-tui, cardamum and neverest already carry. A call site now has to say what it negotiates.
21+
- `imap`, `jmap`, `caldav` and `carddav` each gain an `alpn: Option<Vec<String>>` key. Unset takes the default its client crate owns, `[]` skips ALPN, and a non-empty list replaces the default. Maildir gains none: it opens no socket.
22+
- `tls.cert` is expanded at deserialize, through a private `opt_shell_expanded_path` carrying a TODO for the shared helper pimalaya-config has not shipped yet.
23+
- The DAV wizard's test connection opens with the same profile a watch does, rather than with its own literal.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
cairn: tasks
3+
change: alpn-under-each-backend
4+
---
5+
6+
- [x] Replace `From<TlsConfig> for Tls` with `TlsConfig::into_tls`
7+
- [x] Add `alpn` to the IMAP, JMAP, CalDAV and CardDAV blocks
8+
- [x] Feed it through `into_tls` at every call site, the wizard included
9+
- [x] Expand `tls.cert` at deserialize
10+
- [x] Document the keys in the sample and cover them with round-trip tests
11+
- [x] Fold the delta into the spec and log the change
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
cairn: delta
3+
change: camel-case-json-output
4+
---
5+
6+
## ADDED Requirements
7+
8+
## MODIFIED Requirements
9+
10+
### Requirement: Every printed output has a published schema
11+
Every command handing data to the printer SHALL return a named `*Output` type deriving `Display`, `Serialize` and `JsonSchema`, and `carillon json-schema` SHALL publish the schema of each, keyed by the command path joined with hyphens and prefixed `carillon-`. Every type reaching the printer SHALL spell its keys in camelCase, declared as `rename_all` on the type, which is the convention of the `--json` output alone and not of the TOML configuration, whose keys stay kebab-case. A command that writes files rather than data SHALL stay out of the registry, and `watch` SHALL print nothing, reporting through its hooks alone.
12+
13+
#### Scenario: A consumer reading the check payload
14+
- **GIVEN** `carillon json-schema carillon-check`
15+
- **WHEN** it runs
16+
- **THEN** the JSON Schema of the `--json` payload of `carillon check` is printed on stdout
17+
18+
#### Scenario: Every schema at once
19+
- **GIVEN** `carillon json-schema --dir <DIR>`
20+
- **WHEN** it runs
21+
- **THEN** one file per command is written there, the directory being created if it is not already
22+
23+
## REMOVED Requirements
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
cairn: change
3+
id: camel-case-json-output
4+
status: landed
5+
created: 2026-08-29
6+
---
7+
8+
# Print JSON keys in camelCase
9+
10+
## Why
11+
12+
The `--json` output of every Pimalaya CLI is moving to camelCase keys, matching the wire formats these tools wrap (JMAP, the Google and Microsoft APIs, JSCalendar and JSContact) and sparing a consumer the `."key-name"` quoting jq needs for a hyphenated key. carillon is pre-1.0 and its output types carry no `rename_all` at all, so they emit serde's default snake_case and the convention is unstated in the source.
13+
14+
## What
15+
16+
- `CheckOutput`, `BackendCheck` and `ConfigureOutput`, which is every type reaching `printer.out`, carry `#[serde(rename_all = "camelCase")]`, so the convention is declared where the payload is defined rather than inferred from field names that happen to be single words.
17+
- No compatibility alias: carillon switches now rather than carrying two spellings.
18+
- The TOML configuration is untouched. The `rename_all = "kebab-case"` throughout `src/config.rs` is the config vocabulary, which is a different surface with a different convention.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
cairn: tasks
3+
change: camel-case-json-output
4+
---
5+
6+
- [x] Add `rename_all = "camelCase"` to `CheckOutput`, `BackendCheck` and `ConfigureOutput`
7+
- [x] Leave the `src/config.rs` kebab-case attributes alone
8+
- [x] Regenerate the schemas and read the `carillon-check` and `carillon-configure` properties
9+
- [x] Fold the delta into the spec and log the change

0 commit comments

Comments
 (0)