moqtap_codec/draft_table.rs
1//! One shape for a per-draft lookup, shared by the tables that need it.
2//!
3//! Two functions in this crate answer the same kind of question — *what does
4//! draft N call this number?* — for two different registries:
5//! [`crate::message_names::message_type_name`] for control message type IDs and
6//! [`crate::setup_option_names::setup_option_name`] for setup parameters. Both
7//! have to take the draft as well as the codepoint, because the codepoints are
8//! reused and retired rather than reserved, and each one's per-draft half lives
9//! in that draft's own module. So both are the same one-arm-per-draft dispatch
10//! over [`crate::version::DraftVersion`], and the interesting part is not the
11//! arms but what
12//! happens at the end of them.
13//!
14//! # The arm that is not written here, and why it is the whole point
15//!
16//! A `match` over [`crate::version::DraftVersion`] has to answer for every
17//! variant, and the
18//! obvious way to end one is `_ => None`. That reads as *a draft this build did
19//! not compile has no name for anything*, which is true and is not the whole of
20//! what it does: it also answers for a draft **nobody has added yet**. The day a
21//! `Draft21` variant lands, a table that ends in a catch-all goes on compiling
22//! and starts answering "no name" for every draft-21 codepoint — which is
23//! indistinguishable from a correct answer about an unassigned number, and which
24//! nothing in the build says a word about.
25//!
26//! That is the drift `scripts/check-draft-parity.py` rule 3 exists to catch: a
27//! match that answers for two or more drafts, reaches the newest, and then
28//! falls through to a `_` arm that is not loud. [`by_draft`] answers it
29//! structurally rather than with a louder arm: spell **both** feature states
30//! for every draft and the match has no catch-all left to fall into, so a
31//! fifteenth variant stops this crate compiling instead of being answered
32//! quietly.
33//!
34//! # What that trades away, stated rather than assumed
35//!
36//! A macro invocation lists its drafts as bare identifiers — `Draft07`, not
37//! `DraftVersion::Draft07` — so `check-draft-parity.py` does not recognise
38//! either table as a draft-enumerating construct, and rules 2 and 3 do not
39//! count them. That is a smaller census and a stronger guarantee: rule 2 asks
40//! whether a list reaches the newest draft, and here a list that does not is a
41//! non-exhaustive `match`, which is a compile error in fourteen feature
42//! configurations. A textual gate is what you need when the compiler cannot see
43//! the omission; it is not an improvement on the compiler seeing it.
44//!
45//! The script's own summary already excludes "anything a macro spells by token
46//! concatenation", and both [`crate::setup_option_names`] and its sibling rely
47//! on exactly this, with the argument living in one place instead of being
48//! restated beside each table.
49//!
50//! # Why the answer is not shared, only the shape
51//!
52//! The two tables do not have the same signature and are not meant to. One takes
53//! a `u64` and answers `Option<&'static str>`; the other takes a whole
54//! [`crate::kvp::KeyValuePair`], because drafts 11 through 13 name a setup
55//! parameter from its key *and the shape of its value*, and answers
56//! `Option<String>` out of the draft's own field renderer. Folding those into
57//! one function would mean inventing a common type that neither registry has.
58//!
59//! So what is shared is the dispatch and nothing else: the draft lookup, the
60//! per-draft cfg pair, and the absent answer. Each table keeps its own
61//! signature, its own doctest and its own rationale for what its codepoints
62//! mean.
63
64/// Answer a question per draft, with an arm for every draft and no catch-all.
65///
66/// ```text
67/// by_draft! { draft, <absent>,
68/// ("draft07", Draft07) => <the answer draft-07 gives>,
69/// ...
70/// ("draft21", Draft21) => <the answer draft-21 gives>,
71/// }
72/// ```
73///
74/// `draft` is the draft number as the IETF writes it — 7 through 21, matching
75/// [`crate::version::DraftVersion::number`]. `<absent>` is the answer for a
76/// draft this crate does not implement **and** for one whose feature flag is off
77/// in this build, which are two different facts with one honest answer: this
78/// build has no table to consult. It is an expression rather than a value, so a
79/// caller that needs to leave the function early can pass `return None` and a
80/// caller that is already returning the right type can pass it directly.
81///
82/// Each row names its feature string and its [`crate::version::DraftVersion`]
83/// variant, and the expansion writes two arms for the pair — one under
84/// `#[cfg(feature = ...)]` carrying the answer, one under `#[cfg(not(...))]`
85/// carrying `<absent>`. Every variant is therefore matched under every feature
86/// set, which is what leaves the `match` with no `_` arm and makes a new
87/// `DraftVersion` variant a compile error here rather than a silent `<absent>`.
88///
89/// The answer expressions are transcribed at the invocation, so they read the
90/// invocation's own bindings — `id`, or a slice built above the call — exactly
91/// as if they had been written into the `match` by hand.
92macro_rules! by_draft {
93 ( $number:expr, $absent:expr, $( ($feat:literal, $variant:ident) => $named:expr ),+ $(,)? ) => {
94 match $crate::version::DraftVersion::from_number($number) {
95 // A draft number this crate does not implement at all. Separate
96 // from the arms below only in why it has no answer, which is why
97 // both give the same one.
98 None => $absent,
99 Some(implemented) => match implemented {
100 $(
101 #[cfg(feature = $feat)]
102 $crate::version::DraftVersion::$variant => $named,
103 #[cfg(not(feature = $feat))]
104 $crate::version::DraftVersion::$variant => $absent,
105 )+
106 },
107 }
108 };
109}
110
111pub(crate) use by_draft;