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
//! Reading what a `schema.usda` declares.
//!
//! Composition stays the composition engine's job: inheritance, sublayers and
//! composed values already have one implementation, and codegen must not grow
//! a second. This module's whole responsibility is to open the stage and
//! extract what its layers *declare*, with the origin of each declaration. It
//! classifies nothing, resolves no inheritance and generates no names —
//! [`resolve`](crate::resolve) does that, over the stage this leaves behind.

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

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

use crate::Builder;
use crate::error::Error;
use crate::model::Origin;
use crate::names;

/// The `/GLOBAL` prim every schema library configures itself through.
const GLOBAL: &str = "/GLOBAL";

/// An opened schema library: the stage its layers compose into, and what those
/// layers declare.
pub struct Source {
    /// The composed stage, which [`resolve`](crate::resolve) reads inheritance
    /// and effective property values from.
    pub stage: usd::Stage,
    /// The `libraryName` the root layer's `/GLOBAL` declares.
    pub library: String,
    /// Whether token identifiers keep the spelling the schema gave them.
    pub use_literal_identifiers: bool,
    /// Whether any layer asked for schema data only, with no Rust.
    pub skip_code_generation: bool,
    /// Every class any layer declares, in root-layer declaration order first.
    pub declarations: Vec<Declaration>,
    /// The `libraryTokens` the root layer's `/GLOBAL` declares.
    pub tokens: Vec<DeclaredToken>,
}

/// One class prim, as the layer that declares it wrote it.
pub struct Declaration {
    /// The prim's name, which is the schema identifier.
    pub name: tf::Token,
    /// The library whose `/GLOBAL` covers the layer declaring it.
    pub library: String,
    /// The prefix upstream puts before that library's class names: its
    /// `libraryPrefix` where its `/GLOBAL` declares one, else its name in
    /// proper case (`usdGenSchema._GetLibPrefix`).
    pub prefix: String,
    /// Whether the root layer declares it, and so whether this run generates
    /// it. A class from a sublayer can be inherited from but is never
    /// regenerated.
    pub generated: bool,
    /// The names its `inheritPaths` targets, in authored order.
    pub bases: Vec<tf::Token>,
    /// Its `customData`, read but not interpreted.
    pub custom_data: CustomData,
    /// Its authored `typeName`.
    pub type_name: Option<tf::Token>,
    /// Its authored `apiSchemas` list op, whose mode validation checks.
    pub api_schemas: Option<sdf::TokenListOp>,
    /// Its documentation.
    pub documentation: Option<String>,
    /// The properties it declares itself.
    pub properties: Vec<PropertyDeclaration>,
    /// Every field authored on the prim, by name. Validation reads these rather
    /// than the composed map, flattening having resolved composition away.
    pub fields: Vec<String>,
    /// Where it was declared.
    pub origin: Origin,
}

/// One property of a class prim, as the layer that declares it wrote it.
///
/// Only its `customData`, which steers the accessor this class generates.
/// Everything else about a property — its type, its fields, where the
/// declaration that wins lives — is a composed question, and
/// [`resolve`](crate::resolve) asks it of the stage.
pub struct PropertyDeclaration {
    /// The property's name, as declared and so without a namespace prefix.
    pub name: tf::Token,
    /// Its `customData`, read but not interpreted.
    pub custom_data: CustomData,
    /// Where it was declared, which a flattened layer no longer records.
    pub origin: Origin,
}

/// Opens the layer at `path` as a stage, its sublayers resolving through the
/// builder's search paths, and reports the first diagnostic composing it
/// raised.
///
/// The stage is built on a registry that knows nothing, so a class prim named
/// `Sphere` cannot pick up built-ins from a registry that already knows
/// `Sphere` — what C++ reaches for `USD_DISABLE_PRIM_DEFINITIONS_FOR_USDGENSCHEMA`
/// to get.
pub fn open_stage(builder: &Builder, path: &Path) -> Result<usd::Stage, Error> {
    let resolver = ar::DefaultResolver::with_search_paths(builder.search_paths.clone());
    let registry = usd::SchemaRegistryBuilder::empty()
        .build()
        .map_err(|source| Error::Core(source.into()))?;

    let stage = usd::Stage::builder()
        .resolver(resolver)
        .schema_registry(registry)
        .open(&path.to_string_lossy())?;

    match Error::composition(path, &stage) {
        Some(error) => Err(error),
        None => Ok(stage),
    }
}

