Skip to content

Commit 0093e24

Browse files
authored
Merge pull request #258 from crim-ca/fix-not-keyword-literal-input
fix invalid Not keyword resolution
2 parents 691c8b8 + a13dd93 commit 0093e24

File tree

5 files changed

+84
-8
lines changed

5 files changed

+84
-8
lines changed

CHANGES.rst

+4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ Fixes:
2424
- Fix default execution mode specification in process job control options
2525
(fixes `#182 <https://github.com/opengeospatial/ogcapi-processes/pull/182>`_).
2626
- Fix old OGC-API WPS REST bindings link in landing page for the more recent `OGC-API Processes` specification.
27+
- Fix invalid deserialization of schemas using ``not`` keyword that would result in all fields returned instead of
28+
limiting them to the expected fields from the schema definitions for ``LiteralInputType`` in process description.
29+
- Adjust ``InputType`` and ``OutputType`` schemas to use ``allOf`` instead of ``anyOf`` definition since all sub-schemas
30+
that define them must be combined, with their respectively required or optional fields.
2731

2832
`3.1.0 <https://github.com/crim-ca/weaver/tree/3.1.0>`_ (2021-04-23)
2933
========================================================================

tests/functional/test_wps_package.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,15 @@
4848
IANA_ZIP = IANA_NAMESPACE + ":" + CONTENT_TYPE_APP_ZIP # noqa # pylint: disable=unused-variable
4949

5050
KNOWN_PROCESS_DESCRIPTION_FIELDS = {
51-
"id", "title", "abstract", "inputs", "outputs", "executeEndpoint", "keywords", "metadata", "visibility"
51+
"id", "title", "abstract", "keywords", "metadata", "inputs", "outputs", "executeEndpoint", "visibility"
5252
}
53+
# intersection of fields in InputType and specific sub-schema LiteralInputType
54+
KNOWN_PROCESS_DESCRIPTION_INPUT_DATA_FIELDS = {
55+
"id", "title", "abstract", "keywords", "metadata", "links", "literalDataDomains", "additionalParameters",
56+
"minOccurs", "maxOccurs"
57+
}
58+
# corresponding schemas of input, but min/max occurs not expected
59+
KNOWN_PROCESS_DESCRIPTION_OUTPUT_DATA_FIELDS = KNOWN_PROCESS_DESCRIPTION_INPUT_DATA_FIELDS - {"minOccurs", "maxOccurs"}
5360

5461
LOGGER = logging.getLogger(__name__)
5562

@@ -134,6 +141,11 @@ def test_literal_io_from_package(self):
134141
assert "maxOccurs" not in desc["process"]["outputs"][0]
135142
assert "format" not in desc["process"]["outputs"][0]
136143
assert len(set(desc["process"].keys()) - KNOWN_PROCESS_DESCRIPTION_FIELDS) == 0
144+
# make sure that deserialization of literal fields did not produce over-verbose metadata
145+
for p_input in desc["process"]["inputs"]:
146+
assert len(set(p_input) - KNOWN_PROCESS_DESCRIPTION_INPUT_DATA_FIELDS) == 0
147+
for p_output in desc["process"]["outputs"]:
148+
assert len(set(p_output) - KNOWN_PROCESS_DESCRIPTION_OUTPUT_DATA_FIELDS) == 0
137149

