def test_mapper_chained_with_mapper(self): """ Chaining two FilterMappers together has basically the same effect as combining their Filters. Generally, combining two FilterMappers into a single instance is much easier to read/maintain than chaining them, but in a few cases it may be unavoidable (for example, if you need each FilterMapper to handle extra and/or missing keys differently). """ fm1 = f.FilterMapper( { 'id': f.Int | f.Min(1), }, allow_missing_keys=True, allow_extra_keys=True, ) fm2 = f.FilterMapper( { 'id': f.Required | f.Max(256), 'subject': f.NotEmpty | f.MaxLength(16), }, allow_missing_keys=False, allow_extra_keys=False, ) self.filter_type = lambda: fm1 | fm2 self.assertFilterPasses( { 'id': '42', 'subject': 'Hello, world!', }, { 'id': 42, 'subject': 'Hello, world!', }, ) self.assertFilterErrors( {}, { # ``fm1`` allows missing keys, so it sets 'id' to # ``None``. # However, ``fm2`` does not allow ``None`` for 'id' # (because of the ``Required`` filter). 'id': [f.Required.CODE_EMPTY], # `fm1` does not care about `subject`, but `fm2` # expects it to be there. 'subject': [f.FilterMapper.CODE_MISSING_KEY], }, expected_value={ 'id': None, 'subject': None, }, )
def test_pass_mapping(self): """ A FilterRepeater is applied to a dict containing valid values. """ self.filter_type = lambda: f.FilterMapper({ 'id': f.Required | f.Int | f.Min(1), 'subject': f.NotEmpty | f.MaxLength(16), }) filter_ = self._filter({ 'id': '42', 'subject': 'Hello, world!', }) self.assertFilterPasses( filter_, { 'id': 42, 'subject': 'Hello, world!', }, ) # The result is a dict, to match the type of the filter map. self.assertIs(type(filter_.cleaned_data), dict)
def test_fail_mapping(self): """ A FilterRepeater is applied to a dict containing invalid values. """ self.filter_type = lambda: f.FilterMapper({ 'id': f.Required | f.Int | f.Min(1), 'subject': f.NotEmpty | f.MaxLength(16), }) self.assertFilterErrors( { 'id': None, 'subject': 'Antidisestablishmentarianism', }, { 'id': [f.Required.CODE_EMPTY], 'subject': [f.MaxLength.CODE_TOO_LONG], }, expected_value = { 'id': None, 'subject': None, } )
def test_missing_keys_specified(self): """ FilterMappers can be configured to allow some missing keys but not others. """ self.filter_type = lambda: f.FilterMapper( { 'id': f.Required | f.Int | f.Min(1), 'subject': f.NotEmpty | f.MaxLength(16), }, allow_missing_keys={'subject'}, ) # The FilterMapper is configured to treat missing 'subject' as # if it were set to `None`. self.assertFilterPasses( {'id': '42'}, { 'id': 42, 'subject': None, }, ) # However, 'id' is still required. self.assertFilterErrors({ 'subject': 'Hello, world!', }, { 'id': [f.FilterMapper.CODE_MISSING_KEY], }, expected_value={ 'id': None, 'subject': 'Hello, world!', })
def test_missing_keys_allowed(self): """ By default, FilterMappers treat missing keys as `None`. """ self.filter_type = lambda: f.FilterMapper( { 'id': f.Required | f.Int | f.Min(1), 'subject': f.NotEmpty | f.MaxLength(16), }) # 'subject' allows null values, so no errors are generated. self.assertFilterPasses( { 'id': '42', }, { 'id': 42, 'subject': None, }, ) # However, 'id' has Required in its FilterChain, so a missing # 'id' is still an error. self.assertFilterErrors( { 'subject': 'Hello, world!', }, { 'id': [f.Required.CODE_EMPTY], }, expected_value={ 'id': None, 'subject': 'Hello, world!', }, )
def test_extra_keys_ordered(self): """ When the filter map is an OrderedDict, extra keys are alphabetized. """ # Note that we pass an OrderedDict to the filter initializer. self.filter_type = lambda: f.FilterMapper(OrderedDict(( ('subject', f.NotEmpty | f.MaxLength(16)), ('id', f.Required | f.Int | f.Min(1)), ))) filter_ = self._filter({ 'id': '42', 'subject': 'Hello, world!', 'cat': 'felix', 'bird': 'phoenix', 'fox': 'fennecs', }) self.assertFilterPasses( filter_, OrderedDict(( # The filtered keys are always listed first. ('subject', 'Hello, world!'), ('id', 42), # Extra keys are listed afterward, in alphabetical # order. ('bird', 'phoenix'), ('cat', 'felix'), ('fox', 'fennecs'), )), )
def test_extra_keys_disallowed(self): """ FilterMappers can be configured to treat any extra key as an invalid value. """ self.filter_type = lambda: f.FilterMapper( { 'id': f.Required | f.Int | f.Min(1), 'subject': f.NotEmpty | f.MaxLength(16), }, # Treat all extra keys as invalid values.s allow_extra_keys = False, ) self.assertFilterErrors( { 'id': '42', 'subject': 'Hello, world!', 'extra': 'ignored', }, { 'extra': [f.FilterMapper.CODE_EXTRA_KEY], }, # The valid fields were still included in the return value, # but the invalid field was removed. expected_value = { 'id': 42, 'subject': 'Hello, world!', } )
def test_fail_match_case(self): """ The incoming value matches one of the switch cases, but it is not valid, according to the corresponding filter. """ self.assertFilterErrors( self._filter( { 'name': 'positive', 'value': -1 }, getter=lambda value: value['name'], cases={ 'positive': f.FilterMapper({'value': f.Int | f.Min(0)}), }, ), {'value': [f.Min.CODE_TOO_SMALL]}, # The result is the exact same as if the value were passed # directly to the corresponding filter. expected_value={ 'name': 'positive', 'value': None }, )
def test_missing_keys_disallowed(self): """ FilterMappers can be configured to treat missing keys as invalid values. """ self.filter_type = lambda: f.FilterMapper( { 'id': f.Required | f.Int | f.Min(1), 'subject': f.NotEmpty | f.MaxLength(16), }, # Treat missing keys as invalid values. allow_missing_keys = False, ) self.assertFilterErrors( {}, { 'id': [f.FilterMapper.CODE_MISSING_KEY], 'subject': [f.FilterMapper.CODE_MISSING_KEY], }, expected_value = { 'id': None, 'subject': None, }, )
def test_pass_ordered_mapping(self): """ Configuring the FilterRepeater to return an OrderedDict. """ # Note that we pass an OrderedDict to the filter initializer. self.filter_type = lambda: f.FilterMapper(OrderedDict(( ('subject', f.NotEmpty | f.MaxLength(16)), ('id', f.Required | f.Int | f.Min(1)), ))) filter_ = self._filter({ 'id': '42', 'subject': 'Hello, world!', }) self.assertFilterPasses( filter_, OrderedDict(( ('subject', 'Hello, world!'), ('id', 42), )), ) # The result is an OrderedDict, to match the type of the filter # map. self.assertIs(type(filter_.cleaned_data), OrderedDict)
def test_pass_default(self): """ The incoming value does not match any of the switch cases, but we defined a default filter. """ self.assertFilterPasses( self._filter( { 'name': 'negative', 'value': -42 }, getter=lambda value: value['name'], cases={ 'positive': f.FilterMapper({'value': f.Int | f.Min(0)}), }, default=f.FilterMapper({'value': f.Int | f.Max(0)}), ), )
def __init__( self, filter_map, allow_missing_keys=False, allow_extra_keys=False, ): super(RequestFilter, self).__init__( f.Type(Mapping) | f.FilterMapper(filter_map, allow_missing_keys, allow_extra_keys))
def __init__( self, filter_map, allow_missing_keys=True, allow_extra_keys=True, ): super(ResponseFilter, self).__init__( f.Type(Mapping) | f.FilterMapper(filter_map, allow_missing_keys, allow_extra_keys))
def test_pass_none(self): """ For consistency with all the other Filter classes, `None` is considered a valid value to pass to a FilterMapper, even though it is not iterable. """ self.filter_type = lambda: f.FilterMapper({'id': f.Int}) self.assertFilterPasses(None)
def test_passthru_key(self): """ If you want to make a key required but do not want to run any Filters on it, set its FilterChain to `None`. """ self.filter_type = lambda: f.FilterMapper( { 'id': f.Required | f.Int | f.Min(1), 'subject': None, }, # If you configure a FilterMapper with passthru keys(s), # you generally also want to disallow missing keys. allow_missing_keys = False, ) self.assertFilterPasses( { 'id': '42', 'subject': 'Hello, world!', }, { 'id': 42, 'subject': 'Hello, world!', }, ) self.assertFilterPasses( { 'id': '42', 'subject': None, }, { 'id': 42, 'subject': None, }, ) self.assertFilterErrors( { 'id': '42', }, { 'subject': [f.FilterMapper.CODE_MISSING_KEY], }, expected_value = { 'id': 42, 'subject': None, }, )
def test_fail_non_mapping(self): """The incoming value is not a mapping.""" self.filter_type = lambda: f.FilterMapper({ 'id': f.Required | f.Int | f.Min(1), 'subject': f.NotEmpty | f.MaxLength(16), }) self.assertFilterErrors( # Nope; it's gotta be an explicit mapping. (('id', '42'), ('subject', 'Hello, world!')), [f.Type.CODE_WRONG_TYPE], )
def test_extra_keys_specified(self): """ FilterMappers can be configured only to allow certain extra keys. """ self.filter_type = lambda: f.FilterMapper( { 'id': f.Required | f.Int | f.Min(1), 'subject': f.NotEmpty | f.MaxLength(16), }, allow_extra_keys = {'message', 'extra'}, ) # As long as the extra keys are in the FilterMapper's # ``allow_extra_keys`` setting, everything is fine. self.assertFilterPasses( { 'id': '42', 'subject': 'Hello, world!', 'extra': 'ignored', }, { 'id': 42, 'subject': 'Hello, world!', 'extra': 'ignored', }, ) # But, add a key that isn't in ``allow_extra_keys``, and you've # got a problem. self.assertFilterErrors( { 'id': '42', 'subject': 'Hello, world!', 'attachment': { 'type': 'image/jpeg', 'data': '...', }, }, { 'attachment': [f.FilterMapper.CODE_EXTRA_KEY], }, expected_value = { 'id': 42, 'subject': 'Hello, world!', } )
def test_pass_match_case(self): """ The incoming value matches one of the switch cases. """ self.assertFilterPasses( self._filter( { 'name': 'positive', 'value': 42 }, getter=lambda value: value['name'], cases={ 'positive': f.FilterMapper({'value': f.Int | f.Min(0)}), }, ), )
def test_fail_no_default(self): """ The incoming value does not match any of the switch cases, and we did not define a default filter. """ self.assertFilterErrors( self._filter( { 'name': 'negative', 'value': -42 }, getter=lambda value: value['name'], cases={ 'positive': f.FilterMapper({'value': f.Int | f.Min(0)}), }, ), [f.Choice.CODE_INVALID], )
def test_extra_keys_allowed(self): """ By default, FilterMappers passthru extra keys. """ self.filter_type = lambda: f.FilterMapper( { 'id': f.Required | f.Int | f.Min(1), 'subject': f.NotEmpty | f.MaxLength(16), }) self.assertFilterPasses( { 'id': '42', 'subject': 'Hello, world!', 'extra': 'ignored', }, { 'id': 42, 'subject': 'Hello, world!', 'extra': 'ignored', })
def test_filter_mapper_chained_with_filter(self): """ Chaining a Filter with a FilterMapper causes the chained Filter to operate on the entire mapping. """ fm = f.FilterMapper({ 'id': f.Required | f.Int | f.Min(1), 'subject': f.NotEmpty | f.MaxLength(16), }) self.filter_type = lambda: fm | f.MaxLength(3) self.assertFilterPasses( { 'id': '42', 'subject': 'Hello, world!', 'extra': 'ignored', }, { 'id': 42, 'subject': 'Hello, world!', 'extra': 'ignored', }, ) self.assertFilterErrors( { 'id': '42', 'subject': 'Hello, world!', 'extra': 'ignored', 'attachment': None, }, # The incoming value has 4 items, which fails the MaxLength # filter. [f.MaxLength.CODE_TOO_LONG], )
def test_mapperception(self): """ Want to filter dicts that contain other dicts? We need to go deeper. """ self.filter_type = lambda: f.FilterMapper( { 'id': f.Required | f.Int | f.Min(1), 'subject': f.NotEmpty | f.MaxLength(16), 'attachment': f.FilterMapper( { 'type': f.Required | f.Choice(choices={'image/jpeg', 'image/png'}), 'data': f.Required | f.Base64Decode, }, allow_extra_keys = False, allow_missing_keys = False, ) }, allow_extra_keys = False, allow_missing_keys = False, ) # Valid mapping is valid. self.assertFilterPasses( { 'id': '42', 'subject': 'Hello, world!', 'attachment': { 'type': 'image/jpeg', 'data': b'R0lGODlhDwAPAKECAAAAzMzM/////wAAACwAAAAAD' b'wAPAAACIISPeQHsrZ5ModrLlN48CXF8m2iQ3YmmKq' b'VlRtW4MLwWACH+EVRIRSBDQUtFIElTIEEgTElFOw==', }, }, { 'id': 42, 'subject': 'Hello, world!', 'attachment': { 'type': 'image/jpeg', 'data': b'GIF89a\x0f\x00\x0f\x00\xa1\x02\x00\x00\x00' b'\xcc\xcc\xcc\xff\xff\xff\xff\x00\x00\x00,\x00' b'\x00\x00\x00\x0f\x00\x0f\x00\x00\x02 \x84\x8f' b'y\x01\xec\xad\x9eL\xa1\xda\xcb\x94\xde<\tq|' b'\x9bh\x90\xdd\x89\xa6*\xa5eF\xd5\xb80\xbc\x16' b'\x00!\xfe\x11THE CAKE IS A LIE;' }, }, ) # Invalid mapping... not so much. self.assertFilterErrors( { 'id': 'NaN', 'attachment': { 'type': 'foo', 'data': False, }, }, { # The error keys are the dotted paths to the invalid # values. # This way, we don't have to deal with nested dicts # when processing error codes. 'id': [f.Decimal.CODE_NON_FINITE], 'subject': [f.FilterMapper.CODE_MISSING_KEY], 'attachment.type': [f.Choice.CODE_INVALID], 'attachment.data': [f.Type.CODE_WRONG_TYPE], }, # The resulting value has the expected structure, but it's # a ghost town. expected_value = { 'id': None, 'subject': None, 'attachment': { 'type': None, 'data': None, }, }, )
def filter_kwargs(self, filters): # type: (Dict[Text, Dict[Text, f.FilterCompatible]]) -> Dict[Text, dict] """ Extracts and filters values from the trigger kwargs. :param filters: Keys are the names of triggers to extract params from. Values are dicts used to configure FilterMapper instances. Note: The FilterMapper instances are configured with: - ``allow_missing_keys = True`` - ``allow_extra_keys = True`` Example:: task_params = context.filter_kwargs({ 't_createApplicant': { 'eflId': f.Required | f.ext.Model(Questionnaire), }, 'scoring': { 'model'; f.Required | f.Unicode, }, }) :raise: - ``ValueError`` if the trigger kwargs fail validation. """ # Configure the inner filters, used to process each value # inside ``trigger_kwargs``. map_ = { item_key: f.Optional(default={}) | f.FilterMapper(filter_map) for item_key, filter_map in iteritems(filters) } filter_ =\ f.FilterRunner( # Configure the outer filter, used to apply the inner # filters to the ``trigger_kwargs`` dict. starting_filter = f.FilterMapper( filter_map = map_, allow_missing_keys = True, allow_extra_keys = True, ), incoming_data = self.trigger_kwargs or {}, ) if not filter_.is_valid(): raise with_context( exc=ValueError( 'Invalid trigger kwargs: {errors}'.format( errors=filter_.errors, ), ), context={ 'filter_errors': filter_.get_errors(with_context=True), }, ) return filter_.cleaned_data