/// Where every layer `stage` has loaded was found, root first: what a build
/// script watches so an edit to any of them regenerates.
///
/// A layer across a reference or payload loads when a prim reaching it is
/// composed, so this is asked once the reader has composed what it reads. A
/// layer reached through a search path is watched where it was found, not by
/// the relative path it was asked for by. An anonymous layer was read from
/// nowhere and so is watched nowhere.
pub fn watched_layers(stage: &usd::Stage) -> Vec<PathBuf> {
    let root = stage.root_layer().identifier.clone();
    let identifiers = stage.layer_identifiers();
    iter::once(&root)
        .chain(identifiers.iter().filter(|id| **id != root))
        .filter_map(|identifier| stage.layer(identifier)?.resolved_path().map(watched_path))
        .collect()
}

/// Opens `schema` and reads every declaration its layers carry.
pub fn open(builder: &Builder, schema: &Path) -> Result<Source, Error> {
    let stage = open_stage(builder, schema)?;

    let identifiers = stage.layer_stack();
    let root = stage.root_layer().identifier.clone();

    // The root layer first, so what this run generates leads the list.
    let mut globals = Vec::new();
    for identifier in iter::once(&root).chain(identifiers.iter().filter(|id| **id != root)) {
        let Some(layer) = stage.layer(identifier) else {
            continue;
        };
        globals.push((identifier, read_global(&layer)));
    }

    let skip_code_generation = globals.iter().any(|(_, global)| global.skip_code_generation);
    let use_literal_identifiers = globals.first().is_none_or(|(_, global)| global.use_literal_identifiers);
    // Only the root layer's, the library being generated being the one whose
    // tokens this run declares.
    let tokens = globals
        .first()
        .map(|(_, global)| global.tokens.clone())
        .unwrap_or_default();

    // A layer with no `/GLOBAL` of its own belongs to whichever library
    // sublayered it; only its class prims matter.
    let owners: HashMap<&String, Owner<'_>> = globals
        .iter()
        .filter_map(|(identifier, global)| Some((*identifier, global.owner()?)))
        .collect();
    let root_owner = owners.get(&root).ok_or_else(|| Error::NoLibraryName {
        schema: schema.to_path_buf(),
    })?;

    let mut declarations = Vec::new();
    for (identifier, _) in &globals {
        let Some(layer) = stage.layer(identifier) else {
            continue;
        };
        let owner = owners.get(identifier).unwrap_or(root_owner);
        declarations.extend(read_classes(&layer, identifier, owner, **identifier == root));
    }

    Ok(Source {
        stage,
        library: root_owner.library.to_owned(),
        use_literal_identifiers,
        skip_code_generation,
        declarations,
        tokens,
    })
}

/// A token a schema asks for outright, beyond the ones its properties imply:
/// `schemaTokens` on a class, `libraryTokens` on the library.
#[derive(Debug, Clone)]
pub struct DeclaredToken {
    /// The identifier the schema keyed it by.
    pub id: String,
    /// The string the constant holds, which defaults to that identifier.
    pub value: String,
    /// What the schema said it is for, if it said.
    pub documentation: Option<String>,
}

/// What one layer's `/GLOBAL` prim configures.
struct Global {
    library: Option<String>,
    /// The `libraryPrefix` it declares, where it declares one.
    prefix: Option<String>,
    use_literal_identifiers: bool,
    skip_code_generation: bool,
    tokens: Vec<DeclaredToken>,
}

/// The library a layer's classes belong to: its name, and the prefix upstream
/// puts before its class names.
struct Owner<'a> {
    library: &'a str,
    prefix: String,
}

impl Global {
    /// The library this `/GLOBAL` names, where it names one, with the prefix
    /// upstream gives its classes: `libraryPrefix` where declared, else the
    /// name in proper case (`usdGenSchema._GetLibPrefix`).
    fn owner(&self) -> Option<Owner<'_>> {
        let library = self.library.as_deref()?;
        Some(Owner {
            library,
            prefix: self.prefix.clone().unwrap_or_else(|| names::proper_case(library)),
        })
    }
}

