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}