openusd-build 0.8.0

Generates Rust schema views from OpenUSD schema.usda files, for use as a build dependency
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
//! What the generator knows about a schema library, between reading it and
//! emitting from it.
//!
//! Every output reads this and nothing else. Composition is consulted once,
//! while [`Library`] is built, so the Rust emitter and the schematics writer
//! cannot come to different conclusions about one field — and so a schema is
//! read once however many files it produces.

use std::collections::{BTreeMap, HashMap, HashSet};
use std::path::PathBuf;

use openusd::{sdf, tf, usd};

use crate::plug_info::Plugin;

pub use crate::load::DeclaredToken;

/// The base every typed schema reaches. A schema that reaches it is one a
/// prim's `typeName` can name.
pub const TYPED: &str = "Typed";

/// The base every applied API schema inherits directly.
pub const API_SCHEMA_BASE: &str = "APISchemaBase";

/// The root every schema derives from, which a class inheriting nothing still
/// reaches and the manifest names for it.
pub const SCHEMA_BASE: &str = "SchemaBase";

/// The `customData` key a property sets to say it exists only to change a
/// built-in API schema's declaration, rather than to declare one of its own.
pub const API_SCHEMA_OVERRIDE: &str = "apiSchemaOverride";

/// The `apiSchemaType` an API schema declares to say it is never applied.
///
/// Saying nothing is not the same: an API schema that declares no kind is
/// single-apply, so only this spelling makes one non-applied.
pub const NON_APPLIED: &str = "nonApplied";

/// One schema library: what a single `schema.usda` and its sublayers declare.
#[derive(Debug)]
pub struct Library {
    /// The `libraryName` its `/GLOBAL` prim declares, which names the library
    /// wherever one library refers to another. Not the file's directory.
    pub name: String,
    /// Whether token identifiers keep the spelling the schema gave them
    /// (`useLiteralIdentifier`, default true).
    pub use_literal_identifiers: bool,
    /// Whether the library asked for schema data only, with no Rust
    /// (`skipCodeGeneration`).
    pub skip_code_generation: bool,
    /// The classes to generate, in the order the root layer declares them.
    ///
    /// A class a sublayer declares is not here. What a view can inherit from is
    /// therefore a class of this layer, one of the schema roots, or a class of
    /// a library that declares its own `libraryName` and generates its own
    /// views; anything else is reported as an ungenerated base.
    pub classes: Vec<Class>,
    /// Classes a class here reflects that this run does not generate, resolved
    /// from the same stage so their accessors can be emitted as the reflecting
    /// class's own. Not registered or written to the schematics.
    pub reflected: Vec<Class>,
    /// Classes another library declares that a class here inherits from, and
    /// the schemas those reflect, resolved from the same stage. Their accessors
    /// reach a view through its supertraits, so the checks and documentation
    /// of a descendant need them; nothing is emitted from them here.
    pub inherited: Vec<Class>,
    /// Every class another library in the stack declares, known by name, for
    /// documentation to link where prose names one.
    pub foreign: Vec<ForeignClass>,
    /// Where every layer read was found, for a build script to watch.
    pub source_layers: Vec<PathBuf>,
    /// The tokens the library asks for outright, beyond what its schemas imply.
    pub declared_tokens: Vec<DeclaredToken>,
    /// The metadata fields its `plugInfo.json` registers.
    pub metadata: Vec<MetadataField>,
    /// The kinds its `plugInfo.json` declares.
    pub kinds: Vec<KindDecl>,
}

/// One kind a library declares, as the `Kinds` block of its `plugInfo.json`
/// declares it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KindDecl {
    /// The kind's name.
    pub name: String,
    /// The kind it derives from. A root kind has none.
    pub base: Option<String>,
}