/// A resolved path as a build script reports it.
///
/// Resolving canonicalizes, which on Windows spells a path verbatim
/// (`\?\D:\...`). Nothing a consumer configured matches that spelling, and
/// cargo reads these back, so a verbatim disk path is unwrapped. A verbatim UNC
/// path is left alone: there the prefix is part of the path, not a decoration.
fn watched_path(resolved: &str) -> PathBuf {
    match resolved.strip_prefix(r"\?\") {
        Some(rest) if rest.as_bytes().get(1) == Some(&b':') => PathBuf::from(rest),
        _ => PathBuf::from(resolved),
    }
}

/// Reads a layer's `/GLOBAL` prim.
///
/// The keys that only mean something to the C++ build (`libraryPath`,
/// `tokensPrefix`, `useExportAPI`) are ignored: there is no C++ build here to
/// name. `libraryPrefix` is read, since it is also how upstream's documentation
/// spells the library's classes, which the generated documentation links.
fn read_global(layer: &sdf::Layer) -> Global {
    let custom_data = layer
        .prim(GLOBAL)
        .ok()
        .flatten()
        .map(|prim| CustomData::read(prim.get::<sdf::Value>(sdf::FieldKey::CustomData)))
        .unwrap_or_default();

    Global {
        library: custom_data.string("libraryName"),
        prefix: custom_data.string("libraryPrefix"),
        use_literal_identifiers: custom_data.flag("useLiteralIdentifier").unwrap_or(true),
        skip_code_generation: custom_data.flag("skipCodeGeneration").unwrap_or(false),
        tokens: custom_data.token_declarations("libraryTokens"),
    }
}

/// Reads every class prim one layer declares, in `primChildren` order.
fn read_classes(layer: &sdf::Layer, identifier: &str, owner: &Owner<'_>, generated: bool) -> Vec<Declaration> {
    let Some(root) = layer.pseudo_root() else {
        return Vec::new();
    };

    root.prim_children()
        .unwrap_or_default()
        .into_iter()
        .filter_map(|name| {
            let path = sdf::Path::abs_root().append_path(name.as_str()).ok()?;
            let prim = layer.prim(&path).ok().flatten()?;
            if prim.specifier() != Some(sdf::Specifier::Class) {
                return None;
            }

            let origin = Origin {
                layer: identifier.to_owned(),
                path: path.clone(),
            };
            Some(Declaration {
                name,
                library: owner.library.to_owned(),
                prefix: owner.prefix.clone(),
                generated,
                bases: inherit_names(&prim),
                custom_data: CustomData::read(prim.get::<sdf::Value>(sdf::FieldKey::CustomData)),
                type_name: prim.type_name(),
                api_schemas: prim.api_schemas(),
                documentation: prim.get(sdf::FieldKey::Documentation),
                properties: read_properties(layer, &prim, &path, identifier),
                fields: prim.fields(),
                origin,
            })
        })
        .collect()
}

/// The names a class prim inherits from, in authored order.
///
/// A schema inherits by path (`inherits = </Typed>`), and every target is a
/// root prim, so the name is all that is carried forward.
fn inherit_names(prim: &sdf::PrimSpecRef<'_>) -> Vec<tf::Token> {
    let Some(list_op) = prim.get::<sdf::PathListOp>(sdf::FieldKey::InheritPaths) else {
        return Vec::new();
    };
    list_op
        .iter()
        .filter_map(|target| target.name().map(tf::Token::from))
        .collect()
}

/// Reads the `customData` of every property a class prim declares.
///
/// Unordered: what the emitter and the schematics write follows the composed
/// order [`resolve`](crate::resolve) reads off the stage, where inheritance has
/// been applied.
fn read_properties(
    layer: &sdf::Layer,
    prim: &sdf::PrimSpecRef<'_>,
    prim_path: &sdf::Path,
    identifier: &str,
) -> Vec<PropertyDeclaration> {
    prim.property_children()
        .unwrap_or_default()
        .into_iter()
        .filter_map(|name| {
            let path = prim_path.append_property(name.as_str()).ok()?;
            let custom_data = layer
                .attribute(&path)
                .ok()
                .flatten()
                .map(|attribute| attribute.get::<sdf::Value>(sdf::FieldKey::CustomData))
                .or_else(|| {
                    layer
                        .relationship(&path)
                        .ok()
                        .flatten()
                        .map(|relationship| relationship.get::<sdf::Value>(sdf::FieldKey::CustomData))
                })?;

            Some(PropertyDeclaration {
                name,
                custom_data: CustomData::read(custom_data),
                origin: Origin {
                    layer: identifier.to_owned(),
                    path,
                },
            })
        })
        .collect()
}

