blob: 7ede31f6b79a50f60742c5e5d37d0ae58cf4e4b4 [file] [log] [blame]
/**
*
* Copyright (c) 2025 Silicon Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* This module provides utilities for parsing conformance data from XML into expressions.
*
* @module Validation API: Parse conformance data from XML
*/
const dbEnum = require('../../src-shared/db-enum')
const conformEvaluator = require('./conformance-expression-evaluator')
const env = require('../util/env')
/**
* Parses conformance from XML data.
* The conformance could come from features, attributes, commands, or events
*
* Call recursive helper function to parse conformance only if the conformance exists.
* Otherwise, return empty string directly
*
* An example of parsing the conformance of 'User' feature:
*
* Input operand from xml data:
* {
* "$": {"code": "USR", "name": "User"},
* "mandatoryConform": [
* { "andTerm": [
* {
* "condition": [{"$": {"name": "Matter"}}],
* "orTerm": [
* { "feature": [
* { "$": {"name": "PIN"}},
* { "$": {"name": "RID"}},
* { "$": {"name": "FGP"}},
* { "$": {"name": "FACE"}}
* ]
* }
* ]
* }
* ]
* }
* ]
* }
*
* Output conformance string:
* "Matter & (PIN | RID | FGP | FACE)"
*
* @param {*} operand
* @returns The conformance string
*/
function parseConformanceFromXML(operand) {
let hasConformance = Object.keys(operand).some((key) =>
key.includes('Conform')
)
return hasConformance ? parseConformanceRecursively(operand) : ''
}
/**
* helper function to parse conformance or an operand in conformance recursively
*
* The baseLevelTerms variable include terms that can not have nested terms.
* When they appear, stop recursing and return the name inside directly
*
* @param {*} operand
* @param {*} depth
* @param {*} parentJoinChar
* @returns The conformance string.
*/
function parseConformanceRecursively(operand, depth = 0, parentJoinChar = '') {
if (depth > 200) {
throw new Error(`Maximum recursion depth exceeded
when parsing conformance: ${JSON.stringify(operand)}`)
}
const baseLevelTerms = ['feature', 'condition', 'attribute', 'command']
if (operand.mandatoryConform) {
let insideTerm = operand.mandatoryConform[0]
// Recurse further if insideTerm is not empty
if (insideTerm && Object.keys(insideTerm).toString() != '$') {
return parseConformanceRecursively(operand.mandatoryConform[0], depth + 1)
} else {
return dbEnum.conformanceTag.mandatory
}
} else if (operand.optionalConform) {
let insideTerm = operand.optionalConform[0]
// check '$' key is not the only key in the object to handle special cases
// e.g. '<optionalConform choice="a" more="true"/>'
if (insideTerm && Object.keys(insideTerm).toString() != '$') {
return `[${parseConformanceRecursively(operand.optionalConform[0], depth + 1)}]`
} else {
return dbEnum.conformanceTag.optional
}
} else if (operand.otherwiseConform) {
return Object.entries(operand.otherwiseConform[0])
.map(([key, value]) =>
parseConformanceRecursively({ [key]: value }, depth + 1)
)
.join(', ')
} else if (operand.notTerm) {
// need to surround terms inside a notTerm with '()' if it contains multiple terms
// e.g. !(A | B) or !(A & B)
// able to process multiple parallel notTerms, e.g. !A & !B
return operand.notTerm
.map((term) => {
let nt = parseConformanceRecursively(term, depth + 1)
return nt.includes('&') || nt.includes('|') ? `!(${nt})` : `!${nt}`
})
.join(` ${parentJoinChar} `)
} else if (operand.andTerm || operand.orTerm) {
// process andTerm and orTerm in the same logic
// when joining multiple orTerms inside andTerms, we need to
// surround them with '()', vice versa for andTerms inside orTerms
// e.g. A & (B | C) or A | (B & C)
let joinChar = operand.andTerm ? '&' : '|'
let termKey = operand.andTerm ? 'andTerm' : 'orTerm'
let oppositeChar = joinChar == '&' ? '|' : '&'
return Object.entries(operand[termKey][0])
.map(([key, value]) => {
if (baseLevelTerms.includes(key)) {
return value.map((operand) => operand.$.name).join(` ${joinChar} `)
} else {
let terms = parseConformanceRecursively(
{ [key]: value },
depth + 1,
joinChar
)
return terms.includes(oppositeChar) ? `(${terms})` : terms
}
})
.join(` ${joinChar} `)
} else if (operand.provisionalConform) {
return dbEnum.conformanceTag.provisional
} else if (operand.disallowConform) {
return dbEnum.conformanceTag.disallowed
} else if (operand.deprecateConform) {
return dbEnum.conformanceTag.deprecated
} else if (operand.describedConform) {
return dbEnum.conformanceTag.described
} else {
// reach base level terms, return the name directly
for (const term of baseLevelTerms) {
if (operand[term]) {
return operand[term][0].$.name
}
}
// reaching here means the term is too complex to parse
return dbEnum.conformanceTag.described
}
}
/**
* if optional attribute is defined, return its value
* if optional attribute is undefined, check if the element conformance is mandatory
* if both optional attribute and conformance are undefined, return false
* Optional attribute takes precedence over conformance for backward compatibility on certain elements
* Log warnings to zap.log if both optional attribute and conformance are defined
*
* @param {*} element
* @param {*} elementType
* @returns true if the element is optional, false if the element is mandatory
*/
function getOptionalAttributeFromXML(element, elementType) {
let conformance = parseConformanceFromXML(element)
if (element.$.optional) {
if (conformance) {
env.logWarningToFile(
`Redundant 'optional' attribute and 'conformance' tag defined for ${elementType}: ${element.$.name}.` +
" 'optional' takes precedence, but consider removing it as 'conformance' is the recommended format."
)
}
return element.$.optional == 'true'
} else {
if (conformance) {
return !conformEvaluator.checkIfExpressionHasOperand(
conformance,
dbEnum.conformanceTag.mandatory
)
} else {
return false
}
}
}
exports.parseConformanceFromXML = parseConformanceFromXML
exports.getOptionalAttributeFromXML = getOptionalAttributeFromXML