lexcql-parser 1.2.0__tar.gz → 1.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lexcql_parser-1.2.0 → lexcql_parser-1.3.0}/PKG-INFO +2 -1
- {lexcql_parser-1.2.0 → lexcql_parser-1.3.0}/README.md +1 -0
- {lexcql_parser-1.2.0 → lexcql_parser-1.3.0}/pyproject.toml +1 -1
- {lexcql_parser-1.2.0 → lexcql_parser-1.3.0}/src/lexcql/__init__.py +54 -6
- {lexcql_parser-1.2.0 → lexcql_parser-1.3.0}/src/lexcql/parser.py +10 -1
- {lexcql_parser-1.2.0 → lexcql_parser-1.3.0}/src/lexcql/validation.py +191 -32
- {lexcql_parser-1.2.0 → lexcql_parser-1.3.0}/src/lexcql/LexLexer.g4 +0 -0
- {lexcql_parser-1.2.0 → lexcql_parser-1.3.0}/src/lexcql/LexLexer.interp +0 -0
- {lexcql_parser-1.2.0 → lexcql_parser-1.3.0}/src/lexcql/LexLexer.py +0 -0
- {lexcql_parser-1.2.0 → lexcql_parser-1.3.0}/src/lexcql/LexLexer.tokens +0 -0
- {lexcql_parser-1.2.0 → lexcql_parser-1.3.0}/src/lexcql/LexParser.g4 +0 -0
- {lexcql_parser-1.2.0 → lexcql_parser-1.3.0}/src/lexcql/LexParser.interp +0 -0
- {lexcql_parser-1.2.0 → lexcql_parser-1.3.0}/src/lexcql/LexParser.py +0 -0
- {lexcql_parser-1.2.0 → lexcql_parser-1.3.0}/src/lexcql/LexParser.tokens +0 -0
- {lexcql_parser-1.2.0 → lexcql_parser-1.3.0}/src/lexcql/LexParserListener.py +0 -0
- {lexcql_parser-1.2.0 → lexcql_parser-1.3.0}/src/lexcql/LexParserVisitor.py +0 -0
- {lexcql_parser-1.2.0 → lexcql_parser-1.3.0}/src/lexcql/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lexcql-parser
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: LexCQL Query Grammar and Parser
|
|
5
5
|
Keywords: LexCQL,FCS,CQL,Query Parser
|
|
6
6
|
Author: Erik Körner
|
|
@@ -141,6 +141,7 @@ assert error.message == "Unknown index 'post'!"
|
|
|
141
141
|
assert error.fragment == "post = NOUN"
|
|
142
142
|
assert error.position.start == 0
|
|
143
143
|
assert error.position.stop == 11
|
|
144
|
+
assert error.type == "validation-error"
|
|
144
145
|
```
|
|
145
146
|
|
|
146
147
|
## Development
|
|
@@ -32,6 +32,18 @@ class ExceptionThrowingErrorListener(ErrorListener):
|
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
def antlr_parse(input: str) -> LexParser.QueryContext:
|
|
35
|
+
"""Run the low-level ANTLR4 tokenization and parsing. This returns the parsing
|
|
36
|
+
context ANTLR4 uses instead of the simplified LexCQL query node types.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
input: raw query string
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
LexParser.QueryContext: the ANTLR4 root parsing context (query)
|
|
43
|
+
|
|
44
|
+
Throws:
|
|
45
|
+
SyntaxError: if an error occurred while parsing
|
|
46
|
+
"""
|
|
35
47
|
input_stream = InputStream(input)
|
|
36
48
|
lexer = LexLexer(input_stream)
|
|
37
49
|
stream = CommonTokenStream(lexer)
|
|
@@ -78,12 +90,17 @@ def can_parse(input: str):
|
|
|
78
90
|
return False
|
|
79
91
|
|
|
80
92
|
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
|
|
81
96
|
@overload
|
|
82
97
|
def validate(
|
|
83
98
|
input: str,
|
|
84
99
|
*,
|
|
85
100
|
version: str = DEFAULT_VALIDATOR_SPECIFICATION_VERSION,
|
|
86
101
|
return_errors: Literal[False] = False,
|
|
102
|
+
case_insensitive: bool = True,
|
|
103
|
+
warnings_as_errors: bool = False,
|
|
87
104
|
) -> bool: ...
|
|
88
105
|
|
|
89
106
|
|
|
@@ -93,6 +110,8 @@ def validate(
|
|
|
93
110
|
*,
|
|
94
111
|
version: str = DEFAULT_VALIDATOR_SPECIFICATION_VERSION,
|
|
95
112
|
return_errors: Literal[True] = True,
|
|
113
|
+
case_insensitive: bool = True,
|
|
114
|
+
warnings_as_errors: bool = False,
|
|
96
115
|
) -> List[ErrorDetail]: ...
|
|
97
116
|
|
|
98
117
|
|
|
@@ -101,7 +120,30 @@ def validate(
|
|
|
101
120
|
*,
|
|
102
121
|
version: str = DEFAULT_VALIDATOR_SPECIFICATION_VERSION,
|
|
103
122
|
return_errors: bool = False,
|
|
123
|
+
case_insensitive: bool = True,
|
|
124
|
+
warnings_as_errors: bool = False,
|
|
104
125
|
):
|
|
126
|
+
"""Validate input query string by trying to parse it and if successful run a LexCQL
|
|
127
|
+
specification validation. Collect errors/warnings.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
input: the raw query input string
|
|
131
|
+
version: the specification version to validate against.
|
|
132
|
+
Defaults to DEFAULT_VALIDATOR_SPECIFICATION_VERSION ("0.3").
|
|
133
|
+
return_errors: whether to return simply a boolean if valid or a list of errors.
|
|
134
|
+
Defaults to False.
|
|
135
|
+
case_insensitive: how to handle keywords/indexes in LexCQL. Defaults to True.
|
|
136
|
+
warnings_as_errors: handle warnings as errors. Defaults to False.
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
ValueError: raised if ``version`` argument specifies an unknown LexCQL specification
|
|
140
|
+
or no ``Validator`` can be found for this version.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
bool: if ``return_errors`` is ``False`` only return a boolean.
|
|
144
|
+
Returns ``True`` if parsing and validation is without issues, ``False`` otherwise.
|
|
145
|
+
List[ErrorDetail]: if ``return_errors`` is ``True`` return a list of errors AND warnings.
|
|
146
|
+
"""
|
|
105
147
|
# "check" params
|
|
106
148
|
validator_cls = VALIDATORS.get(version, None)
|
|
107
149
|
if validator_cls is None:
|
|
@@ -109,7 +151,12 @@ def validate(
|
|
|
109
151
|
|
|
110
152
|
# create parser/validator
|
|
111
153
|
parser = QueryParser(enableSourceLocations=True)
|
|
112
|
-
validator = validator_cls(
|
|
154
|
+
validator = validator_cls(
|
|
155
|
+
query=input,
|
|
156
|
+
case_insensitive=case_insensitive,
|
|
157
|
+
raise_at_first_violation=not return_errors,
|
|
158
|
+
warnings_as_errors=warnings_as_errors,
|
|
159
|
+
)
|
|
113
160
|
|
|
114
161
|
# try to parse the input query string
|
|
115
162
|
try:
|
|
@@ -128,14 +175,15 @@ def validate(
|
|
|
128
175
|
try:
|
|
129
176
|
validator.validate(qn)
|
|
130
177
|
except SpecificationValidationError:
|
|
178
|
+
# not will raise if raise_at_first_violation enabled
|
|
131
179
|
return False
|
|
132
180
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
181
|
+
errors = list()
|
|
182
|
+
errors.extend(validator.errors)
|
|
183
|
+
if warnings_as_errors:
|
|
184
|
+
errors.extend(validator.warnings)
|
|
137
185
|
|
|
138
|
-
return
|
|
186
|
+
return errors if return_errors else not bool(errors)
|
|
139
187
|
|
|
140
188
|
|
|
141
189
|
# ---------------------------------------------------------------------------
|
|
@@ -32,7 +32,9 @@ from lexcql.LexParserVisitor import LexParserVisitor
|
|
|
32
32
|
LOGGER = logging.getLogger(__name__)
|
|
33
33
|
|
|
34
34
|
_T = TypeVar("_T", bound="QueryNode")
|
|
35
|
+
"""Type of ``QueryNode``."""
|
|
35
36
|
_R = TypeVar("_R")
|
|
37
|
+
"""Result type for ``QueryVisitor``."""
|
|
36
38
|
|
|
37
39
|
# ---------------------------------------------------------------------------
|
|
38
40
|
|
|
@@ -640,10 +642,17 @@ class Subquery(QueryNode):
|
|
|
640
642
|
|
|
641
643
|
@dataclass(frozen=True)
|
|
642
644
|
class ErrorDetail:
|
|
645
|
+
"""Wrapper for error or warnings messages to include the type of issue
|
|
646
|
+
with optional position and query string fragment."""
|
|
647
|
+
|
|
643
648
|
message: str
|
|
644
|
-
|
|
649
|
+
"""the error or warning message"""
|
|
650
|
+
type: Optional[Union[Literal["syntax-error", "validation-error", "validation-warning"], str]] = None
|
|
651
|
+
"""the type of error or warning"""
|
|
645
652
|
position: Optional[Union[int, SourceLocation]] = None
|
|
653
|
+
"""optional position information in the raw query string"""
|
|
646
654
|
fragment: Optional[str] = None
|
|
655
|
+
"""optional query string fragment, may be a substring to quickly locate the issue"""
|
|
647
656
|
|
|
648
657
|
|
|
649
658
|
class ErrorListener(antlr4.error.ErrorListener.ErrorListener):
|
|
@@ -4,6 +4,9 @@ from typing import Deque
|
|
|
4
4
|
from typing import Dict
|
|
5
5
|
from typing import List
|
|
6
6
|
from typing import Optional
|
|
7
|
+
from typing import Set
|
|
8
|
+
from typing import Type
|
|
9
|
+
from typing import TypeVar
|
|
7
10
|
|
|
8
11
|
from lexcql.parser import ErrorDetail
|
|
9
12
|
from lexcql.parser import Modifier
|
|
@@ -11,6 +14,12 @@ from lexcql.parser import QueryNode
|
|
|
11
14
|
from lexcql.parser import QueryVisitorAdapter
|
|
12
15
|
from lexcql.parser import Relation
|
|
13
16
|
from lexcql.parser import SearchClause
|
|
17
|
+
from lexcql.parser import SearchClauseGroup
|
|
18
|
+
from lexcql.parser import Subquery
|
|
19
|
+
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
_R = TypeVar("_R")
|
|
14
23
|
|
|
15
24
|
# ---------------------------------------------------------------------------
|
|
16
25
|
|
|
@@ -25,12 +34,13 @@ class SpecificationValidationError(Exception):
|
|
|
25
34
|
# TODO: create custom error classes for each error type? or add some type id?
|
|
26
35
|
|
|
27
36
|
|
|
28
|
-
class Validator(QueryVisitorAdapter[
|
|
37
|
+
class Validator(QueryVisitorAdapter[_R], metaclass=ABCMeta):
|
|
29
38
|
"""An abstract base Validator for LexCQL queries.
|
|
30
39
|
|
|
31
40
|
Subclasses should override the ``visit_`` QueryNode methods to
|
|
32
41
|
implement the validation logic and use the ``.validation_error()``
|
|
33
|
-
to raise/track validation errors
|
|
42
|
+
to raise/track validation errors and ``.validation_warning()`` for
|
|
43
|
+
warnings (may also raise errors depending on configuration).
|
|
34
44
|
"""
|
|
35
45
|
|
|
36
46
|
def __init__(
|
|
@@ -39,6 +49,7 @@ class Validator(QueryVisitorAdapter[None], metaclass=ABCMeta):
|
|
|
39
49
|
query: Optional[str] = None,
|
|
40
50
|
case_insensitive: bool = True,
|
|
41
51
|
raise_at_first_violation: bool = True,
|
|
52
|
+
warnings_as_errors: bool = False,
|
|
42
53
|
):
|
|
43
54
|
"""Creates a LexCQL Validator that checks that only known indexes are used
|
|
44
55
|
and that relations and relation modifiers are valid.
|
|
@@ -52,9 +63,13 @@ class Validator(QueryVisitorAdapter[None], metaclass=ABCMeta):
|
|
|
52
63
|
conformance validation or try to gather as many infos
|
|
53
64
|
as possible. Will be available in ``.errors`` attribute.
|
|
54
65
|
Defaults to True.
|
|
66
|
+
warnings_as_errors: Handle warnings the same as errors. May raise ``SpecificationValidationError``.
|
|
55
67
|
"""
|
|
56
68
|
super().__init__()
|
|
57
69
|
|
|
70
|
+
self.stack: Deque[QueryNode] = deque()
|
|
71
|
+
"""(internal) Query node stack to keep track of query node parents."""
|
|
72
|
+
|
|
58
73
|
self.query = query
|
|
59
74
|
"""query string to add context to error messages for better error locations"""
|
|
60
75
|
|
|
@@ -63,8 +78,13 @@ class Validator(QueryVisitorAdapter[None], metaclass=ABCMeta):
|
|
|
63
78
|
|
|
64
79
|
self.raise_at_first_violation = raise_at_first_violation
|
|
65
80
|
"""Whether to raise at first violation."""
|
|
81
|
+
self.warnings_as_errors = warnings_as_errors
|
|
82
|
+
"""Whether to handle warnings the same as errors."""
|
|
66
83
|
self.errors: List[ErrorDetail] = list()
|
|
67
84
|
"""List of specification validation errors if ``.raise_at_first_violation`` is ``False``"""
|
|
85
|
+
self.warnings: List[ErrorDetail] = list()
|
|
86
|
+
"""List of warnings about not-quite-violations of the specification but bad practice or
|
|
87
|
+
what can be unexpected results."""
|
|
68
88
|
|
|
69
89
|
def validate(self, node: QueryNode, *, query: Optional[str] = None):
|
|
70
90
|
"""Validate parse query node tree.
|
|
@@ -112,12 +132,12 @@ class Validator(QueryVisitorAdapter[None], metaclass=ABCMeta):
|
|
|
112
132
|
except SpecificationValidationError:
|
|
113
133
|
return False
|
|
114
134
|
|
|
115
|
-
def validation_error(self, node: QueryNode,
|
|
116
|
-
"""(Internal) Raises or tracks a new validation
|
|
135
|
+
def validation_error(self, node: QueryNode, message: str):
|
|
136
|
+
"""(Internal) Raises or tracks a new validation error.
|
|
117
137
|
|
|
118
138
|
Args:
|
|
119
139
|
node: the query node where the validation error was caused
|
|
120
|
-
|
|
140
|
+
message: the error message
|
|
121
141
|
|
|
122
142
|
Raises:
|
|
123
143
|
SpecificationValidationError: the raised error if ``.raise_at_first_violation``
|
|
@@ -128,22 +148,98 @@ class Validator(QueryVisitorAdapter[None], metaclass=ABCMeta):
|
|
|
128
148
|
fragment = self.query[node.location.start : node.location.stop] # noqa: E203
|
|
129
149
|
|
|
130
150
|
if self.raise_at_first_violation:
|
|
131
|
-
raise SpecificationValidationError(
|
|
151
|
+
raise SpecificationValidationError(message, node, fragment)
|
|
132
152
|
|
|
133
153
|
self.errors.append(
|
|
134
154
|
ErrorDetail(
|
|
135
|
-
message=
|
|
155
|
+
message=message,
|
|
136
156
|
type="validation-error",
|
|
137
157
|
position=node.location,
|
|
138
158
|
fragment=fragment,
|
|
139
159
|
)
|
|
140
160
|
)
|
|
141
161
|
|
|
162
|
+
def validation_warning(self, node: QueryNode, message: str):
|
|
163
|
+
"""(Internal) Tracks validation warnings.
|
|
164
|
+
|
|
165
|
+
If ``.warnings_as_errors`` is ``True`` and ``.raise_at_first_violation``
|
|
166
|
+
is ``True``, it will raise ``SpecificationValidationError``.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
node: the query node where the validation error was caused
|
|
170
|
+
message: the error message
|
|
171
|
+
|
|
172
|
+
Raises:
|
|
173
|
+
SpecificationValidationError: the raised error if ``.raise_at_first_violation``
|
|
174
|
+
and ``.warnings_as_errors`` are set to ``True``
|
|
175
|
+
"""
|
|
176
|
+
fragment = None
|
|
177
|
+
if self.query and node.location:
|
|
178
|
+
fragment = self.query[node.location.start : node.location.stop] # noqa: E203
|
|
179
|
+
|
|
180
|
+
if self.warnings_as_errors:
|
|
181
|
+
if self.raise_at_first_violation:
|
|
182
|
+
raise SpecificationValidationError(message, node, fragment)
|
|
183
|
+
|
|
184
|
+
self.warnings.append(
|
|
185
|
+
ErrorDetail(
|
|
186
|
+
message=message,
|
|
187
|
+
type="validation-warning",
|
|
188
|
+
position=node.location,
|
|
189
|
+
fragment=fragment,
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# ----------------------------------------------------
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def parent_node(self) -> Optional[QueryNode]:
|
|
197
|
+
"""Get the current parent ``QueryNode`` or ``None`` if unavailable.
|
|
198
|
+
|
|
199
|
+
Intended to be used in the ``visit_`` ``QueryNode`` handlers.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Optional[QueryNode]: the parent query node or ``None``
|
|
203
|
+
"""
|
|
204
|
+
if self.stack:
|
|
205
|
+
return self.stack[-1]
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
def visit_Subquery(self, node: Subquery) -> _R:
|
|
209
|
+
self.stack.append(node)
|
|
210
|
+
result = super().visit_Subquery(node)
|
|
211
|
+
self.stack.pop()
|
|
212
|
+
return result
|
|
213
|
+
|
|
214
|
+
def visit_SearchClauseGroup(self, node: SearchClauseGroup) -> _R:
|
|
215
|
+
self.stack.append(node)
|
|
216
|
+
result = super().visit_SearchClauseGroup(node)
|
|
217
|
+
self.stack.pop()
|
|
218
|
+
return result
|
|
219
|
+
|
|
220
|
+
def visit_SearchClause(self, node: SearchClause) -> _R:
|
|
221
|
+
self.stack.append(node)
|
|
222
|
+
result = super().visit_SearchClause(node)
|
|
223
|
+
self.stack.pop()
|
|
224
|
+
return result
|
|
225
|
+
|
|
226
|
+
def visit_Relation(self, node: Relation) -> _R:
|
|
227
|
+
self.stack.append(node)
|
|
228
|
+
result = super().visit_Relation(node)
|
|
229
|
+
self.stack.pop()
|
|
230
|
+
return result
|
|
231
|
+
|
|
232
|
+
def visit_Modifier(self, node: Modifier) -> _R:
|
|
233
|
+
self.stack.append(node)
|
|
234
|
+
result = super().visit_Modifier(node)
|
|
235
|
+
self.stack.pop()
|
|
236
|
+
return result
|
|
237
|
+
|
|
142
238
|
|
|
143
239
|
# ---------------------------------------------------------------------------
|
|
144
240
|
|
|
145
241
|
|
|
146
|
-
class LexCQLValidatorV0_3(Validator):
|
|
242
|
+
class LexCQLValidatorV0_3(Validator[None]):
|
|
147
243
|
"""LexCQL Query Validator for LexCQL Spec v0.3."""
|
|
148
244
|
|
|
149
245
|
SPECIFICATION_VERSION = "0.3"
|
|
@@ -194,14 +290,24 @@ class LexCQLValidatorV0_3(Validator):
|
|
|
194
290
|
"fullMatch",
|
|
195
291
|
]
|
|
196
292
|
"""List of LexCQL relation modifiers"""
|
|
293
|
+
MUTUALLY_EXCLUSIVE_MODIFIERS = [
|
|
294
|
+
# TODO: masked/unmasked/regex ?
|
|
295
|
+
{"masked", "unmasked"},
|
|
296
|
+
{"ignoreCase", "respectCase"},
|
|
297
|
+
{"ignoreAccents", "respectAccents"},
|
|
298
|
+
{"partialMatch", "fullMatch"},
|
|
299
|
+
]
|
|
300
|
+
"""List of mutually exclusive relation modifiers. They should not appear together."""
|
|
197
301
|
|
|
198
302
|
def __init__(
|
|
199
303
|
self,
|
|
200
304
|
*,
|
|
201
305
|
allowed_indexes: Optional[List[str]] = None,
|
|
306
|
+
allowed_modifiers: Optional[List[str]] = None,
|
|
202
307
|
query: Optional[str] = None,
|
|
203
308
|
case_insensitive: bool = True,
|
|
204
309
|
raise_at_first_violation: bool = True,
|
|
310
|
+
warnings_as_errors: bool = False,
|
|
205
311
|
):
|
|
206
312
|
"""Creates a LexCQL v0.3 Validator that checks that only known indexes are used
|
|
207
313
|
and that relations and relation modifiers are valid.
|
|
@@ -209,6 +315,8 @@ class LexCQLValidatorV0_3(Validator):
|
|
|
209
315
|
Args:
|
|
210
316
|
allowed_indexes: Override the default ``KNOWN_INDEXES`` list of LexCQL field names.
|
|
211
317
|
Defaults to None.
|
|
318
|
+
allowed_modifiers: Override the default ``KNOWN_MODIFIERS`` list of LexCQL
|
|
319
|
+
relation modifiers. Defaults to None.
|
|
212
320
|
query: the original query string used for constructing the query node tree.
|
|
213
321
|
Used to provide more context for validation errors. Defaults to None.
|
|
214
322
|
case_insensitive: Whether indexes (field names) are check case-insensitively.
|
|
@@ -217,27 +325,33 @@ class LexCQLValidatorV0_3(Validator):
|
|
|
217
325
|
conformance validation or try to gather as many infos
|
|
218
326
|
as possible. Will be available in ``.errors`` attribute.
|
|
219
327
|
Defaults to True.
|
|
328
|
+
warnings_as_errors: Handle warnings the same as errors. May raise ``SpecificationValidationError``.
|
|
220
329
|
"""
|
|
221
330
|
|
|
222
331
|
super().__init__(
|
|
223
332
|
query=query,
|
|
224
333
|
case_insensitive=case_insensitive,
|
|
225
334
|
raise_at_first_violation=raise_at_first_violation,
|
|
335
|
+
warnings_as_errors=warnings_as_errors,
|
|
226
336
|
)
|
|
227
337
|
|
|
228
|
-
self.stack: Deque[QueryNode] = deque()
|
|
229
|
-
"""(internal) Query node stack to keep track of parents."""
|
|
230
|
-
|
|
231
338
|
self.allowed_indexes = allowed_indexes
|
|
232
339
|
"""User-defined list of known indexes (LexCQL field names)"""
|
|
340
|
+
self.allowed_modifiers = allowed_modifiers
|
|
341
|
+
"""User-defined list of known modifiers (LexCQL relation modifiers)"""
|
|
233
342
|
|
|
234
343
|
if self.case_insensitive:
|
|
235
344
|
# NOTE: overrides on instance (not on class level)
|
|
236
345
|
self.KNOWN_INDEXES = list(map(str.lower, self.KNOWN_INDEXES))
|
|
346
|
+
self.KNOWN_RELATIONS = list(map(str.lower, self.KNOWN_RELATIONS))
|
|
237
347
|
self.KNOWN_MODIFIERS = list(map(str.lower, self.KNOWN_MODIFIERS))
|
|
348
|
+
self.MUTUALLY_EXCLUSIVE_MODIFIERS = [set(map(str.lower, ms)) for ms in self.MUTUALLY_EXCLUSIVE_MODIFIERS]
|
|
238
349
|
|
|
239
350
|
if self.allowed_indexes:
|
|
240
351
|
self.allowed_indexes = list(map(str.lower, self.allowed_indexes))
|
|
352
|
+
if self.allowed_modifiers:
|
|
353
|
+
self.allowed_modifiers = list(map(str.lower, self.allowed_modifiers))
|
|
354
|
+
# TODO: update self.MUTUALLY_EXCLUSIVE_MODIFIERS?
|
|
241
355
|
|
|
242
356
|
# ----------------------------------------------------
|
|
243
357
|
|
|
@@ -257,29 +371,62 @@ class LexCQLValidatorV0_3(Validator):
|
|
|
257
371
|
|
|
258
372
|
# TODO: check `search_term` against relations/modifiers? (regex/masked)
|
|
259
373
|
|
|
260
|
-
|
|
374
|
+
# warn about single quotes (not working being quotes?)
|
|
375
|
+
search_term = node.search_term
|
|
376
|
+
if search_term.startswith("'") and search_term.endswith("'"):
|
|
377
|
+
self.validation_warning(
|
|
378
|
+
node,
|
|
379
|
+
(
|
|
380
|
+
f"Search term {search_term!r} is enclosed with single quotes [']."
|
|
381
|
+
' Single quotes are not used quoting, only double qoutes ["]!'
|
|
382
|
+
" Endpoint may include the literal single quote when searching."
|
|
383
|
+
),
|
|
384
|
+
)
|
|
385
|
+
|
|
261
386
|
super().visit_SearchClause(node)
|
|
262
|
-
self.stack.pop()
|
|
263
387
|
|
|
264
388
|
def visit_Relation(self, node: Relation):
|
|
265
389
|
# parent = self.stack[-1]
|
|
266
390
|
|
|
267
391
|
relation = node.relation
|
|
268
|
-
if
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
self.validation_error(node, f"Relation '{node.relation}' is unspecified!")
|
|
392
|
+
if self.case_insensitive:
|
|
393
|
+
relation = relation.lower()
|
|
394
|
+
if relation not in self.KNOWN_RELATIONS:
|
|
395
|
+
self.validation_error(node, f"Relation '{node.relation}' is unspecified!")
|
|
273
396
|
|
|
274
|
-
|
|
397
|
+
# check modifiers
|
|
398
|
+
if node.modifiers:
|
|
399
|
+
if relation == "is":
|
|
275
400
|
self.validation_error(node, f"Relation '{node.relation}' does not support any modifiers!")
|
|
401
|
+
# TODO: relation == "==", what modifiers make sense here?
|
|
402
|
+
|
|
403
|
+
# check duplicate modifiers (should not be useful in any scenario imaginable)
|
|
404
|
+
modifier_names: Set[str] = set()
|
|
405
|
+
for modifier in node.modifiers:
|
|
406
|
+
modifier_name = modifier.name
|
|
407
|
+
if self.case_insensitive:
|
|
408
|
+
modifier_name = modifier_name.lower()
|
|
409
|
+
if modifier_name in modifier_names:
|
|
410
|
+
self.validation_warning(
|
|
411
|
+
node, f"Relation '{node.relation}' has duplicate modifier '{modifier.name}'?"
|
|
412
|
+
)
|
|
413
|
+
modifier_names.add(modifier_name)
|
|
414
|
+
|
|
415
|
+
# check modifiers that ignore/respect do not appear together
|
|
416
|
+
if len(modifier_names) >= 2:
|
|
417
|
+
for i, excl_set in enumerate(self.MUTUALLY_EXCLUSIVE_MODIFIERS):
|
|
418
|
+
intersection = excl_set & modifier_names
|
|
419
|
+
if len(intersection) > 1:
|
|
420
|
+
self.validation_warning(
|
|
421
|
+
node,
|
|
422
|
+
(
|
|
423
|
+
f"Relation '{node.relation}' uses mutually exclusive modifiers"
|
|
424
|
+
f" {sorted(modifier_names)!r} (not allowed together "
|
|
425
|
+
f"{sorted(LexCQLValidatorV0_3.MUTUALLY_EXCLUSIVE_MODIFIERS[i])!r})!"
|
|
426
|
+
),
|
|
427
|
+
)
|
|
276
428
|
|
|
277
|
-
# TODO: check modifiers that ignore/respect do not appear together
|
|
278
|
-
# TODO: masked/unmasked/regex should not appear together
|
|
279
|
-
|
|
280
|
-
self.stack.append(node)
|
|
281
429
|
super().visit_Relation(node)
|
|
282
|
-
self.stack.pop()
|
|
283
430
|
|
|
284
431
|
def visit_Modifier(self, node: Modifier):
|
|
285
432
|
# parent = self.stack[-1]
|
|
@@ -287,24 +434,36 @@ class LexCQLValidatorV0_3(Validator):
|
|
|
287
434
|
name = node.name
|
|
288
435
|
if self.case_insensitive:
|
|
289
436
|
name = name.lower()
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
437
|
+
if self.allowed_modifiers:
|
|
438
|
+
if name not in self.allowed_modifiers:
|
|
439
|
+
self.validation_error(
|
|
440
|
+
node, f"Unknown modifier '{node.name}' (only allowed: {self.allowed_modifiers!r})!"
|
|
441
|
+
)
|
|
442
|
+
else:
|
|
443
|
+
if name not in self.KNOWN_MODIFIERS:
|
|
444
|
+
self.validation_error(node, f"Modifier '{node.name}' is unspecified!")
|
|
293
445
|
|
|
294
446
|
# check against parent? --> visit_Relation "is" check
|
|
295
447
|
|
|
296
448
|
relation = node.relation
|
|
297
449
|
if relation and name != "lang":
|
|
298
|
-
self.
|
|
450
|
+
if self.allowed_modifiers and name in self.allowed_modifiers and name not in self.KNOWN_MODIFIERS:
|
|
451
|
+
self.validation_warning(node, f"Custom modifier '{node.name}' may not support any extra relation?")
|
|
452
|
+
else:
|
|
453
|
+
self.validation_error(node, f"Modifier '{node.name}' does not support any extra relation!")
|
|
454
|
+
|
|
455
|
+
if not relation and name == "lang":
|
|
456
|
+
self.validation_error(node, f"Modifier '{node.name}' requires a relation value, e.g. 'lang=deu'.")
|
|
457
|
+
|
|
458
|
+
if relation and relation != "=":
|
|
459
|
+
self.validation_error(node, f"Modifier '{node.name}' uses unspecified relation: {relation!r}!")
|
|
299
460
|
|
|
300
|
-
|
|
301
|
-
# TODO: do a valid language code check
|
|
302
|
-
pass
|
|
461
|
+
# TODO: check valid `node.value` for lang modifier?
|
|
303
462
|
|
|
304
463
|
|
|
305
464
|
# ---------------------------------------------------------------------------
|
|
306
465
|
|
|
307
|
-
VALIDATORS: Dict[str, Validator] = {
|
|
466
|
+
VALIDATORS: Dict[str, Type[Validator]] = {
|
|
308
467
|
LexCQLValidatorV0_3.SPECIFICATION_VERSION: LexCQLValidatorV0_3,
|
|
309
468
|
}
|
|
310
469
|
"""Mapping of all known LexCQL Validators. Uses the LexCQL specification
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|