Skip to content

Commit 3d572e8

Browse files
authored
Resolve element optionality via both optional attribute and conformance (#1578)
* parse isOptional from both optional attribute and conformance
1 parent e26405c commit 3d572e8

File tree

9 files changed

+281
-11
lines changed

9 files changed

+281
-11
lines changed

docs/api.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17935,6 +17935,7 @@ Environment utilities for ZAP
1793517935
* [.logBrowser(msg, err)](#module_JS API_ Environment utilities.logBrowser)
1793617936
* [.logIpc(msg, err)](#module_JS API_ Environment utilities.logIpc)
1793717937
* [.logDebug(msg, err)](#module_JS API_ Environment utilities.logDebug)
17938+
* [.logWarningToFile(msg)](#module_JS API_ Environment utilities.logWarningToFile)
1793817939
* [.isMatchingVersion(versionsArray, providedVersion)](#module_JS API_ Environment utilities.isMatchingVersion) ⇒
1793917940
* [.versionsCheck()](#module_JS API_ Environment utilities.versionsCheck) ⇒
1794017941
* [.httpStaticContent()](#module_JS API_ Environment utilities.httpStaticContent) ⇒
@@ -18244,6 +18245,17 @@ Debug level message.
1824418245
| msg | <code>\*</code> |
1824518246
| err | <code>\*</code> |
1824618247

18248+
<a name="module_JS API_ Environment utilities.logWarningToFile"></a>
18249+
18250+
### JS API: Environment utilities.logWarningToFile(msg)
18251+
Log Warning level message to zap.log file.
18252+
18253+
**Kind**: static method of [<code>JS API: Environment utilities</code>](#module_JS API_ Environment utilities)
18254+
18255+
| Param | Type |
18256+
| --- | --- |
18257+
| msg | <code>\*</code> |
18258+
1824718259
<a name="module_JS API_ Environment utilities.isMatchingVersion"></a>
1824818260

1824918261
### JS API: Environment utilities.isMatchingVersion(versionsArray, providedVersion) ⇒
@@ -20175,6 +20187,7 @@ This module provides utilities for evaluating conformance expressions.
2017520187
* [~evaluateBooleanExpression(expr)](#module_Validation API_ Evaluate conformance expressions..evaluateConformanceExpression..evaluateBooleanExpression)
2017620188
* [~evaluateWithParentheses(expr)](#module_Validation API_ Evaluate conformance expressions..evaluateConformanceExpression..evaluateWithParentheses)
2017720189
* [~checkMissingTerms(expression, elementMap)](#module_Validation API_ Evaluate conformance expressions..checkMissingTerms) ⇒
20190+
* [~checkIfExpressionHasTerm(expression, term)](#module_Validation API_ Evaluate conformance expressions..checkIfExpressionHasTerm) ⇒
2017820191

2017920192
<a name="module_Validation API_ Evaluate conformance expressions..evaluateConformanceExpression"></a>
2018020193

@@ -20235,6 +20248,19 @@ If so, it means the conformance depends on terms with unknown values and changes
2023520248
| expression | <code>\*</code> |
2023620249
| elementMap | <code>\*</code> |
2023720250

20251+
<a name="module_Validation API_ Evaluate conformance expressions..checkIfExpressionHasTerm"></a>
20252+
20253+
### Validation API: Evaluate conformance expressions~checkIfExpressionHasTerm(expression, term) ⇒
20254+
Check if the expression contains a given term.
20255+
20256+
**Kind**: inner method of [<code>Validation API: Evaluate conformance expressions</code>](#module_Validation API_ Evaluate conformance expressions)
20257+
**Returns**: true if the expression contains the term, false otherwise
20258+
20259+
| Param |
20260+
| --- |
20261+
| expression |
20262+
| term |
20263+
2023820264
<a name="module_Validation API_ Parse conformance data from XML"></a>
2023920265

2024020266
## Validation API: Parse conformance data from XML
@@ -20244,6 +20270,7 @@ This module provides utilities for parsing conformance data from XML into expres
2024420270
* [Validation API: Parse conformance data from XML](#module_Validation API_ Parse conformance data from XML)
2024520271
* [~parseConformanceFromXML(operand)](#module_Validation API_ Parse conformance data from XML..parseConformanceFromXML) ⇒
2024620272
* [~parseConformanceRecursively(operand, depth, parentJoinChar)](#module_Validation API_ Parse conformance data from XML..parseConformanceRecursively) ⇒
20273+
* [~getOptionalAttributeFromXML(element, elementType)](#module_Validation API_ Parse conformance data from XML..getOptionalAttributeFromXML) ⇒
2024720274

2024820275
<a name="module_Validation API_ Parse conformance data from XML..parseConformanceFromXML"></a>
2024920276

@@ -20305,6 +20332,23 @@ When they appear, stop recursing and return the name inside directly
2030520332
| depth | <code>\*</code> | <code>0</code> |
2030620333
| parentJoinChar | <code>\*</code> | |
2030720334

20335+
<a name="module_Validation API_ Parse conformance data from XML..getOptionalAttributeFromXML"></a>
20336+
20337+
### Validation API: Parse conformance data from XML~getOptionalAttributeFromXML(element, elementType) ⇒
20338+
if optional attribute is defined, return its value
20339+
if optional attribute is undefined, check if the element conformance is mandatory
20340+
if both optional attribute and conformance are undefined, return false
20341+
Optional attribute takes precedence over conformance for backward compatibility on certain elements
20342+
Log warnings to zap.log if both optional attribute and conformance are defined
20343+
20344+
**Kind**: inner method of [<code>Validation API: Parse conformance data from XML</code>](#module_Validation API_ Parse conformance data from XML)
20345+
**Returns**: true if the element is optional, false if the element is mandatory
20346+
20347+
| Param | Type |
20348+
| --- | --- |
20349+
| element | <code>\*</code> |
20350+
| elementType | <code>\*</code> |
20351+
2030820352
<a name="module_Validation API_ Validation APIs"></a>
2030920353

2031020354
## Validation API: Validation APIs

src-electron/util/env.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,11 @@ let httpStaticContentPath = path.join(__dirname, '../../../spa')
196196
let versionObject = null
197197
let applicationStateDirectory = null
198198

199+
let file_pino_logger = pino(
200+
pinoOptions,
201+
pino.destination({ dest: path.join(appDirectory(), 'zap.log'), sync: true })
202+
)
203+
199204
/**
200205
* Set up the devlopment environment.
201206
*/
@@ -524,6 +529,18 @@ export function logDebug(msg, err = null) {
524529
log('debug', msg, err)
525530
}
526531

532+
/**
533+
* Log Warning level message to zap.log file.
534+
*
535+
* @param {*} msg
536+
*/
537+
export function logWarningToFile(msg) {
538+
let objectToLog = {
539+
msg: msg
540+
}
541+
file_pino_logger.warn(objectToLog)
542+
}
543+
527544
/**
528545
* Returns true if major or minor component of versions is different.
529546
*

src-electron/validation/conformance-expression-evaluator.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,5 +132,18 @@ function checkMissingTerms(expression, elementMap) {
132132
return missingTerms
133133
}
134134

135+
/**
136+
* Check if the expression contains a given term.
137+
*
138+
* @param expression
139+
* @param term
140+
* @returns true if the expression contains the term, false otherwise
141+
*/
142+
function checkIfExpressionHasTerm(expression, term) {
143+
let terms = expression.match(/[A-Za-z][A-Za-z0-9_]*/g)
144+
return terms && terms.includes(term)
145+
}
146+
135147
exports.evaluateConformanceExpression = evaluateConformanceExpression
136148
exports.checkMissingTerms = checkMissingTerms
149+
exports.checkIfExpressionHasTerm = checkIfExpressionHasTerm

src-electron/validation/conformance-xml-parser.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
* @module Validation API: Parse conformance data from XML
2222
*/
2323

24+
const dbEnum = require('../../src-shared/db-enum')
25+
const conformEvaluator = require('./conformance-expression-evaluator')
26+
const env = require('../util/env')
27+
2428
/**
2529
* Parses conformance from XML data.
2630
* The conformance could come from features, attributes, commands, or events
@@ -155,4 +159,38 @@ function parseConformanceRecursively(operand, depth = 0, parentJoinChar = '') {
155159
}
156160
}
157161

162+
/**
163+
* if optional attribute is defined, return its value
164+
* if optional attribute is undefined, check if the element conformance is mandatory
165+
* if both optional attribute and conformance are undefined, return false
166+
* Optional attribute takes precedence over conformance for backward compatibility on certain elements
167+
* Log warnings to zap.log if both optional attribute and conformance are defined
168+
*
169+
* @param {*} element
170+
* @param {*} elementType
171+
* @returns true if the element is optional, false if the element is mandatory
172+
*/
173+
function getOptionalAttributeFromXML(element, elementType) {
174+
let conformance = parseConformanceFromXML(element)
175+
if (element.$.optional) {
176+
if (conformance) {
177+
env.logWarningToFile(
178+
`Redundant 'optional' attribute and 'conformance' tag defined for ${elementType}: ${element.$.name}.` +
179+
" 'optional' takes precedence, but consider removing it as 'conformance' is the recommended format."
180+
)
181+
}
182+
return element.$.optional == 'true'
183+
} else {
184+
if (conformance) {
185+
return !conformEvaluator.checkIfExpressionHasTerm(
186+
conformance,
187+
dbEnum.conformance.mandatory
188+
)
189+
} else {
190+
return false
191+
}
192+
}
193+
}
194+
158195
exports.parseConformanceFromXML = parseConformanceFromXML
196+
exports.getOptionalAttributeFromXML = getOptionalAttributeFromXML

src-electron/zcl/zcl-loader-silabs.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,10 @@ function prepareCluster(cluster, context, isExtension = false) {
506506
name: command.$.name,
507507
description: command.description ? command.description[0].trim() : '',
508508
source: command.$.source,
509-
isOptional: command.$.optional == 'true' ? true : false,
509+
isOptional: conformParser.getOptionalAttributeFromXML(
510+
command,
511+
'command'
512+
),
510513
conformance: conformParser.parseConformanceFromXML(command),
511514
mustUseTimedInvoke: command.$.mustUseTimedInvoke == 'true',
512515
introducedIn: command.$.introducedIn,
@@ -567,7 +570,7 @@ function prepareCluster(cluster, context, isExtension = false) {
567570
conformance: conformParser.parseConformanceFromXML(event),
568571
priority: event.$.priority,
569572
description: event.description ? event.description[0].trim() : '',
570-
isOptional: event.$.optional == 'true',
573+
isOptional: conformParser.getOptionalAttributeFromXML(event, 'event'),
571574
isFabricSensitive: event.$.isFabricSensitive == 'true'
572575
}
573576
ev.access = extractAccessIntoArray(event)
@@ -685,7 +688,10 @@ function prepareCluster(cluster, context, isExtension = false) {
685688
: null,
686689
isWritable: attribute.$.writable == 'true',
687690
defaultValue: attribute.$.default,
688-
isOptional: attribute.$.optional == 'true',
691+
isOptional: conformParser.getOptionalAttributeFromXML(
692+
attribute,
693+
'attribute'
694+
),
689695
reportingPolicy: reportingPolicy,
690696
storagePolicy: storagePolicy,
691697
isSceneRequired:

src-shared/db-enum.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,3 +247,12 @@ exports.featureMapAttribute = {
247247
name: 'FeatureMap',
248248
code: 65532
249249
}
250+
251+
exports.conformance = {
252+
mandatory: 'M',
253+
optional: 'O',
254+
disallowed: 'X',
255+
deprecated: 'D',
256+
provisional: 'P',
257+
desc: 'desc'
258+
}

test/test-query.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,17 @@ WHERE
255255
.then((rows) => rows.map(dbMapping.map.cluster))
256256
}
257257

258+
/**
259+
*
260+
* @param {*} elements
261+
* @param {*} name
262+
* @returns true if the element is optional, false if the element is mandatory
263+
*/
264+
function checkIfElementIsOptional(elements, name) {
265+
let element = elements.find((element) => element.name == name)
266+
return element ? element.isOptional : false
267+
}
268+
258269
exports.getAllEndpointTypeClusterState = getAllEndpointTypeClusterState
259270
exports.selectCountFrom = selectCountFrom
260271
exports.getEndpointTypeAttributes = getEndpointTypeAttributes
@@ -263,3 +274,4 @@ exports.createSession = createSession
263274
exports.getAllSessionNotifications = getAllSessionNotifications
264275
exports.getAllNotificationMessages = getAllNotificationMessages
265276
exports.getAllSessionClusters = getAllSessionClusters
277+
exports.checkIfElementIsOptional = checkIfElementIsOptional

test/zcl-loader.test.js

Lines changed: 111 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const dbEnum = require('../src-shared/db-enum')
2323
const queryZcl = require('../src-electron/db/query-zcl')
2424
const queryDeviceType = require('../src-electron/db/query-device-type')
2525
const queryCommand = require('../src-electron/db/query-command')
26+
const queryEvent = require('../src-electron/db/query-event')
2627
const queryPackage = require('../src-electron/db/query-package')
2728
const queryPackageNotification = require('../src-electron/db/query-package-notification')
2829
const zclLoader = require('../src-electron/zcl/zcl-loader')
@@ -594,14 +595,14 @@ test(
594595
let ctx = await zclLoader.loadZcl(db, env.builtinMatterZclMetafile())
595596
let packageId = ctx.packageId
596597

597-
let zclCluster = await queryZcl.selectClusterByCode(db, packageId, 0x001f)
598+
let aclCluster = await queryZcl.selectClusterByCode(db, packageId, 0x001f)
598599

599600
/* Verify that the ACL attribute, defined using the list type format `array="true" type="X"` in XML,
600601
is correctly parsed and stored in the database as an array of AccessControlEntryStruct. */
601602
let attributes = await dbApi.dbAll(
602603
db,
603604
"SELECT * FROM ATTRIBUTE WHERE CLUSTER_REF = ? AND CODE = 0x0000 AND NAME = 'ACL'",
604-
[zclCluster.id]
605+
[aclCluster.id]
605606
)
606607
expect(attributes.length).toBe(1)
607608
let aclAttribute = attributes[0]
@@ -623,3 +624,111 @@ test(
623624
},
624625
testUtil.timeout.long()
625626
)
627+
628+
test(
629+
'test loading IS_OPTIONAL column for Matter attributes, commands, and events',
630+
async () => {
631+
let db = await dbApi.initRamDatabase()
632+
try {
633+
await dbApi.loadSchema(db, env.schemaFile(), env.zapVersion())
634+
let ctx = await zclLoader.loadZcl(db, env.builtinMatterZclMetafile())
635+
let packageId = ctx.packageId
636+
637+
let doorLockCluster = await queryZcl.selectClusterByCode(
638+
db,
639+
packageId,
640+
0x0101
641+
)
642+
let doorLockClusterId = doorLockCluster.id
643+
644+
let attributes =
645+
await queryZcl.selectAttributesByClusterIdIncludingGlobal(
646+
db,
647+
doorLockClusterId,
648+
[packageId]
649+
)
650+
// optional attribute undefined, conformance undefined -> mandatory
651+
expect(
652+
testQuery.checkIfElementIsOptional(attributes, 'ActuatorEnabled')
653+
).toBeFalsy()
654+
// optional="true", conformance undefined -> optional
655+
expect(
656+
testQuery.checkIfElementIsOptional(attributes, 'DoorClosedEvents')
657+
).toBeTruthy()
658+
// optional="false", conformance undefined -> mandatory
659+
expect(
660+
testQuery.checkIfElementIsOptional(attributes, 'LockType')
661+
).toBeFalsy()
662+
// mandatory conformance, optional attribute undefined -> mandatory
663+
expect(
664+
testQuery.checkIfElementIsOptional(attributes, 'OperatingMode')
665+
).toBeFalsy()
666+
// optionalConform to feature DPS, optional="true" -> optional
667+
expect(
668+
testQuery.checkIfElementIsOptional(attributes, 'DoorOpenEvents')
669+
).toBeTruthy()
670+
// mandatory conformance, optional="true" -> optional as optional="true" takes precedence
671+
expect(
672+
testQuery.checkIfElementIsOptional(attributes, 'LockState')
673+
).toBeTruthy()
674+
// mandatoryConform to feature DPS, optional="false" -> mandatory as optional="false" takes precedence
675+
expect(
676+
testQuery.checkIfElementIsOptional(attributes, 'DoorState')
677+
).toBeFalsy()
678+
679+
let commands = await queryCommand.selectCommandsByClusterId(
680+
db,
681+
doorLockClusterId,
682+
[packageId]
683+
)
684+
// optional attribute undefined, conformance undefined -> mandatory
685+
expect(
686+
testQuery.checkIfElementIsOptional(commands, 'LockDoor')
687+
).toBeFalsy()
688+
// optional="true", conformance undefined -> optional
689+
expect(
690+
testQuery.checkIfElementIsOptional(commands, 'GetWeekDaySchedule')
691+
).toBeTruthy()
692+
// mandatory conformance, optional attribute undefined -> mandatory
693+
expect(
694+
testQuery.checkIfElementIsOptional(commands, 'UnlockDoor')
695+
).toBeFalsy()
696+
// mandatoryConform to feature WDSCH, optional="true" -> optional
697+
expect(
698+
testQuery.checkIfElementIsOptional(commands, 'SetWeekDaySchedule')
699+
).toBeTruthy()
700+
// optional conformance, optional="false" -> mandatory as optional="false" takes precedence
701+
expect(
702+
testQuery.checkIfElementIsOptional(commands, 'UnlockWithTimeout')
703+
).toBeFalsy()
704+
705+
let events = await queryEvent.selectEventsByClusterId(
706+
db,
707+
doorLockClusterId
708+
)
709+
// optional attribute undefined, conformance undefined -> mandatory
710+
expect(
711+
testQuery.checkIfElementIsOptional(events, 'LockUserChange')
712+
).toBeFalsy()
713+
// optional="true", conformance undefined -> optional
714+
expect(
715+
testQuery.checkIfElementIsOptional(events, 'LockOperation')
716+
).toBeTruthy()
717+
// mandatory conformance, optional attribute undefined -> mandatory
718+
expect(
719+
testQuery.checkIfElementIsOptional(events, 'LockOperationError')
720+
).toBeFalsy()
721+
// mandatoryConform to feature DPS, optional="true" -> optional
722+
expect(
723+
testQuery.checkIfElementIsOptional(events, 'DoorStateChange')
724+
).toBeTruthy()
725+
// mandatory conformance, optional="true" -> optional as optional="true" takes precedence
726+
expect(
727+
testQuery.checkIfElementIsOptional(events, 'DoorLockAlarm')
728+
).toBeTruthy()
729+
} finally {
730+
await dbApi.closeDatabase(db)
731+
}
732+
},
733+
testUtil.timeout.long()
734+
)

0 commit comments

Comments
 (0)