/// A `customData` dictionary, read but not interpreted.
///
/// Both a class prim and a property carry one, and the generator asks the same
/// few questions of each.
#[derive(Default)]
pub struct CustomData(sdf::Dictionary);

impl CustomData {
    /// Reads one, empty when the field is absent or holds something else.
    fn read(value: Option<sdf::Value>) -> Self {
        Self(value.and_then(sdf::Value::try_as_dictionary).unwrap_or_default())
    }

    /// One entry as a string, written either as a string or as a token.
    pub fn string(&self, key: &str) -> Option<String> {
        self.0.get(key).and_then(sdf::Value::as_str).map(str::to_owned)
    }

    /// One entry as a boolean.
    pub fn flag(&self, key: &str) -> Option<bool> {
        self.0.get(key).and_then(sdf::Value::try_as_bool_ref).copied()
    }

    /// One entry as a token list, empty when it is absent.
    ///
    /// Written either as `token[]` or as `string[]`: `customData` is untyped
    /// to the generator that reads it, and upstream schemas use both.
    pub fn tokens(&self, key: &str) -> Vec<tf::Token> {
        let Some(value) = self.0.get(key) else {
            return Vec::new();
        };
        if let Some(tokens) = value.try_as_token_vec_ref() {
            return tokens.clone();
        }
        value
            .try_as_string_vec_ref()
            .map(|strings| strings.iter().map(|text| tf::Token::from(text.as_str())).collect())
            .unwrap_or_default()
    }

    /// The tokens an entry declares outright, rather than through a property.
    ///
    /// Written as `dictionary <id> = { string value = "..."; string doc =
    /// "..." }`, both entries optional: a token that names no value holds its
    /// own identifier.
    pub fn token_declarations(&self, key: &str) -> Vec<DeclaredToken> {
        self.0
            .get(key)
            .and_then(sdf::Value::try_as_dictionary_ref)
            .into_iter()
            .flatten()
            .map(|(id, entry)| {
                let entry = CustomData(entry.try_as_dictionary_ref().cloned().unwrap_or_default());
                DeclaredToken {
                    value: entry.string("value").unwrap_or_else(|| id.clone()),
                    documentation: entry.string("doc"),
                    id: id.clone(),
                }
            })
            .collect()
    }

    /// The per-instance `apiSchemaCanOnlyApplyTo` lists a multiple-apply
    /// schema declares, keyed by instance name.
    ///
    /// An empty list is not recorded, matching what the registry stores when it
    /// reads the manifest this generator writes, so the instance keeps the
    /// schema-wide restriction.
    pub fn instance_restrictions(&self) -> BTreeMap<tf::Token, Vec<tf::Token>> {
        self.0
            .get("apiSchemaInstances")
            .and_then(sdf::Value::try_as_dictionary_ref)
            .into_iter()
            .flatten()
            .filter_map(|(instance, entry)| {
                let allowed = CustomData(entry.try_as_dictionary_ref()?.clone()).tokens("apiSchemaCanOnlyApplyTo");
                (!allowed.is_empty()).then(|| (tf::Token::from(instance.as_str()), allowed))
            })
            .collect()
    }
}

#[cfg(test)]
mod tests {
    use crate::error::Error;
    use crate::tests::read_fixture;

    /// A library has to name itself, since one library refers to another by
    /// that name and not by path.
    #[test]
    fn library_name_required() {
        let error = read_fixture("schema_sublayer.usda").expect_err("a sublayer declares no library of its own");
        assert!(matches!(error, Error::NoLibraryName { .. }), "{error}");
    }

    /// A codeless library asks for schema data and no Rust.
    #[test]
    fn codeless_flag() {
        let library = read_fixture("codeless_schema.usda").expect("resolves");
        assert!(library.skip_code_generation);

        let library = read_fixture("schema.usda").expect("resolves");
        assert!(!library.skip_code_generation);
    }

    /// Every layer read is reported, so a build script can watch the sublayers
    /// a schema resolves to and not just the file it was given.
    #[test]
    fn sublayers_reported() {
        let library = read_fixture("schema.usda").expect("resolves");
        assert!(
            library
                .source_layers
                .iter()
                .any(|layer| layer.ends_with("schema_sublayer.usda")),
            "{:?}",
            library.source_layers
        );
    }
}