Skip to content
3 changes: 2 additions & 1 deletion examples/validate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"

# run build-openapi to ensure the schema is up to date.
echo "Building schema..."
"$SCRIPT_DIR/../build-openapi.sh"
cd "$SCRIPT_DIR/.."
./build-openapi.sh

echo ""
echo "Validating examples..."
Expand Down
35 changes: 35 additions & 0 deletions tools/checks/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright © MMXXVI 2026 by the Society of Motion Picture and Television Engineers
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation and/or
* other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

'use strict';

const WARNING = 'warning';
const ERROR = 'error';

module.exports = { WARNING, ERROR };
75 changes: 75 additions & 0 deletions tools/checks/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright © MMXXVI 2026 by the Society of Motion Picture and Television Engineers
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation and/or
* other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

'use strict';

const mandatory = require('./mandatory');
const nestedValues = require('./nested-values');
const { WARNING, ERROR } = require('./constants');

/**
* Returns the list of all checks. Each check has a `name` and a
* `run(data, opts)` function that returns an array of errors (empty if the
* check passes or is disabled/not applicable via opts).
*
* @returns {Array<{name: string, run: function(object, object): Array}>}
*/
function getChecks() {
return [
{
name: 'mandatory',
run: mandatory.validateRequiredParamsAndScopes,
},
{
name: 'nestedValues',
run: nestedValues.checkNestedValues,
},
];
}

/**
* Runs all applicable checks against the provided data.
*
* @param {object} data the parsed descriptor to validate
* @param {object} opts
* @param {string} opts.schemaName the schema being validated
* @param {boolean} opts.disable... multiple flags to disable specific checks
* @returns {Array<{message: string, instancePath: string, type?: string}>} aggregated errors from all checks
*/
function runChecks(data, opts) {
const errors = [];
const checks = module.exports.getChecks();
for (const check of checks) {
const checkErrors = check.run(data, opts);
errors.push(...checkErrors);
}
return errors;
}

module.exports = { getChecks, runChecks, WARNING, ERROR };
17 changes: 15 additions & 2 deletions tools/mandatory.js → tools/checks/mandatory.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright © MMXXV 2026 by the Society of Motion Picture and Television Engineers
* Copyright © MMXXVI 2026 by the Society of Motion Picture and Television Engineers
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
Expand Down Expand Up @@ -56,11 +56,16 @@ function getDerivedScope(param, productScope, defaultScope) {
* Validates that all mandatory product parameters and scopes are present
* and have valid values.
* @param {object} deviceDesc the complete device model object
* @param {object} opts
* @param {string} opts.schemaName the schema being validated
* @param {boolean} opts.disableMandatoryParams if true, skip this check
* @returns {Array<{message: string, instancePath: string}>} array of errors
* (empty if valid). Each entry carries an instancePath suitable for
* source-map lookup so callers can report line numbers.
*/
function validateRequiredParamsAndScopes(deviceDesc) {
function validateRequiredParamsAndScopes(deviceDesc, opts) {
if (opts.disableMandatoryParams || opts.schemaName !== 'device') return [];

const errors = [];

if (!deviceDesc || !deviceDesc.params || !deviceDesc.params.product) {
Expand Down Expand Up @@ -113,6 +118,14 @@ function validateRequiredParamsAndScopes(deviceDesc) {
} else if (String(stringValue).trim() === '') {
errors.push({ message: `Product parameter '${key}' has empty string value`, instancePath: `${basePath}/value/string_value` });
}

if (param.value !== undefined && param.value !== null) {
errors.push({ message: `Product parameter '${key}' should not have a 'value' field (use 'value.struct_value.fields.${key}.string_value' instead)`, instancePath: `${basePath}/value` });
}

if (param.params !== undefined && param.params !== null) {
errors.push({ message: `Product parameter '${key}' should not have a 'params' field`, instancePath: `${basePath}/params` });
}
}

return errors;
Expand Down
133 changes: 133 additions & 0 deletions tools/checks/nested-values.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* Copyright © MMXXVI 2026 by the Society of Motion Picture and Television Engineers
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation and/or
* other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

'use strict';

const { WARNING } = require('./constants');

/**
*
* @param {object} desc The input object to check
* @param {object} opts Collection of options
* @param {string} opts.schemaName The schema name of the input object
* @param {boolean} opts.disableNestedValueChecks If true, skip checks for nested values
* @returns {Array<{message: string, instancePath: string, type: string}>} Array of error messages for any nested values found
*/
function checkNestedValues(desc, opts) {
// base disable checks
if (opts.disableNestedValueChecks) return [];
if (opts.schemaName !== 'device') return [];

// no device, nothing to do
if (!desc) return [];

// if the device has no params, there's nothing to check
if (!desc.params) return [];

// first scan for all template_oids and build a map of them
const templateOids = new Set();
for (const param of Object.values(desc.params)) {
searchTemplate(param, templateOids);
}

const warnings = [];
// go through each top-level param
for (const [key, param] of Object.entries(desc.params)) {
// check if it has a value, it should unless its a template
if ((param.value === undefined || param.value === null) && !param.template_oid) {
warnings.push({
message: `Top-level parameter '${key}' has no value and is not a template`,
// leading slash, this is a json pointer for the source map, not an fqoid
instancePath: `/params/${key}/value`,
type: WARNING,
});
}

// look for any nested values in the params
checkParam(param, `/params/${key}`, templateOids, warnings);
}

return warnings;
}

/**
* Search for template_oid in the given object and add them to the templateOids set.
* @param {object} obj The object to search
* @param {Set<string>} templateOids The set to add found template_oids to
* @returns {void}
*/
function searchTemplate(obj, templateOids) {
if (obj.template_oid) {
templateOids.add(obj.template_oid);
}
if (obj.params) {
for (const param of Object.values(obj.params)) {
searchTemplate(param, templateOids);
}
}
}

/**
* Check the given parameter for nested values and warn if its not a template_oid.
* @param {object} param The parameter to check
* @param {string} path The current path in the object
* @param {Set<string>} templateOids The set of template_oids to check against
* @param {Array<{message: string, instancePath: string, type: string}>} warnings The array to add warnings to
* @returns {void}
*/
function checkParam(param, path, templateOids, warnings) {
// nothing to check if there are no sub-params
if (!param.params) return;

for (const [key, subParam] of Object.entries(param.params)) {
const subPath = `${path}/params/${key}`;

// recursively check sub-params
checkParam(subParam, subPath, templateOids, warnings);

// no value, no problem
if (subParam.value === undefined || subParam.value === null) {
continue;
}

// if the param has a value, check if something else references it as a template_oid
// convert from source map pointer to template_oid by replacing /params/ with /
// and removing the leading slash
const template_oid = subPath.replaceAll('/params/', '/').substring(1);
if (!templateOids.has(template_oid)) {
warnings.push({
message: `Nested value found in parameter '${key}' which is not referenced by any template_oid`,
instancePath: `${subPath}/value`,
type: WARNING
});
}
}
}

module.exports = { checkNestedValues };
2 changes: 1 addition & 1 deletion tools/jest.config.cjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module.exports = {
collectCoverageFrom: [
'mandatory.js',
'checks/*.js',
'validator.js',
],
coverageReporters: ['text', 'lcov']
Expand Down
Loading
Loading