|
| 1 | +# ADR-0042: The Emacs org→atom Mapping — Struct Seam, `dom-print` Serialization, Emacs 29.1 Floor |
| 2 | + |
| 3 | +- Status: accepted |
| 4 | +- Deciders: mdorman, Claude |
| 5 | +- Date: 2026-07-02 |
| 6 | + |
| 7 | +## Context and Problem Statement |
| 8 | + |
| 9 | +C2 (#160) fills the `jaunder--org->atom` seam: turning an authored org buffer |
| 10 | +into the AtomPub `<entry>` the server parses on create/update. It is pure and |
| 11 | +serverless-testable. Four cross-cutting shape decisions govern how the mapping |
| 12 | +is structured and how it composes with the later units (C3 media, C4 publish |
| 13 | +flow, D reverse mapping) that build on it. This ADR records them; the mechanical |
| 14 | +field mapping and timezone details live in the issue spec. |
| 15 | + |
| 16 | +## Decision Drivers |
| 17 | + |
| 18 | +- The forward mapping should be trivially unit-testable as pure data, and should |
| 19 | + compose cleanly with C3 (media substitution in the body) and C4 (send). |
| 20 | +- Mistakes in a hand-built wire document (a dropped field, a mislabeled type) |
| 21 | + should fail loud/early, not silently produce wrong XML. |
| 22 | +- Keep the client dependency-light (the transport already added only `plz`). |
| 23 | +- Pin a realistic minimum Emacs so the above can rely on built-ins. |
| 24 | + |
| 25 | +## Decision Outcome |
| 26 | + |
| 27 | +### D1 — Two-layer seam: abstract struct, then a separate serializer |
| 28 | + |
| 29 | +`jaunder--org->atom` returns an **abstract `jaunder-entry`**, not wire XML; a |
| 30 | +separate `jaunder--atom-entry->xml` renders the wire `<entry>`. This keeps the |
| 31 | +forward mapping pure-data (`(should (equal (jaunder-entry-title e) …))`), lets |
| 32 | +C3 substitute media in the body slot with one mutation before serialization, and |
| 33 | +localizes all **wire knowledge** (namespaces, media-type strings, |
| 34 | +`app:control/app:draft` nesting, element order) in one serializer tested once. |
| 35 | +It is symmetric with the reverse primitive C4/D will add (`atom-entry-fields`, |
| 36 | +XML→fields). |
| 37 | + |
| 38 | +### D2 — `cl-defstruct jaunder-entry` for the representation (not a plist) |
| 39 | + |
| 40 | +The intermediate is a `cl-defstruct`, not a plist. A misnamed field is then |
| 41 | +caught early: the constructor rejects an unknown keyword **at byte-compile |
| 42 | +time** (the struct's compiler macro) and at runtime, and a misnamed accessor is |
| 43 | +a loud `void-function` plus an undefined-function warning under file |
| 44 | +byte-compilation (→ a build error once #108's warnings-as-errors elisp gate |
| 45 | +lands). A plist gives none of this — `(plist-get f :titel)` is a silent `nil`, |
| 46 | +so a typo drops a field from every post with no signal. `cl-defstruct` does |
| 47 | +**not** add value-type safety (`:type` is not runtime-enforced); it raises the |
| 48 | +floor on field-name mistakes only. Cost is negligible — `cl-lib` is built in and |
| 49 | +`plz-response` (already consumed by C1) is itself a `cl-defstruct`. |
| 50 | + |
| 51 | +### D3 — Emit XML via built-in `dom.el` / `dom-print`, not hand-rolled strings |
| 52 | + |
| 53 | +The serializer builds a `dom` node and calls `dom-print`. `dom-print` is built |
| 54 | +into Emacs (since 28), so this is **not** a new dependency — the |
| 55 | +hand-rolled-vs-library choice had no dependency asymmetry, and the library |
| 56 | +removes a hand-written escaper. Verified on the pinned Emacs (30.2): `dom-print` |
| 57 | +escapes both text and attribute values (`&`,`<`,`>`,`"`), emits prefixed |
| 58 | +elements (`app:control`/`app:draft`) and root `xmlns:*` attributes, and |
| 59 | +self-closes empty elements. The byte-determinism argument that might favor |
| 60 | +hand-rolling was **withdrawn**: it applies to Unit D's org-buffer round-trip, |
| 61 | +not to the entry we send (the server re-parses and re-serializes; the client |
| 62 | +stores the response ETag). The `dom` node is a private detail of the serializer; |
| 63 | +the seam remains the struct (D1). |
| 64 | + |
| 65 | +### D4 — Emacs floor raised 27.1 → 29.1 |
| 66 | + |
| 67 | +`Package-Requires` becomes `(emacs "29.1")`. 29.1 (2023) is the floor on current |
| 68 | +Ubuntu LTS (ships 29.3); it comfortably covers `dom-print` (since 28) and the |
| 69 | +`encode-time` zone handling the mapping relies on. No user is expected below it. |
| 70 | + |
| 71 | +Rejected alternatives: |
| 72 | + |
| 73 | +- **Return finished XML from `jaunder--org->atom`** (collapse D1). C3 would then |
| 74 | + have to reach into an XML string to substitute media links, and the tests |
| 75 | + would conflate mapping with serialization. |
| 76 | +- **plist instead of `cl-defstruct`** (D2). Lighter and consistent with the HTTP |
| 77 | + layer's `(:status …)` plist, but a field typo is a silent wrong post — the |
| 78 | + correctness argument outranks the consistency one for a fixed-shape record. |
| 79 | +- **Hand-rolled XML string** (D3). No dependency advantage over `dom.el` (both |
| 80 | + built-in), and it reintroduces a bespoke escaper. |
| 81 | + |
| 82 | +## Consequences |
| 83 | + |
| 84 | +- Good: the forward mapping is pure data — trivial, fast ERT; C3/C4 compose on |
| 85 | + the struct without touching wire concerns. |
| 86 | +- Good: field-name mistakes fail at byte-compile / first call, not silently. |
| 87 | +- Good: no new dependency for serialization; the escaper is Emacs', not ours. |
| 88 | +- Neutral: `cl-lib`/`dom`/`url-util` are now required (all built-in). |
| 89 | +- Bad/So-what: the 29.1 floor drops pre-2023 Emacs, an acceptable line for a new |
| 90 | + client. |
| 91 | + |
| 92 | +## Verification |
| 93 | + |
| 94 | +- Pure ERT suite green on host **and** in the hermetic `ert-check` (the latter |
| 95 | + needs a zone database — `TZDIR` provided to the derivation for `encode-time`). |
| 96 | +- `jaunder.el` byte-compiles clean with `byte-compile-error-on-warn` (exercising |
| 97 | + D2's accessor/constructor checks). |
| 98 | +- Serializer output verified well-formed via `libxml-parse-xml-region` and |
| 99 | + structurally against the elements `common::atompub::entry_from_xml` parses; a |
| 100 | + live round-trip against the server is C4's territory. |
0 commit comments