blob: 89157c226351b1028aad1427c0fd85384019aa16 [file] [log] [blame]
#
# Copyright (c) 2023 Project CHIP Authors
# All rights reserved.
#
# 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.
#
import xml.etree.ElementTree as ElementTree
from dataclasses import dataclass
from enum import Enum, auto
from typing import Callable
from chip.tlv import uint
OTHERWISE_CONFORM = 'otherwiseConform'
OPTIONAL_CONFORM = 'optionalConform'
PROVISIONAL_CONFORM = 'provisionalConform'
MANDATORY_CONFORM = 'mandatoryConform'
DEPRECATE_CONFORM = 'deprecateConform'
DISALLOW_CONFORM = 'disallowConform'
TOP_LEVEL_CONFORMANCE_TAGS = {OTHERWISE_CONFORM, OPTIONAL_CONFORM,
PROVISIONAL_CONFORM, MANDATORY_CONFORM, DEPRECATE_CONFORM, DISALLOW_CONFORM}
AND_TERM = 'andTerm'
OR_TERM = 'orTerm'
NOT_TERM = 'notTerm'
FEATURE_TAG = 'feature'
ATTRIBUTE_TAG = 'attribute'
COMMAND_TAG = 'command'
CONDITION_TAG = 'condition'
class ConformanceException(Exception):
def __init__(self, msg):
self.msg = msg
def __str__(self):
return f"ConformanceException({self.msg})"
class ConformanceDecision(Enum):
MANDATORY = auto()
OPTIONAL = auto()
NOT_APPLICABLE = auto()
DISALLOWED = auto()
PROVISIONAL = auto()
@dataclass
class ConformanceParseParameters:
feature_map: dict[str, uint]
attribute_map: dict[str, uint]
command_map: dict[str, uint]
def conformance_allowed(conformance_decision: ConformanceDecision, allow_provisional: bool):
if conformance_decision == ConformanceDecision.NOT_APPLICABLE or conformance_decision == ConformanceDecision.DISALLOWED:
return False
if conformance_decision == ConformanceDecision.PROVISIONAL:
return allow_provisional
return True
def is_disallowed(conformance: Callable):
# Deprecated and disallowed conformances will come back as disallowed regardless of the implemented features / attributes / etc.
return conformance(0, [], []) == ConformanceDecision.DISALLOWED
class zigbee:
def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision:
return ConformanceDecision.NOT_APPLICABLE
def __str__(self):
return "Zigbee"
class mandatory:
def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision:
return ConformanceDecision.MANDATORY
def __str__(self):
return 'M'
class optional:
def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision:
return ConformanceDecision.OPTIONAL
def __str__(self):
return 'O'
class deprecated:
def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision:
return ConformanceDecision.DISALLOWED
def __str__(self):
return 'D'
class disallowed:
def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision:
return ConformanceDecision.DISALLOWED
def __str__(self):
return 'X'
class provisional:
def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision:
return ConformanceDecision.PROVISIONAL
def __str__(self):
return 'P'
class feature:
def __init__(self, requiredFeature: uint, code: str):
self.requiredFeature = requiredFeature
self.code = code
def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision:
if self.requiredFeature & feature_map != 0:
return ConformanceDecision.MANDATORY
return ConformanceDecision.NOT_APPLICABLE
def __str__(self):
return f'{self.code}'
class attribute:
def __init__(self, requiredAttribute: uint, name: str):
self.requiredAttribute = requiredAttribute
self.name = name
def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision:
if self.requiredAttribute in attribute_list:
return ConformanceDecision.MANDATORY
return ConformanceDecision.NOT_APPLICABLE
def __str__(self):
return f'{self.name}'
class command:
def __init__(self, requiredCommand: uint, name: str):
self.requiredCommand = requiredCommand
self.name = name
def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision:
if self.requiredCommand in all_command_list:
return ConformanceDecision.MANDATORY
return ConformanceDecision.NOT_APPLICABLE
def __str__(self):
return f'{self.name}'
def strip_outer_parentheses(inner: str) -> str:
if inner[0] == '(' and inner[-1] == ')':
return inner[1:-1]
return inner
class optional_wrapper:
def __init__(self, op: Callable):
self.op = op
def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision:
decision = self.op(feature_map, attribute_list, all_command_list)
if decision == ConformanceDecision.MANDATORY or decision == ConformanceDecision.OPTIONAL:
return ConformanceDecision.OPTIONAL
elif decision == ConformanceDecision.NOT_APPLICABLE:
return ConformanceDecision.NOT_APPLICABLE
else:
raise ConformanceException(f'Optional wrapping invalid op {decision}')
def __str__(self):
return f'[{strip_outer_parentheses(str(self.op))}]'
class mandatory_wrapper:
def __init__(self, op: Callable):
self.op = op
def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision:
return self.op(feature_map, attribute_list, all_command_list)
def __str__(self):
return strip_outer_parentheses(str(self.op))
class not_operation:
def __init__(self, op: Callable):
self.op = op
def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision:
# not operations can't be used with anything that returns DISALLOWED
# not operations also can't be used with things that are optional
# ie, ![AB] doesn't make sense, nor does !O
decision = self.op(feature_map, attribute_list, all_command_list)
if decision == ConformanceDecision.OPTIONAL or decision == ConformanceDecision.DISALLOWED or decision == ConformanceDecision.PROVISIONAL:
raise ConformanceException('NOT operation on optional or disallowed item')
elif decision == ConformanceDecision.NOT_APPLICABLE:
return ConformanceDecision.MANDATORY
elif decision == ConformanceDecision.MANDATORY:
return ConformanceDecision.NOT_APPLICABLE
else:
raise ConformanceException('NOT called on item with non-conformance value')
def __str__(self):
return f'!{str(self.op)}'
class and_operation:
def __init__(self, op_list: list[Callable]):
self.op_list = op_list
def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision:
for op in self.op_list:
decision = op(feature_map, attribute_list, all_command_list)
# and operations can't happen on optional or disallowed
if decision == ConformanceDecision.OPTIONAL or decision == ConformanceDecision.DISALLOWED or decision == ConformanceDecision.PROVISIONAL:
raise ConformanceException('AND operation on optional or disallowed item')
elif decision == ConformanceDecision.NOT_APPLICABLE:
return ConformanceDecision.NOT_APPLICABLE
elif decision == ConformanceDecision.MANDATORY:
continue
else:
raise ConformanceException('Oplist item returned non-conformance value')
return ConformanceDecision.MANDATORY
def __str__(self):
op_strs = [str(op) for op in self.op_list]
return f'({" & ".join(op_strs)})'
class or_operation:
def __init__(self, op_list: list[Callable]):
self.op_list = op_list
def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision:
for op in self.op_list:
decision = op(feature_map, attribute_list, all_command_list)
if decision == ConformanceDecision.DISALLOWED or decision == ConformanceDecision.PROVISIONAL:
raise ConformanceException('OR operation on optional or disallowed item')
elif decision == ConformanceDecision.NOT_APPLICABLE:
continue
elif decision == ConformanceDecision.MANDATORY:
return ConformanceDecision.MANDATORY
elif decision == ConformanceDecision.OPTIONAL:
return ConformanceDecision.OPTIONAL
else:
raise ConformanceException('Oplist item returned non-conformance value')
return ConformanceDecision.NOT_APPLICABLE
def __str__(self):
op_strs = [str(op) for op in self.op_list]
return f'({" | ".join(op_strs)})'
# TODO: add xor operation once it's required
# TODO: how would equal and unequal operations work here?
class otherwise:
def __init__(self, op_list: list[Callable]):
self.op_list = op_list
def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_list: list[uint]) -> ConformanceDecision:
# Otherwise operations apply from left to right. If any of them
# has a definite decision (optional, mandatory or disallowed), that is the one that applies
# Provisional items are meant to be marked as the first item in the list
# Deprecated items are either on their own, or follow an O as O,D.
# For O,D, optional applies (leftmost), but we should consider some way to warn here as well,
# possibly in another function
for op in self.op_list:
decision = op(feature_map, attribute_list, all_command_list)
if decision == ConformanceDecision.NOT_APPLICABLE:
continue
return decision
return ConformanceDecision.NOT_APPLICABLE
def __str__(self):
op_strs = [strip_outer_parentheses(str(op)) for op in self.op_list]
return ', '.join(op_strs)
def parse_callable_from_xml(element: ElementTree.Element, params: ConformanceParseParameters) -> Callable:
if len(list(element)) == 0:
# no subchildren here, so this can only be mandatory, optional, provisional, deprecated, disallowed, feature or attribute
if element.tag == MANDATORY_CONFORM:
return mandatory()
elif element.tag == OPTIONAL_CONFORM:
return optional()
elif element.tag == PROVISIONAL_CONFORM:
return provisional()
elif element.tag == DEPRECATE_CONFORM:
return deprecated()
elif element.tag == DISALLOW_CONFORM:
return disallowed()
elif element.tag == FEATURE_TAG:
try:
return feature(params.feature_map[element.get('name')], element.get('name'))
except KeyError:
raise ConformanceException(f'Conformance specifies feature not in feature table: {element.get("name")}')
elif element.tag == ATTRIBUTE_TAG:
# Some command conformance tags are marked as attribute, so if this key isn't in attribute, try command
name = element.get('name')
if name in params.attribute_map:
return attribute(params.attribute_map[name], name)
elif name in params.command_map:
return command(params.command_map[name], name)
else:
raise ConformanceException(f'Conformance specifies attribute or command not in table: {name}')
elif element.tag == COMMAND_TAG:
return command(params.command_map[element.get('name')], element.get('name'))
elif element.tag == CONDITION_TAG and element.get('name').lower() == 'zigbee':
return zigbee()
else:
raise ConformanceException(
f'Unexpected xml conformance element with no children {str(element.tag)} {str(element.attrib)}')
# First build the list, then create the callable for this element
ops = []
for sub in element:
ops.append(parse_callable_from_xml(sub, params))
# optional can be a wrapper as well as a standalone
# This can be any of the boolean operations, optional or otherwise
if element.tag == OPTIONAL_CONFORM:
if len(ops) > 1:
raise ConformanceException(f'OPTIONAL term found with more than one subelement {list(element)}')
return optional_wrapper(ops[0])
elif element.tag == MANDATORY_CONFORM:
if len(ops) > 1:
raise ConformanceException(f'MANDATORY term found with more than one subelement {list(element)}')
return mandatory_wrapper(ops[0])
elif element.tag == AND_TERM:
return and_operation(ops)
elif element.tag == OR_TERM:
return or_operation(ops)
elif element.tag == NOT_TERM:
if len(ops) > 1:
raise ConformanceException(f'NOT term found with more than one subelement {list(element)}')
return not_operation(ops[0])
elif element.tag == OTHERWISE_CONFORM:
return otherwise(ops)
else:
raise ConformanceException(f'Unexpected conformance tag with children {element}')