/// One metadata field a library registers, as its `plugInfo.json` declares it.
#[derive(Debug, Clone)]
pub struct MetadataField {
    /// The key it is authored under.
    pub name: tf::Token,
    /// The value type, under the spelling the declaration gave it. A
    /// metadata-only spelling — `dictionary`, the list-op types — answers no
    /// kind.
    pub type_name: sdf::ValueTypeName,
    /// The specs it may be authored on.
    pub applies_to: usd::MetadataTargets,
    /// What a stage reads where no layer authors it, converted to the type.
    pub fallback: Option<sdf::Value>,
    /// What the declaration says the field is for.
    pub documentation: Option<String>,
}

/// One schema: a class prim in the root layer, and everything generation needs
/// to know about it.
#[derive(Debug)]
pub struct Class {
    /// The registered identifier, which is the class prim's name.
    pub identifier: tf::Token,
    /// The identifier with any version suffix removed.
    pub family: tf::Token,
    /// The version its identifier's suffix names; 0 when it has none.
    pub version: u32,
    /// The `libraryName` of the layer declaring it: this library's for a class
    /// it generates, another's for one it holds to reflect.
    pub library: String,
    /// What upstream calls the class: its library's prefix and its class name,
    /// as in `UsdGeomMesh`, which is how upstream's documentation names it.
    pub upstream_name: String,
    /// What kind of schema it is, which decides what the emitter writes and
    /// what the manifest records.
    pub kind: usd::SchemaKind,
    /// The inheritance chain, nearest first. Empty for a class that inherits
    /// nothing, whose implicit root is `SchemaBase`.
    pub bases: Vec<Base>,
    /// Every property the schema carries, its own and its ancestors', in the
    /// order the schematics writes them. Each records the class that declared
    /// it, so the emitter can take the local ones.
    pub properties: Vec<Property>,
    /// The API schemas applied to every instance of this schema, under the
    /// names the schematics records them by. A multiple-apply schema carries
    /// its built-ins under whatever instance name it is applied with, so those
    /// are templates rather than plain identifiers.
    pub applied_api_schemas: Vec<tf::Token>,
    /// The schema's own documentation, as the schema wrote it.
    pub documentation: Option<String>,
    /// Whether `Typed` is among its ancestors, which is what makes a schema
    /// instantiable.
    pub is_typed: bool,
    /// How many bases the class authored, which is at most one in a schema the
    /// generator can represent.
    pub authored_base_count: usize,
    /// The base the registry walks `is_a` up to: the class's own, or the root
    /// every schema derives from for a class that inherits nothing. `None` only
    /// on that root itself, which derives from nothing.
    pub direct_base: Option<tf::Token>,
    /// Every field authored on the class prim, by name.
    ///
    /// Kept apart from [`fields`](Self::fields) because flattening resolves
    /// composition away: a `references` or `variantSets` opinion is gone from
    /// the composed map, and validation has to see what the schema wrote to
    /// reject it.
    pub authored_fields: Vec<String>,
    /// Every field composition left on the class prim, its own and its
    /// ancestors'.
    ///
    /// These are the prim metadata a registry serves as fallbacks from the
    /// schema's definition — `hidden`, `assetInfo`, `propertyOrder` — so they
    /// travel whole exactly as a property's fields do, and the schematics
    /// writer is the one place that narrows them.
    pub fields: BTreeMap<String, sdf::Value>,
    /// The `typeName` as authored, before a parent declaring the same one
    /// clears it.
    pub authored_type_name: Option<tf::Token>,
    /// The `apiSchemas` list op as authored, whose mode validation checks.
    pub api_schemas_op: Option<sdf::TokenListOp>,
    /// What its `customData` asked for.
    pub metadata: Metadata,
    /// Where it was declared.
    pub origin: Origin,
}

