rakata_generics/from_gff.rs
1//! The roster of formats this crate models, and everything derived from it.
2//!
3//! One list names each view, the error its reader answers with, and the magic
4//! its container carries. From that come the [`FromGff`] impls and the
5//! magic-to-schema lookup, so adding a format is one edit and a view cannot
6//! reach the roster without also reaching the lookup.
7//!
8//! The impls live together rather than beside their types because they are one
9//! decision made fourteen times, and a view added without one would otherwise
10//! be invisible until somebody tried to read it out of a document.
11
12use rakata_formats::schema::{Field, FromGff, HasSchema};
13use rakata_formats::Gff;
14
15use crate::{
16 Are, AreError, Dlg, DlgError, Fac, FacError, Git, GitError, Ifo, IfoError, Utc, UtcError, Utd,
17 UtdError, Ute, UteError, Uti, UtiError, Utm, UtmError, Utp, UtpError, Uts, UtsError, Utt,
18 UttError, Utw, UtwError,
19};
20
21/// Declares this crate's formats, and emits what each of them owes.
22///
23/// The trait impl points at the inherent reader. An inherent associated
24/// function wins the name against a trait's, so the body is the inherent
25/// `from_gff` and not the recursion it reads as. Deleting an inherent
26/// `from_gff` would turn its impl here into an infinite loop rather than a
27/// compile error.
28///
29/// [`schema_for`] comes out of the same rows, which is what stops a view
30/// being modelled and then left out of the lookup. Pairing a magic with the
31/// wrong view's schema is not expressible either, since both sides of a row
32/// read `$view`.
33macro_rules! formats {
34 ($($view:ty => $error:ty, $magic:literal;)+) => {
35 $(
36 impl FromGff for $view {
37 type Error = $error;
38
39 const MAGIC: [u8; 4] = *$magic;
40
41 fn from_gff(gff: &Gff) -> Result<Self, Self::Error> {
42 <$view>::from_gff(gff)
43 }
44 }
45 )+
46
47 /// The schema of the view modelling `file_type`, in parts, or `None`.
48 ///
49 /// A caller holding a parsed GFF and no idea what it is cannot name a
50 /// view, and every schema entry point is generic over one: `T::parts()`,
51 /// `routes_in::<T>()` and `FieldRoute::field_in::<T>()` all resolve at
52 /// compile time. A raw tree over a file the user picked at runtime is
53 /// the caller that cannot supply the `T`.
54 ///
55 /// `None` means this crate models nothing for that container, which is
56 /// the answer for a format it does not cover and for the untyped
57 /// [`GENERIC_FILE_TYPE`](rakata_formats::GENERIC_FILE_TYPE). The generic
58 /// container is not a gap: every reader here accepts it alongside its
59 /// own magic, so such a file could be any of them and naming one would
60 /// be a guess. A caller wanting a schema for one has to be told which
61 /// view by something other than the bytes.
62 ///
63 /// Parts rather than one slice, because that is how a schema holds a
64 /// type that flattens a shared block, and it is what
65 /// [`FieldRoute::field_under`](rakata_formats::schema::FieldRoute::field_under)
66 /// takes.
67 pub fn schema_for(file_type: [u8; 4]) -> Option<&'static [&'static [Field]]> {
68 match file_type {
69 $(<$view as FromGff>::MAGIC => Some(<$view>::parts()),)+
70 _ => None,
71 }
72 }
73
74 #[cfg(test)]
75 /// Every format this crate declares, for a test that has to visit all.
76 const DECLARED: &[([u8; 4], &str)] = &[$((*$magic, stringify!($view)),)+];
77 };
78}
79
80formats! {
81 Are => AreError, b"ARE ";
82 Dlg => DlgError, b"DLG ";
83 Fac => FacError, b"FAC ";
84 Git => GitError, b"GIT ";
85 Ifo => IfoError, b"IFO ";
86 Utc => UtcError, b"UTC ";
87 Utd => UtdError, b"UTD ";
88 Ute => UteError, b"UTE ";
89 Uti => UtiError, b"UTI ";
90 Utm => UtmError, b"UTM ";
91 Utp => UtpError, b"UTP ";
92 Uts => UtsError, b"UTS ";
93 Utt => UttError, b"UTT ";
94 Utw => UtwError, b"UTW ";
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 use rakata_formats::GENERIC_FILE_TYPE;
102
103 /// Every format on the roster is reachable through the lookup.
104 ///
105 /// The one thing the macro cannot enforce, and the reason this survives.
106 /// Both sides of a row come from `$view`, so a magic cannot be paired with
107 /// another view's schema, and a row cannot reach the trait impl without
108 /// reaching the table. What no amount of generation catches is a format
109 /// modelled in this crate and never added to the roster at all, which a
110 /// caller meets as `None` rather than as a wrong answer.
111 #[test]
112 fn every_declared_format_resolves_to_a_schema() {
113 for (magic, view) in DECLARED {
114 let parts = schema_for(*magic)
115 .unwrap_or_else(|| panic!("`{view}` is on the roster and answered nothing"));
116 assert!(
117 parts.iter().any(|part| !part.is_empty()),
118 "`{view}` resolved to a schema declaring nothing"
119 );
120 }
121 }
122
123 /// A container this crate does not model answers nothing rather than
124 /// guessing, and the untyped magic is the case worth pinning.
125 ///
126 /// Every reader here accepts `GFF ` as well as its own, so the pull is to
127 /// treat it as a wildcard and hand back something. Which view it would be
128 /// is unknowable from the bytes.
129 #[test]
130 fn an_unmodelled_container_answers_nothing() {
131 for magic in [
132 GENERIC_FILE_TYPE,
133 *b"NFO ",
134 *b"PT ",
135 *b"BIC ",
136 *b"\0\0\0\0",
137 ] {
138 assert!(
139 schema_for(magic).is_none(),
140 "`{}` was answered for by a crate that does not model it",
141 String::from_utf8_lossy(&magic)
142 );
143 }
144 }
145}