oxedyne/fe2o3/fe2o3_jdat/dat_map/TODO.md
7.0 KiB, 1 run
created by r1870400018:9510, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | # TODO for fe2o3_jdat/dat_map |
| 2 | |
| 3 | Improvements to the `FromDatMap` / `ToDatMap` procedural macros that would |
| 4 | lift the derive from "just sufficient" toward a first-class |
| 5 | Dat/JSON-through-Dat (de)serialisation facility for Hematite. Captured after |
| 6 | writing `fe2o3_net::acme::rfc8555`, where every item below was felt as |
| 7 | friction during development. |
| 8 | |
| 9 | The derive is finicky and should be improved carefully in a dedicated |
| 10 | session, with `fe2o3_net/src/acme/rfc8555.rs` (a non-trivial real-world |
| 11 | user) used as a stress test for each change. None of the items below block |
| 12 | the ACME migration currently under way. |
| 13 | |
| 14 | ## Ranked from biggest usability win down |
| 15 | |
| 16 | ### 1. Nested-struct field support (biggest payoff) |
| 17 | |
| 18 | Today a field typed `Vec<Challenge>` or `Option<Order>` or |
| 19 | `BTreeMap<String, Foo>` panics at derive expansion with |
| 20 | `"from_datmap: Cannot find an equivalent Dat for type ..."`. The supported |
| 21 | type list in `fe2o3_jdat/dat_map/src/lib.rs` is a fixed whitelist of |
| 22 | stringified type names: primitives, `String`, `Dat`, `Box<Dat>`, |
| 23 | `Vec<u8>`, `Vec<Dat>`, `Vec<String>`, `DaticleMap`. |
| 24 | |
| 25 | Consequences: |
| 26 | |
| 27 | - Every compound field in a protocol struct has to fall back to `Dat` or |
| 28 | `Vec<Dat>` at the field site and the enclosing type has to expose a |
| 29 | `typed_*()` helper that iterates and calls `T::from_datmap(...)` |
| 30 | explicitly. |
| 31 | - `fe2o3_net::acme::rfc8555::Authorization::typed_challenges()` is one such |
| 32 | hand-rolled unwrap helper; `fe2o3_steel::srv::cfg::ServerConfig::get_vhosts` |
| 33 | and `::get_acme` are two more. Every new protocol type in the workspace |
| 34 | will need another. |
| 35 | |
| 36 | Desired behaviour: |
| 37 | |
| 38 | - For any field of a type `T` where `T: FromDatMap`, emit |
| 39 | `res!(T::from_datmap(...))` in place of the current `get_*` lookup. |
| 40 | - For `Vec<T>`, `Option<T>` and `BTreeMap<String, T>` where `T: FromDatMap`, |
| 41 | unwrap the generic wrapper and recurse. |
| 42 | - Keep the existing primitive/`Dat`/`Vec<Dat>` fast paths as they are. |
| 43 | |
| 44 | Implementation sketch: |
| 45 | |
| 46 | - When the derive encounters a type it does not recognise, stop panicking. |
| 47 | Instead, generate code that assumes `<T as FromDatMap>::from_datmap(...)` |
| 48 | and lets rustc emit a trait-bound error at the call site if the user's |
| 49 | type does not implement `FromDatMap`. This gives the user a clear, |
| 50 | compilable-code error message instead of a derive panic. |
| 51 | - For `Vec<T>`, parse the generic argument, emit a loop that pulls each |
| 52 | element out of a `Dat::List(v)`, calls `T::from_datmap` on each element's |
| 53 | `Dat::Map`, and collects. |
| 54 | - For `Option<T>`, emit presence check plus `T::from_datmap`. |
| 55 | |
| 56 | ### 2. Native `Option<T>` support |
| 57 | |
| 58 | Separate from (1) but related. Today there is no way to express "may be |
| 59 | absent **and** distinguish between absent and a default value". The |
| 60 | `#[optional]` attribute makes an absent field default to `Default::default()` |
| 61 | for the field type, which for `String` is `""` -- indistinguishable from a |
| 62 | present `""` in the wire payload. |
| 63 | |
| 64 | Desired behaviour: a `pub x: Option<T>` field **with no attribute** means |
| 65 | "absent ⇒ `None`, present ⇒ `Some(T)`", no confusion with a default value. |
| 66 | `#[optional]` stays as a separate escape hatch for the value-with-default |
| 67 | case. |
| 68 | |
| 69 | ACME example: `Order.certificate` defaults to `""` today but semantically |
| 70 | means "not yet issued". An `Option<String>` field would make that explicit |
| 71 | and survive typo-style bugs where the caller checks `if order.certificate.is_empty()` |
| 72 | when they meant `.is_some()`. |
| 73 | |
| 74 | ### 3. Error messages should carry field and struct names |
| 75 | |
| 76 | Currently the derive-generated code calls `res!(m.get_string(...))` etc. and |
| 77 | the resulting error is something like `"expected Str, got X"`. Nothing in |
| 78 | that message tells you **which field** of **which struct** blew up, so |
| 79 | debugging a malformed CA response turns into a println/eprintln binary |
| 80 | search. |
| 81 | |
| 82 | Desired behaviour: the derive wraps every field extraction with an |
| 83 | `err!` context carrying the field name and the enclosing struct name. |
| 84 | Something like `"while decoding field `new_nonce` of struct `Directory`"`. |
| 85 | |
| 86 | Cheap to implement (quote!-interpolate the literals), huge debugging win. |
| 87 | |
| 88 | ### 4. JSON `null` versus missing key |
| 89 | |
| 90 | RFC 8555 permits CAs to emit either `"error": null` or to omit the `error` |
| 91 | key entirely. Both paths currently end up indistinguishable: the field |
| 92 | defaults to `Dat::Empty`. This is the same shape as issue (2); resolving |
| 93 | both at once by adopting `Option<T>` semantics with explicit null handling |
| 94 | covers this case too. |
| 95 | |
| 96 | ### 5. `#[rename_all = "camelCase"]` at the struct level |
| 97 | |
| 98 | The current attribute is `#[rename(name = "newNonce")]` -- wordy, and |
| 99 | easily repeated across every field of a camelCase wire format. Serde's |
| 100 | `#[serde(rename_all = "camelCase")]` at the struct level would let us |
| 101 | delete every per-field `#[rename(...)]` line in `rfc8555.rs` and remove a |
| 102 | whole class of typos. |
| 103 | |
| 104 | Relatedly: the simpler per-field form `#[rename = "newNonce"]` (without the |
| 105 | `name =` wrapper) would match serde's de facto standard and be nicer to |
| 106 | type. |
| 107 | |
| 108 | ### 6. Support for simple enums mapped to `Dat::Str` |
| 109 | |
| 110 | ACME status is a closed string set (`"pending"`, `"ready"`, `"valid"`, |
| 111 | `"invalid"`, `"processing"`, `"revoked"`, `"deactivated"`, `"expired"`). |
| 112 | Today we store it as `String` and compare with `==`, losing compile-time |
| 113 | safety. |
| 114 | |
| 115 | Desired: `#[derive(FromDatMap, ToDatMap)]` works on unit-only enums whose |
| 116 | variants map to `Dat::Str` values. Serde supports this via |
| 117 | `#[serde(rename_all = "lowercase")]` at the enum level. The derive would |
| 118 | need to accept a second input kind (enum) and emit a match. |
| 119 | |
| 120 | ### 7. Compile-time errors should be actionable |
| 121 | |
| 122 | Unsupported types currently hit |
| 123 | `unimplemented!("from_datmap: Cannot find an equivalent Dat for type '{}'.")` |
| 124 | at derive expansion time. That manifests as a derive panic, which is one |
| 125 | of the harder errors in Rust to interpret. |
| 126 | |
| 127 | Desired: emit `compile_error!` with a message like |
| 128 | `"field `challenges` of struct `Authorization` has type `Vec<Challenge>`, which is not currently supported by `FromDatMap`. Either use `#[skip]` on the field, or change the field type to `Vec<Dat>` and write a typed getter, or add nested-struct support (see TODO #1)."` |
| 129 | |
| 130 | ### 8. Attribute hygiene |
| 131 | |
| 132 | Attributes currently live in the global attribute namespace: `#[optional]`, |
| 133 | `#[skip]`, `#[rename(...)]`. A shared namespace such as `#[datmap(optional)]`, |
| 134 | `#[datmap(rename = "x")]`, `#[datmap(skip)]` would: |
| 135 | |
| 136 | - make them easier to discover and grep for; |
| 137 | - reduce the risk of name collisions with other attribute macros; |
| 138 | - leave room to add new attribute options like |
| 139 | `#[datmap(default = "expression")]` or |
| 140 | `#[datmap(skip_serializing_if = "Vec::is_empty")]` without polluting the |
| 141 | global namespace further. |
| 142 | |
| 143 | ## Practical takeaway |
| 144 | |
| 145 | Items (1) and (5) would individually eliminate most of the boilerplate in |
| 146 | any fe2o3 module that parses a wire protocol. (2) closes a real semantic |
| 147 | gap that will bite us in future protocol parsers. (3) is cheap and makes |
| 148 | debugging bearable. The rest are quality-of-life polish. |
| 149 | |
| 150 | None of these are prerequisites for finishing the ACME migration. This |
| 151 | list exists so the work is not lost and so whoever picks up the derive |
| 152 | improvements next starts with context for why each item matters, from a |
| 153 | real, non-trivial user of the derive (`fe2o3_net::acme::rfc8555`). |