/// A class this one inherits from.
#[derive(Debug, Clone)]
pub struct Base {
    /// The identifier it is registered under.
    pub identifier: tf::Token,
    /// The `libraryName` of the layer declaring it. Where that is another
    /// library's, its views live in another crate or module, reached through
    /// the `extern_library` mapping.
    pub library: String,
    /// Whether the root layer declares it, and so whether this run generates a
    /// view of it.
    pub generated: bool,
    /// The API schemas it names in `reflectedAPISchemas`, whose accessors a
    /// descendant reaches through its trait.
    pub reflected_api_schemas: Vec<tf::Token>,
    /// The class name it declares (`className`), defaulting to its identifier
    /// in proper case. What a generator makes of it is the generator's own.
    pub class_name: String,
    /// What kind of schema it is, classified against what stands behind it.
    pub kind: usd::SchemaKind,
}

/// A class of another library in the stack, known by name: what it is called
/// there and by upstream, for documentation to reach it by.
#[derive(Debug)]
pub struct ForeignClass {
    /// The library declaring it.
    pub library: String,
    /// The class name it declares (`className`), defaulting to its identifier
    /// in proper case, which is what its view is called in its own module.
    pub class_name: String,
    /// What upstream calls it: its library's prefix and that class name.
    pub upstream_name: String,
}

