Skip to main content

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}