138150
def test_literal_io_from_package_and_offering(self):
139151
"""

tests/wps_restapi/test_colander_extras.py

+43
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,49 @@ class ObjOneOf(ce.OneOfKeywordSchema):
105105
.format(ce._get_node_name(test_schema), test_value, result))
106106

107107

108+
def test_not_keyword_extra_fields_handling():
109+
"""
110+
Using ``not`` keyword without any other schemas must return an empty mapping with additional fields dropped.
111+
When providing other schemas, only fields in those inherited definitions should remain.
112+
In should raise when matching the ``not`` conditions regardless.
113+
"""
114+
115+
class RequiredItem(ce.ExtendedMappingSchema):
116+
item = ce.ExtendedSchemaNode(colander.String())
117+
118+
class MappingWithType(ce.ExtendedMappingSchema):
119+
type = ce.ExtendedSchemaNode(colander.String())
120+
121+
class MappingWithoutType(ce.NotKeywordSchema, RequiredItem):
122+
_not = [MappingWithType()]
123+
124+
class MappingOnlyNotType(ce.NotKeywordSchema):
125+
_not = [MappingWithType()]
126+
127+
value = {"type": "invalid", "item": "valid"}
128+
try:
129+
result = MappingWithoutType().deserialize(value)
130+
except colander.Invalid:
131+
pass
132+
except Exception:
133+
raise AssertionError("Incorrect exception raised from deserialize of '{!s}' with {!s}"
134+
.format(ce._get_node_name(MappingWithoutType), value))
135+
else:
136+
raise AssertionError("Should have raised invalid schema from deserialize of '{!s}' with {!s}, but got {!s}"
137+
.format(ce._get_node_name(MappingWithoutType), value, result))
138+
139+
test_cases = [
140+
(MappingWithoutType, {"item": "valid", "value": "ignore"}, {"item": "valid"}),
141+
(MappingOnlyNotType, {"item": "valid", "value": "ignore"}, {})
142+
]
143+
for test_schema, test_value, test_expect in test_cases:
144+
try:
145+
result = test_schema().deserialize(test_value)
146+
assert result == test_expect, "Bad result from [{}] with [{}]".format(test_schema.__name__, test_value)
147+
except colander.Invalid:
148+
pytest.fail("Expected valid format from [{}] with [{}]".format(test_schema.__name__, test_value))
149+
150+
108151
class FieldTestString(ce.ExtendedSchemaNode):
109152
schema_type = colander.String
110153

weaver/wps_restapi/colander_extras.py

+20-3
Original file line numberDiff line numberDiff line change
@@ -1422,7 +1422,9 @@ class NotKeywordSchema(KeywordMapper):
14221422
"""
14231423
Allows specifying specific schema conditions that fails underlying schema definition validation if present.
14241424
1425-
This is equivalent to OpenAPI object mapping with ``additionalProperties: false``, but is more explicit in
1425+
Corresponds to the ``not`` specifier of `OpenAPI` specification.
1426+
1427+
This is equivalent to `OpenAPI` object mapping with ``additionalProperties: false``, but is more explicit in
14261428
the definition of invalid or conflicting field names with explicit definitions during deserialization.
14271429
14281430
Example::
@@ -1436,9 +1438,21 @@ class MappingWithType(ExtendedMappingSchema):
14361438
class MappingWithoutType(NotKeywordSchema, RequiredItem):
14371439
_not = [MappingWithType()]
14381440
1441+
class MappingOnlyNotType(NotKeywordSchema):
1442+
_not = [MappingWithType()]
1443+
14391444
# following will raise invalid error even if 'item' is valid because 'type' is also present
14401445
MappingWithoutType().deserialize({"type": "invalid", "item": "valid"})
14411446
1447+
# following will return successfully with only 'item' because 'type' was not present
1448+
MappingWithoutType().deserialize({"item": "valid", "value": "ignore"})
1449+
# result: {"item": "valid"}
1450+
1451+
# following will return an empty mapping dropping 'item' since it only needs to ensure 'type' was not present,
1452+
# but did not provide any additional fields requirement from other class inheritances
1453+
MappingOnlyNotType().deserialize({"item": "valid"})
1454+
# result: {}
1455+
14421456
.. seealso::
14431457
- :class:`OneOfKeywordSchema`
14441458
- :class:`AllOfKeywordSchema`
@@ -1457,7 +1471,7 @@ def _not(cls):
14571471

14581472
def _deserialize_keyword(self, cstruct):
14591473
"""
1460-
Test each possible case, raise if any corresponding schema was successfully validated.
1474+
Raise if any sub-node schema that should NOT be present was successfully validated.
14611475
"""
14621476
invalid_not = dict()
14631477
for schema_class in self._not: # noqa
@@ -1473,7 +1487,10 @@ def _deserialize_keyword(self, cstruct):
14731487
if invalid_not:
14741488
message = "Value contains not allowed fields from schema conditions: {}".format(invalid_not)
14751489
raise colander.Invalid(node=self, msg=message, value=cstruct)
1476-
return cstruct
1490+
# If schema was a plain NotKeywordSchema, the result will be empty as it serves only to validate
1491+
# that the subnodes are not present. Otherwise, if it derives from other mapping classes, apply them.
1492+
# If deserialization was not applied here, everything in the original cstruct would bubble up.
1493+
return ExtendedMappingSchema.deserialize(self, cstruct)
14771494

14781495

14791496
class KeywordTypeConverter(TypeConverter):

weaver/wps_restapi/swagger_definitions.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -768,8 +768,8 @@ class InputTypeDefinition(OneOfKeywordSchema):
768768
]
769769

770770

771-
class InputType(AnyOfKeywordSchema):
772-
_any_of = [
771+
class InputType(AllOfKeywordSchema):
772+
_all_of = [
773773
InputDescriptionType(),
774774
InputTypeDefinition(),
775775
WithMinMaxOccurs(),
@@ -803,8 +803,8 @@ class OutputTypeDefinition(OneOfKeywordSchema):
803803
]
804804

805805

806-
class OutputType(AnyOfKeywordSchema):
807-
_any_of = [
806+
class OutputType(AllOfKeywordSchema):
807+
_all_of = [
808808
OutputTypeDefinition(),
809809
OutputDescriptionType(),
810810
]

0 commit comments

Comments
 (0)