/// One API schema a class names in `reflectedAPISchemas`, and what comes of
/// it, in the order upstream asks: whether it can be reflected at all, then
/// whether the class applies it.
#[derive(Debug)]
pub enum Reflection<'a> {
    /// A single-apply schema the class applies, whose properties it takes as
    /// its own and whose view is one method away.
    Taken {
        /// The schema reflected.
        schema: &'a Class,
        /// Its properties this class takes: every one that no earlier reflected
        /// schema already declared under the same name.
        properties: Vec<&'a Property>,
        /// Its properties an earlier reflected schema already declared, each
        /// with that schema, whose property keeps the name.
        shadowed: Vec<(&'a Property, &'a tf::Token)>,
    },
    /// A schema that is not single-apply, which has no one set of properties
    /// to take: a multiple-apply schema's belong to whatever instance it is
    /// applied under, and a non-applied schema is never applied at all. Set
    /// aside, as upstream sets it aside.
    NotSingleApply(&'a Class),
    /// A single-apply schema the class does not apply, so its properties are
    /// not every prim's to read.
    NotApplied(&'a Class),
}

/// A class's `customData`, with what the registry reads kept apart from what
/// only this crate reads.
///
/// The first group is runtime meaning: it reaches the manifest and decides how
/// a stage composes and what `can_apply` answers. The second is generator
/// convention, and steers the Rust alone.
#[derive(Debug, Default)]
pub struct Metadata {
    /// The namespace every property of a multiple-apply schema sits under.
    pub property_namespace_prefix: Option<tf::Token>,
    /// Schemas this API schema is applied to automatically.
    pub auto_apply_to: Vec<tf::Token>,
    /// The only schemas this API schema may be applied to.
    pub can_only_apply_to: Vec<tf::Token>,
    /// The only instance names a multiple-apply schema admits.
    pub allowed_instance_names: Vec<tf::Token>,
    /// Per-instance overrides of [`can_only_apply_to`](Self::can_only_apply_to).
    pub instance_restrictions: BTreeMap<tf::Token, Vec<tf::Token>>,
    /// The concrete schemas a stage falls back to for this type.
    pub fallback_types: Vec<tf::Token>,

    /// The Rust type name, which defaults to the identifier in proper case.
    pub class_name: String,
    /// Single-apply API schemas whose accessors this class re-emits as its
    /// own, so a caller reaches them without applying the schema first.
    pub reflected_api_schemas: Vec<tf::Token>,
    /// The tokens this schema asks for outright, beyond what its properties
    /// imply.
    pub schema_tokens: Vec<DeclaredToken>,
}

/// One property of a schema.
///
/// The authored fields travel whole, in [`fields`](Self::fields): validation
/// has to see a field it means to reject, the emitter reads the documentation,
/// and the schematics writer is the one place that drops what a schematics
/// does not carry. The accessors below are typed reads over that map, so no
/// two consumers can disagree about a value.
#[derive(Debug)]
pub struct Property {
    /// The name the schema declared, without a namespace prefix.
    pub name: tf::Token,
    /// The name the schematics records. For a multiple-apply schema this is
    /// the template `prefix:__INSTANCE_NAME__:name`.
    pub schematics_name: tf::Token,
    /// What it is at this class, as composition settled it: an attribute or a
    /// relationship, of what type, variability and `custom`.
    pub shape: Shape,
    /// Every class declaring it, nearest first: this class where it redeclares
    /// the property, then each ancestor that declared it.
    ///
    /// The composed result cannot say which class asked for what, and two
    /// questions need to know. The strongest site decides what the property is,
    /// and a rule about a redeclaration compares its shape against the weaker
    /// sites'.
    pub sites: Vec<Site>,
    /// Whether this class declares it in its own layer, which a redeclaration
    /// does as much as a first declaration.
    ///
    /// A redeclaration is both a declaration and an inheritance: an ancestor
    /// introduced the property, and this class still declares it — to change
    /// its fallback, or to ask for the accessor the ancestor suppressed.
    pub is_local: bool,
    /// What its `customData` asked the generator for.
    pub api: PropertyApi,
    /// Every field authored on it, composed over the layers that declare it.
    pub fields: BTreeMap<String, sdf::Value>,
    /// Where it was declared.
    pub origin: Origin,
}

/// One class's declaration of a property.
#[derive(Debug)]
pub struct Site {
    /// The class that declared it.
    pub class: tf::Token,
    /// Whether that declaration asked for `apiSchemaOverride`.
    pub is_override: bool,
    /// What the property is at that class, as composition settled it there,
    /// which is what a class redeclaring it further down is held to.
    pub shape: Shape,
    /// The accessor name that declaration asked for, or `None` where it asked
    /// for none.
    ///
    /// A class redeclaring an ancestor's property has to know what the ancestor
    /// called it, and the ancestor may be declared in a library this run does
    /// not generate — so the name travels with the site rather than being
    /// looked up on a class that may not be here.
    pub api_name: Option<String>,
    /// Where it was written.
    pub origin: Origin,
}

/// What a property is at one class, apart from its value: the fields an
/// accessor's creator authors, and so the ones a redeclaration may not change.
///
/// What composes at the class is what a stage sees and a creator authors, so
/// the shape is read there: a class that redeclares an inherited uniform
/// property without spelling `uniform` again composes to uniform through the
/// class it inherits from, and is uniform.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Shape {
    /// Whether it is an attribute or a relationship.
    pub spec_type: sdf::SpecType,
    /// An attribute's `typeName` as the schema spells it, before anything looks
    /// it up. `None` for a relationship, and for an attribute holding no token.
    pub type_name: Option<tf::Token>,
    /// Whether the property may only be authored at the default time, which
    /// USD leaves varying unless a schema says otherwise.
    pub variability: sdf::Variability,
    /// Whether the schema declared it `custom`, which its creator authors.
    pub custom: bool,
}

/// A property's `customData`, which steers its accessor and nothing else.
#[derive(Debug, Default)]
pub struct PropertyApi {
    /// Whether the library supplies the read accessor by hand
    /// (`apiGetImplementation = "custom"`), so the emitter writes everything
    /// but that one method and leaves its name free.
    pub custom_get: bool,
}

/// Where something was declared, so a diagnostic names the file a contributor
/// has to open.
#[derive(Debug, Clone)]
pub struct Origin {
    /// The layer that declared it.
    pub layer: String,
    /// Its path in that layer.
    pub path: sdf::Path,
}

impl Library {
    /// A library called `name` that declares no schema, for what a plugin
    /// declares alone.
    pub fn declarations(name: &str) -> Self {
        Library {
            name: name.to_owned(),
            use_literal_identifiers: true,
            skip_code_generation: false,
            classes: Vec::new(),
            reflected: Vec::new(),
            inherited: Vec::new(),
            foreign: Vec::new(),
            source_layers: Vec::new(),
            declared_tokens: Vec::new(),
            metadata: Vec::new(),
            kinds: Vec::new(),
        }
    }

