///|
/// Extension usage audit for raw glTF JSON documents.
///
/// The core typed model intentionally focuses on the glTF 2.0 baseline. This
/// small raw-document audit preserves extension declarations so a production
/// importer can make an explicit compatibility decision before discarding
/// extension-specific fields.
///|
pub(all) struct ExtensionAudit {
used : Array[String]
required : Array[String]
unsupported_required : Array[String]
unknown_used : Array[String]
duplicate_used : Array[String]
duplicate_required : Array[String]
required_not_used : Array[String]
valid : Bool
} derive(Debug, ToJson, Eq)
///|
pub(all) struct ExtensionPolicy {
supported : Array[String]
allowed_required : Array[String]
reject_unknown_used : Bool
reject_unknown_required : Bool
reject_duplicate_declarations : Bool
} derive(Debug, ToJson, Eq)
///|
pub fn default_extension_policy() -> ExtensionPolicy {
{
supported: [
"KHR_materials_pbrSpecularGlossiness", "KHR_materials_unlit", "KHR_texture_transform",
"KHR_lights_punctual", "KHR_animation_pointer", "EXT_meshopt_compression",
"KHR_draco_mesh_compression",
],
allowed_required: [
"KHR_materials_unlit", "KHR_texture_transform", "EXT_meshopt_compression",
],
reject_unknown_used: false,
reject_unknown_required: true,
reject_duplicate_declarations: true,
}
}
///|
fn string_array_field(root : Map[String, Json], key : String) -> Array[String] {
match root.get(key) {
Some(Json::Array(values)) => {
let result : Array[String] = []
for value in values {
match value {
Json::String(text) => result.push(text)
_ => ()
}
}
result
}
_ => []
}
}
///|
fn duplicate_strings(values : Array[String]) -> Array[String] {
let seen : Map[String, Bool] = Map([])
let duplicates : Map[String, Bool] = Map([])
for value in values {
if seen.contains(value) {
duplicates[value] = true
} else {
seen[value] = true
}
}
duplicates.keys().collect()
}
///|
pub fn extension_audit(
json : String,
policy : ExtensionPolicy,
) -> ExtensionAudit raise ParseError {
let root = @json.parse(json) catch {
error => raise ParseError::InvalidJson(msg="\{Repr(error)}")
}
match root {
Json::Object(object) => {
let used = string_array_field(object, "extensionsUsed")
let required = string_array_field(object, "extensionsRequired")
let unsupported_required = required.filter(extension => {
!policy.allowed_required.contains(extension)
})
let unknown_used = used.filter(extension => {
!policy.supported.contains(extension)
})
let duplicate_used = duplicate_strings(used)
let duplicate_required = duplicate_strings(required)
let required_not_used = required.filter(extension => {
!used.contains(extension)
})
let valid = (!policy.reject_unknown_used || unknown_used.length() == 0) &&
(!policy.reject_unknown_required || unsupported_required.length() == 0) &&
(
!policy.reject_duplicate_declarations ||
(duplicate_used.length() == 0 && duplicate_required.length() == 0)
) &&
required_not_used.length() == 0
{
used,
required,
unsupported_required,
unknown_used,
duplicate_used,
duplicate_required,
required_not_used,
valid,
}
}
_ => raise ParseError::InvalidFieldType(field="root", expected="object")
}
}
///|
pub fn extension_audit_default(
json : String,
) -> ExtensionAudit raise ParseError {
extension_audit(json, default_extension_policy())
}
///|
pub fn extension_is_used(audit : ExtensionAudit, extension : String) -> Bool {
audit.used.contains(extension)
}
///|
pub fn extension_is_required(
audit : ExtensionAudit,
extension : String,
) -> Bool {
audit.required.contains(extension)
}
///|
pub fn extension_usage_count(audit : ExtensionAudit) -> Int {
audit.used.length()
}
///|
pub fn extension_required_count(audit : ExtensionAudit) -> Int {
audit.required.length()
}
///|
pub fn extension_audit_issues(audit : ExtensionAudit) -> Array[String] {
let result : Array[String] = []
for extension in audit.unsupported_required {
result.push("unsupported required extension: \{extension}")
}
for extension in audit.unknown_used {
result.push("unknown used extension: \{extension}")
}
for extension in audit.duplicate_used {
result.push("duplicate used extension: \{extension}")
}
for extension in audit.duplicate_required {
result.push("duplicate required extension: \{extension}")
}
for extension in audit.required_not_used {
result.push(
"required extension is absent from extensionsUsed: \{extension}",
)
}
result
}
///|
pub fn extension_audit_summary_lines(audit : ExtensionAudit) -> Array[String] {
[
"used=\{audit.used.length()}, required=\{audit.required.length()}, valid=\{audit.valid}",
"unsupported_required=\{audit.unsupported_required.length()}, unknown_used=\{audit.unknown_used.length()}",
"duplicates=\{audit.duplicate_used.length() + audit.duplicate_required.length()}",
]
}