Skip to main content

rakata_formats/
gff_schema.rs

1//! Schema types for GFF field validation.
2//!
3//! Defines [`GffType`], [`FieldSchema`], and the [`GffSchema`] trait used to
4//! associate engine-derived field schemas with typed GFF resource wrappers.
5//!
6//! These types live in `rakata-formats` (next to [`GffValue`](crate::gff::GffValue))
7//! so both `rakata-generics` (schema provider) and `rakata-lint` (schema consumer)
8//! can depend on them without circular imports.
9
10use crate::gff::GffValue;
11
12/// Expected GFF field type, mirroring the variants of [`GffValue`].
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
14pub enum GffType {
15    /// `BYTE` - unsigned 8-bit integer.
16    UInt8,
17    /// `CHAR` - signed 8-bit integer.
18    Int8,
19    /// `WORD` - unsigned 16-bit integer.
20    UInt16,
21    /// `SHORT` - signed 16-bit integer.
22    Int16,
23    /// `DWORD` - unsigned 32-bit integer.
24    UInt32,
25    /// `INT` - signed 32-bit integer.
26    Int32,
27    /// `DWORD64` - unsigned 64-bit integer.
28    UInt64,
29    /// `INT64` - signed 64-bit integer.
30    Int64,
31    /// `FLOAT` - 32-bit float.
32    Single,
33    /// `DOUBLE` - 64-bit float.
34    Double,
35    /// `CExoString` - variable-length string.
36    String,
37    /// `CResRef` - resource reference (max 16 chars).
38    ResRef,
39    /// `CExoLocString` - localized string with optional StrRef.
40    LocalizedString,
41    /// `VOID` - raw binary data.
42    Binary,
43    /// Nested struct.
44    Struct,
45    /// List of structs.
46    List,
47    /// Vector3 - 3 packed f32 values (position).
48    Vector3,
49    /// Vector4 - 4 packed f32 values (orientation/quaternion).
50    Vector4,
51}
52
53impl GffType {
54    /// Human-readable name matching the KotOR GFF wire-format type names.
55    pub fn name(self) -> &'static str {
56        match self {
57            GffType::UInt8 => "BYTE",
58            GffType::Int8 => "CHAR",
59            GffType::UInt16 => "WORD",
60            GffType::Int16 => "SHORT",
61            GffType::UInt32 => "DWORD",
62            GffType::Int32 => "INT",
63            GffType::UInt64 => "DWORD64",
64            GffType::Int64 => "INT64",
65            GffType::Single => "FLOAT",
66            GffType::Double => "DOUBLE",
67            GffType::String => "CExoString",
68            GffType::ResRef => "CResRef",
69            GffType::LocalizedString => "CExoLocString",
70            GffType::Binary => "VOID",
71            GffType::Struct => "Struct",
72            GffType::List => "List",
73            GffType::Vector3 => "Vector",
74            GffType::Vector4 => "Quaternion",
75        }
76    }
77}
78
79/// What the engine does with a label, in each direction.
80///
81/// A schema answers "what can a legitimate file contain", which is a wider
82/// question than "what does the engine read". Labels the engine never reads
83/// still appear in files the toolset wrote and in files the engine itself
84/// wrote, so they belong in the schema; leaving them out would make an
85/// unrecognized-field check fire once per shipped resource.
86///
87/// Keeping them in without saying they are dead is not much better. A
88/// diagnostic can then only say the field is unrecognized, when the useful
89/// message is that setting it does nothing and here is what the engine reads
90/// instead. This is the axis that carries the difference.
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum FieldLife {
93    /// The engine reads this label at this path.
94    Live,
95    /// Legitimate file content the engine never reads, with why.
96    ///
97    /// A modder trap: the label looks settable and has no effect. The
98    /// canonical case is a value that lives on a blueprint, where the copy on
99    /// an instance placement loses to the blueprint's with no overlay.
100    ///
101    /// The payload is the reason, phrased to finish the sentence "setting it
102    /// has no effect, because ...", and it is a payload rather than an
103    /// optional note so that no marking can exist without one. A diagnostic
104    /// that can only say "this field is dead" tells an author less than the
105    /// schema already knows; what makes it worth reading is "the engine takes
106    /// a door's tag from the blueprint and ignores this".
107    ///
108    /// Two entries sharing a leaf label can carry different reasons, which is
109    /// why the reason takes part in how diagnostics are grouped rather than
110    /// only in how they read.
111    ReadOnlyDead(&'static str),
112    /// The engine writes this label and never reads it back, with why.
113    ///
114    /// Distinct from [`Self::ReadOnlyDead`] in who put it there. Editing one
115    /// of these is equally pointless, but a file missing it is not a file
116    /// anyone authored wrong, so a diagnostic should not treat the two the
117    /// same way.
118    WriteOnlyDead(&'static str),
119}
120
121/// A constant the engine resolves an absent field to.
122///
123/// Const-constructible so it can sit in the `static` schema tables, which is
124/// why it does not reuse [`GffValue`] (whose string variants own a `String`).
125///
126/// Deliberately narrower than [`GffType`]: there is no `UInt64`, `Int64`,
127/// `Double` or `Binary` variant because no audited page reports a default for
128/// one. Add the variant when a page does, rather than picking an encoding for
129/// a case nobody has traced.
130#[derive(Debug, Clone, Copy, PartialEq)]
131pub enum DefaultValue {
132    /// `BYTE`, and the usual home for a documented boolean default.
133    UInt8(u8),
134    /// `CHAR`.
135    Int8(i8),
136    /// `WORD`.
137    UInt16(u16),
138    /// `SHORT`.
139    Int16(i16),
140    /// `DWORD`. Also carries sentinels such as `OBJECT_INVALID`.
141    UInt32(u32),
142    /// `INT`.
143    Int32(i32),
144    /// `FLOAT`.
145    Single(f32),
146    /// The text a `CExoString` or `CResRef` resolves to.
147    ///
148    /// Usually `""`, but not always: a door's fifteen script slots are seeded
149    /// with the literal `"default"` before any read runs, which is the
150    /// mechanism behind the `traps.2da` hook fallback.
151    Text(&'static str),
152    /// A `CExoLocString` with no strref and no substrings.
153    EmptyLocalizedString,
154    /// `Vector`.
155    Vector3([f32; 3]),
156    /// `Quaternion`.
157    Vector4([f32; 4]),
158}
159
160impl DefaultValue {
161    /// The GFF type this default is a value of.
162    ///
163    /// Lets a guard check a declared default against its entry's
164    /// `expected_type`. A default written at the wrong width never matches
165    /// anything, so without this check it reports as a reader mismatch
166    /// forever and looks exactly like a real finding.
167    ///
168    /// [`Self::Text`] answers `String`; a resref default carries the same
169    /// literal and is accepted against either by [`Self::matches`].
170    pub fn gff_type(&self) -> GffType {
171        match self {
172            DefaultValue::UInt8(_) => GffType::UInt8,
173            DefaultValue::Int8(_) => GffType::Int8,
174            DefaultValue::UInt16(_) => GffType::UInt16,
175            DefaultValue::Int16(_) => GffType::Int16,
176            DefaultValue::UInt32(_) => GffType::UInt32,
177            DefaultValue::Int32(_) => GffType::Int32,
178            DefaultValue::Single(_) => GffType::Single,
179            DefaultValue::Text(_) => GffType::String,
180            DefaultValue::EmptyLocalizedString => GffType::LocalizedString,
181            DefaultValue::Vector3(_) => GffType::Vector3,
182            DefaultValue::Vector4(_) => GffType::Vector4,
183        }
184    }
185
186    /// Returns whether `value` is what this default describes.
187    ///
188    /// Text compares against strings and resrefs alike, since the engine's
189    /// seeded value is the same literal either way. Floats compare bitwise
190    /// rather than by tolerance: both sides originate as a literal from an
191    /// audited page, so a mismatch is a wrong constant rather than drift.
192    pub fn matches(&self, value: &GffValue) -> bool {
193        match (self, value) {
194            (DefaultValue::UInt8(a), GffValue::UInt8(b)) => a == b,
195            (DefaultValue::Int8(a), GffValue::Int8(b)) => a == b,
196            (DefaultValue::UInt16(a), GffValue::UInt16(b)) => a == b,
197            (DefaultValue::Int16(a), GffValue::Int16(b)) => a == b,
198            (DefaultValue::UInt32(a), GffValue::UInt32(b)) => a == b,
199            (DefaultValue::Int32(a), GffValue::Int32(b)) => a == b,
200            (DefaultValue::Int32(a), GffValue::StrRef(b)) => *a == b.raw(),
201            (DefaultValue::Single(a), GffValue::Single(b)) => a.to_bits() == b.to_bits(),
202            (DefaultValue::Text(a), GffValue::String(b)) => a == b,
203            (DefaultValue::Text(a), GffValue::ResRef(b)) => b == a,
204            (DefaultValue::EmptyLocalizedString, GffValue::LocalizedString(b)) => {
205                b.string_ref.is_invalid() && b.substrings.is_empty()
206            }
207            (DefaultValue::Vector3(a), GffValue::Vector3(b)) => {
208                a.iter().zip(b).all(|(x, y)| x.to_bits() == y.to_bits())
209            }
210            (DefaultValue::Vector4(a), GffValue::Vector4(b)) => {
211                a.iter().zip(b).all(|(x, y)| x.to_bits() == y.to_bits())
212            }
213            _ => false,
214        }
215    }
216}
217
218/// Where an audited absent-value came from.
219///
220/// A short quotation of the finding rather than a page name, because one
221/// sentence routinely answers a whole family: a single line of `utd.md` settles
222/// all fifteen door script slots, and one of `uts.md` settles seven playback
223/// scalars. Naming the finding is what lets a reader check the entry without
224/// reading the page top to bottom, and it is what makes a stale entry visible
225/// when the page changes under it.
226///
227/// No line numbers. They rot faster than the prose does.
228pub type DocCitation = &'static str;
229
230/// What the engine holds for a label the file does not carry.
231///
232/// The two audited variants describe different engine mechanisms and read
233/// differently in a diagnostic, so they stay apart. They behave identically
234/// for everything this codebase does with them, because a typed view is always
235/// built from bytes and never loaded onto an object that already holds state:
236/// there is no prior value for a carry-over to carry. Both therefore reduce to
237/// "compare against this constant" for the omit rule and the reader guard,
238/// which is why those share one code path over [`Self::value`].
239///
240/// Every variant that claims to know something carries a [`DocCitation`],
241/// which is what keeps the axis a record of audited findings rather than a
242/// second copy of the reader.
243#[derive(Debug, Clone, Copy, PartialEq)]
244pub enum AbsentDefault {
245    /// The loader stamps this constant whether or not the field was present.
246    ///
247    /// Overwrites whatever the object already held, which is what makes it
248    /// distinct from [`Self::Constructed`] rather than a rewording of it. A
249    /// placeable's trap flags are the clear case: the constructor arms all
250    /// three, and the read stamps `0` over them regardless.
251    Stamped(DefaultValue, DocCitation),
252    /// The loader leaves the object's own value alone; this is what a freshly
253    /// constructed one holds.
254    ///
255    /// Reporting the mechanism without the value would leave the question
256    /// open, so the constructed value is part of the answer rather than a
257    /// footnote to it.
258    Constructed(DefaultValue, DocCitation),
259    /// Audited, and the answer is known, but it is not one constant.
260    ///
261    /// Two shapes land here. The value can be a function of a sibling field:
262    /// `Uti::MaxCharges` reuses whatever `Charges` resolved to, and `Utc`'s
263    /// `MovementRate` and `WalkRate` each fall back to the other's label. Or it
264    /// can be structured rather than scalar: an absent `Utc::SkillList` leaves
265    /// eight skill ranks at eight zeroes, which no [`DefaultValue`] can hold.
266    ///
267    /// Carries a citation but no encoding of the answer. Encoding a derivation
268    /// would mean a small expression language in a static table to serve a
269    /// handful of fields, when the readers already do it in a line or two with
270    /// the siblings in scope; encoding a structured value would mean a variant
271    /// per shape. What the schema needs to say is that the value is known, is
272    /// not a constant, and must never be omitted on write.
273    ///
274    /// **Not a synonym for [`Self::Unverified`].** Folding these together would
275    /// throw away an audit, and it would do real damage downstream: the danger
276    /// list a consumer builds is unverified fields weighted by how often their
277    /// fallback fires, so filing a known answer as unknown plants a permanent
278    /// false alarm in the one output that ranking exists to produce.
279    NotAConstant(DocCitation),
280    /// Nobody has traced what the engine does without this field.
281    ///
282    /// A terminal state, not a gap to be embarrassed about: it is what lets
283    /// the audited entries be trusted. A writer must emit an unverified field,
284    /// since omitting it would bet on a substitution nobody has checked.
285    ///
286    /// The one variant with no citation, because there is nothing to cite.
287    Unverified,
288}
289
290impl AbsentDefault {
291    /// The constant the engine ends up holding, when it is one.
292    ///
293    /// `None` for [`Self::NotAConstant`] and [`Self::Unverified`], which is what
294    /// keeps both out of the omit rule and out of the reader guard.
295    pub fn value(&self) -> Option<DefaultValue> {
296        match self {
297            AbsentDefault::Stamped(v, _) | AbsentDefault::Constructed(v, _) => Some(*v),
298            AbsentDefault::NotAConstant(_) | AbsentDefault::Unverified => None,
299        }
300    }
301
302    /// The audited finding this entry rests on.
303    ///
304    /// `None` only for [`Self::Unverified`]. Every entry that claims to know
305    /// something says where it learned it.
306    pub fn citation(&self) -> Option<DocCitation> {
307        match self {
308            AbsentDefault::Stamped(_, doc)
309            | AbsentDefault::Constructed(_, doc)
310            | AbsentDefault::NotAConstant(doc) => Some(*doc),
311            AbsentDefault::Unverified => None,
312        }
313    }
314}
315
316/// Schema definition for a single GFF field.
317///
318/// Describes a field that can legitimately appear in a particular GFF
319/// resource type. Whether the engine reads it is [`FieldSchema::life`], not a
320/// condition of being here.
321#[derive(Debug, Clone)]
322pub struct FieldSchema {
323    /// GFF field label (e.g., `"Tag"`, `"Appearance_Type"`).
324    pub label: &'static str,
325    /// Expected GFF value type.
326    pub expected_type: GffType,
327    /// What the engine does with this label, in each direction.
328    ///
329    /// [`FieldLife::Live`] for anything the engine reads, which is nearly
330    /// everything here. The dead variants exist so a diagnostic can name what
331    /// actually happens instead of reporting the label as unrecognized.
332    pub life: FieldLife,
333    /// Whether the engine misbehaves when this field is absent.
334    ///
335    /// Set this only where an audit says the loader does something worse than
336    /// substitute a default. The one traced case aborts the containing
337    /// structure outright, dropping a whole gun bank rather than loading it
338    /// with a zero rate of fire, so "absent" and "absent and defaulted" are
339    /// not the same outcome.
340    ///
341    /// An earlier version of this comment said the engine "typically falls
342    /// back to a default" and treated the flag as a hint that the author
343    /// probably meant to set the field. That was never audited, and the case
344    /// we have since traced does the opposite. A field the loader defaults
345    /// harmlessly is not required, however much an author might have meant to
346    /// fill it in; leave it `false`.
347    ///
348    /// Consumed by `rakata-lint`'s SCHEMA-002, which warns on absence. The
349    /// severity of that warning is the same for every required field, so this
350    /// stays a `bool`: one audited failure mode is not two, and until there
351    /// are more the type costs nothing to widen later.
352    pub required: bool,
353    /// What the engine holds when a file omits this label.
354    ///
355    /// Populated from the audited format pages only. The point of sourcing it
356    /// from documentation rather than from the reader is that it can then
357    /// disagree with the reader, which is the whole value of the guard built
358    /// on it: a round-trip check compares a reader against a writer that
359    /// shares its constants, so both being wrong together looks green.
360    ///
361    /// Per view, so the same label can resolve differently for different
362    /// consumers. It does: an absent `TrapDetectable` is `1` on a door and `0`
363    /// on a trigger, because a door carries its constructed value over while
364    /// a trigger's read stamps a literal.
365    pub absent: AbsentDefault,
366    /// Sub-schema for List elements or Struct children. `None` for leaf fields.
367    ///
368    /// When present, validation recurses into each list element or the inner
369    /// struct, checking fields against this child schema.
370    pub children: Option<&'static [FieldSchema]>,
371    /// Optional bounds check for numeric types.
372    ///
373    /// Defines min/max constraints that the engine actually honors before truncating.
374    pub constraint: Option<FieldConstraint>,
375}
376
377/// A numeric boundary constraint for engine value truncation/clamping.
378#[derive(Debug, Clone, PartialEq)]
379pub enum FieldConstraint {
380    /// Integer inclusive range `(min, max)`.
381    RangeInt(i64, i64),
382    /// Floating-point inclusive range `(min, max)`.
383    RangeFloat(f64, f64),
384}
385
386/// Trait providing the engine-derived field schema for a GFF resource type.
387///
388/// Implemented by typed generics (e.g., `Utw`, `Utc`) to expose the full
389/// engine schema - including fields not modeled as struct fields. The schema
390/// is the single source of truth for GFF field validation.
391///
392/// # Example
393///
394/// ```
395/// use rakata_formats::gff_schema::{AbsentDefault, FieldLife, FieldSchema, GffSchema, GffType};
396///
397/// struct MyType;
398///
399/// impl GffSchema for MyType {
400///     fn schema() -> &'static [FieldSchema] {
401///         &[
402///             FieldSchema {
403///                 label: "Tag",
404///                 expected_type: GffType::String,
405///                 life: FieldLife::Live,
406///                 required: false,
407///                 absent: AbsentDefault::Unverified,
408///                 children: None,
409///                 constraint: None,
410///             },
411///         ]
412///     }
413/// }
414///
415/// assert_eq!(MyType::schema().len(), 1);
416/// ```
417pub trait GffSchema {
418    /// Returns the root field schema for this GFF resource type.
419    fn schema() -> &'static [FieldSchema];
420}
421
422/// Map a [`GffValue`] variant to its corresponding [`GffType`].
423///
424/// Extension variants not part of the standard GFF V3.2 type set are mapped
425/// to their logical equivalents:
426/// - [`GffValue::Vector3`] -> [`GffType::Vector3`]
427/// - [`GffValue::Vector4`] -> [`GffType::Vector4`]
428/// - [`GffValue::StrRef`] -> [`GffType::UInt32`]
429pub fn gff_value_type(value: &GffValue) -> GffType {
430    match value {
431        GffValue::UInt8(_) => GffType::UInt8,
432        GffValue::Int8(_) => GffType::Int8,
433        GffValue::UInt16(_) => GffType::UInt16,
434        GffValue::Int16(_) => GffType::Int16,
435        GffValue::UInt32(_) => GffType::UInt32,
436        GffValue::Int32(_) => GffType::Int32,
437        GffValue::UInt64(_) => GffType::UInt64,
438        GffValue::Int64(_) => GffType::Int64,
439        GffValue::Single(_) => GffType::Single,
440        GffValue::Double(_) => GffType::Double,
441        GffValue::String(_) => GffType::String,
442        GffValue::ResRef(_) => GffType::ResRef,
443        GffValue::LocalizedString(_) => GffType::LocalizedString,
444        GffValue::Binary(_) => GffType::Binary,
445        GffValue::Struct(_) => GffType::Struct,
446        GffValue::List(_) => GffType::List,
447        GffValue::Vector4(_) => GffType::Vector4,
448        GffValue::Vector3(_) => GffType::Vector3,
449        GffValue::StrRef(_) => GffType::UInt32,
450    }
451}
452
453#[cfg(test)]
454mod tests {
455    use super::*;
456    use crate::gff::GffLocalizedString;
457    use rakata_core::StrRef;
458
459    #[test]
460    fn gff_type_name_roundtrip() {
461        assert_eq!(GffType::UInt8.name(), "BYTE");
462        assert_eq!(GffType::String.name(), "CExoString");
463        assert_eq!(GffType::ResRef.name(), "CResRef");
464        assert_eq!(GffType::LocalizedString.name(), "CExoLocString");
465        assert_eq!(GffType::Binary.name(), "VOID");
466        assert_eq!(GffType::List.name(), "List");
467    }
468
469    #[test]
470    fn gff_value_type_maps_standard_types() {
471        assert_eq!(gff_value_type(&GffValue::UInt8(0)), GffType::UInt8);
472        assert_eq!(gff_value_type(&GffValue::Int32(0)), GffType::Int32);
473        assert_eq!(
474            gff_value_type(&GffValue::String("x".into())),
475            GffType::String
476        );
477        assert_eq!(gff_value_type(&GffValue::resref_lit("x")), GffType::ResRef);
478        assert_eq!(
479            gff_value_type(&GffValue::LocalizedString(GffLocalizedString::new(
480                StrRef::invalid()
481            ))),
482            GffType::LocalizedString
483        );
484        assert_eq!(gff_value_type(&GffValue::Binary(vec![])), GffType::Binary);
485    }
486
487    #[test]
488    fn gff_value_type_maps_extension_types() {
489        assert_eq!(
490            gff_value_type(&GffValue::Vector3([0.0; 3])),
491            GffType::Vector3
492        );
493        assert_eq!(
494            gff_value_type(&GffValue::Vector4([0.0; 4])),
495            GffType::Vector4
496        );
497        assert_eq!(
498            gff_value_type(&GffValue::StrRef(StrRef::invalid())),
499            GffType::UInt32
500        );
501    }
502
503    #[test]
504    fn text_default_matches_strings_and_resrefs_alike() {
505        // A door's script slots are seeded with this literal, and the same
506        // seeded value reaches us as a CExoString on one field and a CResRef
507        // on another, so one default has to answer for both spellings.
508        let seeded = DefaultValue::Text("default");
509        assert!(seeded.matches(&GffValue::String("default".into())));
510        assert!(seeded.matches(&GffValue::resref_lit("default")));
511        assert!(!seeded.matches(&GffValue::String(String::new())));
512        assert!(!DefaultValue::Text("").matches(&GffValue::resref_lit("default")));
513    }
514
515    #[test]
516    fn a_default_does_not_match_across_types() {
517        // `0` as a BYTE and `0` as an INT are different answers about what the
518        // loader holds, so the guard built on this must not accept either for
519        // the other.
520        assert!(DefaultValue::UInt8(0).matches(&GffValue::UInt8(0)));
521        assert!(!DefaultValue::UInt8(0).matches(&GffValue::Int32(0)));
522        assert!(!DefaultValue::Int32(0).matches(&GffValue::UInt8(0)));
523    }
524
525    #[test]
526    fn sentinel_defaults_are_distinguishable_from_zero() {
527        // The two that would silently pass a comparison written against zero.
528        assert!(DefaultValue::UInt8(0xFF).matches(&GffValue::UInt8(0xFF)));
529        assert!(!DefaultValue::UInt8(0xFF).matches(&GffValue::UInt8(0)));
530        assert!(DefaultValue::Single(-1.0).matches(&GffValue::Single(-1.0)));
531        assert!(!DefaultValue::Single(-1.0).matches(&GffValue::Single(0.0)));
532    }
533
534    #[test]
535    fn empty_localized_string_needs_both_halves_empty() {
536        let mut carries_a_strref = GffLocalizedString::new(StrRef::from_raw(42));
537        assert!(!DefaultValue::EmptyLocalizedString
538            .matches(&GffValue::LocalizedString(carries_a_strref.clone())));
539        carries_a_strref.string_ref = StrRef::invalid();
540        assert!(DefaultValue::EmptyLocalizedString
541            .matches(&GffValue::LocalizedString(carries_a_strref)));
542    }
543
544    #[test]
545    fn only_the_audited_variants_yield_a_value_to_compare() {
546        // What keeps NotAConstant and Unverified out of both the omit rule and the
547        // reader guard: neither can produce a constant to compare against.
548        assert_eq!(
549            AbsentDefault::Stamped(DefaultValue::UInt8(0), "a page").value(),
550            Some(DefaultValue::UInt8(0))
551        );
552        assert_eq!(
553            AbsentDefault::Constructed(DefaultValue::UInt8(1), "a page").value(),
554            Some(DefaultValue::UInt8(1))
555        );
556        assert_eq!(AbsentDefault::NotAConstant("a page").value(), None);
557        // Everything that claims to know something says where it learned it.
558        assert_eq!(
559            AbsentDefault::NotAConstant("a page").citation(),
560            Some("a page")
561        );
562        assert_eq!(AbsentDefault::Unverified.citation(), None);
563        assert_eq!(AbsentDefault::Unverified.value(), None);
564    }
565
566    #[test]
567    fn gff_schema_trait_is_implementable() {
568        struct TestType;
569        impl GffSchema for TestType {
570            fn schema() -> &'static [FieldSchema] {
571                &[FieldSchema {
572                    label: "Tag",
573                    expected_type: GffType::String,
574                    life: FieldLife::Live,
575                    required: false,
576                    absent: AbsentDefault::Unverified,
577                    children: None,
578                    constraint: None,
579                }]
580            }
581        }
582        assert_eq!(TestType::schema().len(), 1);
583        assert_eq!(TestType::schema()[0].label, "Tag");
584    }
585}