    /// Takes what the plugins named after this library declare: their
    /// metadata fields and kinds, and each file among the layers to watch.
    pub fn take_declarations(&mut self, plugins: &[Plugin]) {
        for plugin in plugins.iter().filter(|plugin| plugin.name == self.name) {
            self.metadata.extend(plugin.fields.iter().cloned());
            self.kinds.extend(plugin.kinds.iter().cloned());
            self.source_layers.push(plugin.path.clone());
        }
    }

    /// Every class whose accessors this library emits: the ones it generates,
    /// then the ones it holds to reflect.
    pub fn emitted(&self) -> impl Iterator<Item = &Class> {
        self.classes.iter().chain(&self.reflected)
    }

    /// Every class this library holds: the ones it emits accessors for, then
    /// the ones another library declares that its classes inherit from.
    pub fn held(&self) -> impl Iterator<Item = &Class> {
        self.emitted().chain(&self.inherited)
    }

    /// The class `identifier` names, among the ones this library holds.
    pub fn find(&self, identifier: &tf::Token) -> Option<&Class> {
        self.held().find(|class| &class.identifier == identifier)
    }
}

impl Class {
    /// The properties this class declares itself, in schematics order,
    /// including any it redeclares.
    pub fn local_properties(&self) -> impl Iterator<Item = &Property> {
        self.properties.iter().filter(|property| property.is_local)
    }

    /// What comes of each API schema this class names in
    /// `reflectedAPISchemas`, in that order.
    ///
    /// A name the library does not hold is left out, which resolving reports.
    /// A name listed twice is one schema, and one an ancestor lists is left to
    /// the ancestor, whichever library declares it: what the ancestor takes
    /// arrives through its trait, and taking it again here would offer every
    /// accessor twice, while what the ancestor sets aside would be set aside
    /// here for the same reason. Where two taken schemas declare one property
    /// name, the first keeps it and the later one's is recorded as shadowed,
    /// which is the order upstream generates in.
    pub fn reflections<'a>(&self, library: &'a Library) -> Vec<Reflection<'a>> {
        let inherited: HashSet<&tf::Token> = self.bases.iter().flat_map(|base| &base.reflected_api_schemas).collect();
        let mut named: HashSet<&tf::Token> = HashSet::new();
        let mut taken: HashMap<&'a tf::Token, &'a tf::Token> = HashMap::new();
        self.metadata
            .reflected_api_schemas
            .iter()
            .filter(|name| named.insert(*name) && !inherited.contains(*name))
            .filter_map(|name| library.find(name))
            .map(|schema| {
                if schema.kind != usd::SchemaKind::SingleApplyApi {
                    return Reflection::NotSingleApply(schema);
                }
                if !self.applied_api_schemas.contains(&schema.identifier) {
                    return Reflection::NotApplied(schema);
                }

                let mut properties = Vec::new();
                let mut shadowed = Vec::new();
                for property in schema.local_properties() {
                    if let Some(first) = taken.get(&property.name) {
                        shadowed.push((property, *first));
                    } else {
                        taken.insert(&property.name, &schema.identifier);
                        properties.push(property);
                    }
                }
                Reflection::Taken {
                    schema,
                    properties,
                    shadowed,
                }
            })
            .collect()
    }

    /// The properties this schema declares only to override a built-in API
    /// schema's, which reach the schematics and get no accessor.
    pub fn override_properties(&self) -> impl Iterator<Item = &Property> {
        self.properties.iter().filter(|property| property.is_override())
    }
}

