1#
2# Copyright (c), 2016-2021  , SISSA (International School for Advanced Studies).
3# All rights reserved.
4# This file is distributed under the terms of the MIT License.
5# See the file 'LICENSE' in the root directory of the present
6# distribution, or http://opensource.org/licenses/MIT.
7#
8# @author Davide Brunato <brunato@sissa.it>
9#
10from typing import Any, Optional, Tuple, Union
11
12from ..exceptions import XMLSchemaValueError
13from ..aliases import ElementType, ModelParticleType
14
15
16class ParticleMixin:
17    """
18    Mixin for objects related to XSD Particle Schema Components:
19
20      https://www.w3.org/TR/2012/REC-xmlschema11-1-20120405/structures.html#p
21      https://www.w3.org/TR/2012/REC-xmlschema11-1-20120405/structures.html#t
22
23    :ivar min_occurs: the minOccurs property of the XSD particle. Defaults to 1.
24    :ivar max_occurs: the maxOccurs property of the XSD particle. Defaults to 1, \
25    a `None` value means 'unbounded'.
26    """
27    name: Any
28    maps: Any
29
30    min_occurs: int = 1
31    max_occurs: Optional[int] = 1
32
33    def __init__(self, min_occurs: int = 1, max_occurs: Optional[int] = 1) -> None:
34        self.min_occurs = min_occurs
35        self.max_occurs = max_occurs
36
37    @property
38    def occurs(self) -> Tuple[int, Optional[int]]:
39        return self.min_occurs, self.max_occurs
40
41    @property
42    def effective_min_occurs(self) -> int:
43        """
44        A property calculated from minOccurs, that is equal to minOccurs
45        for elements and may vary for content model groups, in dependance
46        of group model and structure.
47        """
48        return self.min_occurs
49
50    @property
51    def effective_max_occurs(self) -> Optional[int]:
52        """
53        A property calculated from maxOccurs, that is equal to maxOccurs
54        for elements and may vary for content model groups, in dependance
55        of group model and structure. Used for checking restrictions of
56        xs:choice model groups in XSD 1.1.
57        """
58        return self.max_occurs
59
60    def is_emptiable(self) -> bool:
61        """
62        Tests if max_occurs == 0. A zero-length model group is considered emptiable.
63        For model groups the test outcome depends also on nested particles.
64        """
65        return self.min_occurs == 0
66
67    def is_empty(self) -> bool:
68        """
69        Tests if max_occurs == 0. A zero-length model group is considered empty.
70        """
71        return self.max_occurs == 0
72
73    def is_single(self) -> bool:
74        """
75        Tests if the particle has max_occurs == 1. For elements the test
76        outcome depends also on parent group. For model groups the test
77        outcome depends also on nested model groups.
78        """
79        return self.max_occurs == 1
80
81    def is_multiple(self) -> bool:
82        """Tests the particle can have multiple occurrences."""
83        return not self.is_empty() and not self.is_single()
84
85    def is_ambiguous(self) -> bool:
86        """Tests if min_occurs != max_occurs."""
87        return self.min_occurs != self.max_occurs
88
89    def is_univocal(self) -> bool:
90        """Tests if min_occurs == max_occurs."""
91        return self.min_occurs == self.max_occurs
92
93    def is_missing(self, occurs: int) -> bool:
94        """Tests if provided occurrences are under the minimum."""
95        return not self.is_emptiable() if occurs == 0 else self.min_occurs > occurs
96
97    def is_over(self, occurs: int) -> bool:
98        """Tests if provided occurrences are over the maximum."""
99        return self.max_occurs is not None and self.max_occurs <= occurs
100
101    def has_occurs_restriction(self, other: Union[ModelParticleType, 'OccursCalculator']) -> bool:
102        if self.min_occurs < other.min_occurs:
103            return False
104        elif self.max_occurs == 0:
105            return True
106        elif other.max_occurs is None:
107            return True
108        elif self.max_occurs is None:
109            return False
110        else:
111            return self.max_occurs <= other.max_occurs
112
113    def parse_error(self, message: Any) -> None:
114        raise XMLSchemaValueError(message)
115
116    def _parse_particle(self, elem: ElementType) -> None:
117        if 'minOccurs' in elem.attrib:
118            try:
119                min_occurs = int(elem.attrib['minOccurs'])
120            except (TypeError, ValueError):
121                self.parse_error("minOccurs value is not an integer value")
122            else:
123                if min_occurs < 0:
124                    self.parse_error("minOccurs value must be a non negative integer")
125                else:
126                    self.min_occurs = min_occurs
127
128        max_occurs = elem.get('maxOccurs')
129        if max_occurs is None:
130            if self.min_occurs > 1:
131                self.parse_error("minOccurs must be lesser or equal than maxOccurs")
132        elif max_occurs == 'unbounded':
133            self.max_occurs = None
134        else:
135            try:
136                self.max_occurs = int(max_occurs)
137            except ValueError:
138                self.parse_error("maxOccurs value must be a non negative integer or 'unbounded'")
139            else:
140                if self.min_occurs > self.max_occurs:
141                    self.parse_error("maxOccurs must be 'unbounded' or greater than minOccurs")
142                    self.max_occurs = None
143
144
145class OccursCalculator:
146    """
147    An helper class for adding and multiplying min/max occurrences of XSD particles.
148    """
149    min_occurs: int
150    max_occurs: Optional[int]
151
152    def __init__(self) -> None:
153        self.min_occurs = self.max_occurs = 0
154
155    def __repr__(self) -> str:
156        return '%s(%r, %r)' % (self.__class__.__name__, self.min_occurs, self.max_occurs)
157
158    def __add__(self, other: ParticleMixin) -> 'OccursCalculator':
159        self.min_occurs += other.min_occurs
160        if self.max_occurs is not None:
161            if other.max_occurs is None:
162                self.max_occurs = None
163            else:
164                self.max_occurs += other.max_occurs
165        return self
166
167    def __mul__(self, other: ParticleMixin) -> 'OccursCalculator':
168        self.min_occurs *= other.min_occurs
169        if self.max_occurs is None:
170            if other.max_occurs == 0:
171                self.max_occurs = 0
172        elif other.max_occurs is None:
173            if self.max_occurs != 0:
174                self.max_occurs = None
175        else:
176            self.max_occurs *= other.max_occurs
177        return self
178
179    def reset(self) -> None:
180        self.min_occurs = self.max_occurs = 0
181