impl Property {
    /// An attribute's declared type. `None` for a relationship, and for an
    /// attribute whose `typeName` names no registered type — which validation
    /// rejects, since an accessor would have no type to read. Unlike every
    /// other reader in the workspace this resolves through
    /// `ValueTypeName::find`, so an unregistered spelling stays visible rather
    /// than becoming an unregistered type name.
    pub fn type_name(&self) -> Option<sdf::ValueTypeName> {
        sdf::ValueTypeName::find(self.shape.type_name.as_ref()?)
    }

    /// The fallback a stage resolves when nothing is authored.
    pub fn fallback(&self) -> Option<&sdf::Value> {
        self.fields.get(sdf::FieldKey::Default.as_str())
    }

    /// The values a token attribute admits, which the tokens module emits and
    /// the accessor's documentation lists.
    ///
    /// Read as tokens or as strings: `allowedTokens` is `token[]` in the schema
    /// SDF declares, but the text parser types the metadata it does not know
    /// from the values it sees, so a list of quoted names arrives as strings.
    pub fn allowed_tokens(&self) -> Vec<tf::Token> {
        allowed_tokens(self.fields.get(sdf::FieldKey::AllowedTokens.as_str()))
    }

    /// The property's own documentation, as the schema wrote it.
    pub fn documentation(&self) -> Option<&str> {
        self.fields
            .get(sdf::FieldKey::Documentation.as_str())?
            .try_as_string_ref()
            .map(String::as_str)
    }

    /// The accessor's name before Rust casing, or `None` where this class
    /// asked for no accessor — an `apiName` of `""`, an override, or a
    /// property it only inherits.
    ///
    /// The strongest site is this class's own declaration exactly where it has
    /// one, which is what [`is_local`](Self::is_local) says.
    pub fn api_name(&self) -> Option<&str> {
        let site = self.sites.first()?;
        self.is_local.then_some(site.api_name.as_deref()).flatten()
    }

    /// Whether an accessor is emitted for it at all.
    pub fn has_accessor(&self) -> bool {
        self.api_name().is_some()
    }

    /// Whether the schematics records it among the class's API schema override
    /// property names.
    ///
    /// The strongest declaration decides: this class's where it redeclares the
    /// property, the nearest ancestor's otherwise. An override therefore
    /// reaches a class that says nothing about the property at all.
    pub fn is_override(&self) -> bool {
        self.sites.first().is_some_and(|site| site.is_override)
    }
}

impl Origin {
    /// Names the declaration the way a diagnostic should: the layer, then the
    /// path inside it.
    pub fn describe(&self) -> String {
        format!("{}{}", self.layer, self.path)
    }
}

/// Whether `name` is one of the schema roots, which are the core's own traits
/// rather than anything a library generates: a layer may still declare one,
/// since that is how a base is there to inherit from.
///
/// The question is asked of the schema family, so a versioned spelling of a
/// root is one. The rule that classifies a schema and the filter that decides
/// whether to generate it both ask here, so a root is a root to both: one that
/// counted for only one of them would be left half generated.
pub fn is_root(name: &tf::Token) -> bool {
    // The families are short and a root is spelled unversioned almost always,
    // so the name itself answers before the family has to be parsed out of it.
    matches!(name.as_str(), TYPED | API_SCHEMA_BASE | SCHEMA_BASE) || {
        let (family, _) = usd::SchemaRegistry::parse_schema_family_and_version(name);
        matches!(family.as_str(), TYPED | API_SCHEMA_BASE | SCHEMA_BASE)
    }
}

/// An `allowedTokens` value as the tokens it lists. A layer read with no schema
/// to type the field holds a list of quoted names as strings.
///
/// TODO: type it in the parser instead, which would let this be one read.
pub fn allowed_tokens(value: Option<&sdf::Value>) -> Vec<tf::Token> {
    match value {
        Some(sdf::Value::TokenVec(tokens)) => tokens.clone(),
        Some(sdf::Value::StringVec(texts)) => texts.iter().map(|text| tf::Token::from(text.as_str())).collect(),
        _ => Vec::new(),
    }
}