Implementing a custom pattern¶
For many implementations you will be able to use the default pattern objects supplied by this library for your needs.
If your notifications have additional requirements, such as service-specific validation rules, or additional required or optional fields, you can create your own pattern classes by subclassing the base pattern classes.
Adding a simple field¶
Suppose we want to add a field to an AnnounceEndorsement
pattern to indicate a “time to live” for the endorsement.
It doesn’t really matter what this means, but lets suppose it’s the number of days for which the endorsement record
is guaranteed to be available at the given identifier.
We would extend the AnnounceEndorsement
class like this:
from coarnotify.patterns import AnnounceEndorsement
class AnnounceEndorsementWithTTL(AnnounceEndorsement):
@property
def ttl(self):
return self.get_property('ttl')
@ttl.setter
def ttl(self, value):
self.set_property('ttl', value)
Now any Announce Endorsement notification which contains a ttl
field can be read and written using this object
Extending the validation¶
We have added a custom field to the pattern in the previous section. Now we want to validate that field to be sure that it contains a positive integer.
There are two ways to approach this. The simple way is for us to hard-code our validation:
from coarnotify.patterns import AnnounceEndorsement
class AnnounceEndorsementWithTTL(AnnounceEndorsement):
@property
def ttl(self):
return self.get_property('ttl')
@ttl.setter
def ttl(self, value):
if not isinstance(value, int) or value < 0:
raise ValueError('ttl must be a positive integer')
self.set_property('ttl', value)
def validate(self):
# ask the superclass to do its own validation first, and catch
# and keep any exceptions it raises to add to
ve = ValidationError()
try:
super(AnnounceEndorsementItem, self).validate()
except ValidationError as superve:
ve = superve
# now add our custom validation
if not isinstance(self.ttl, int) or self.ttl < 0:
ve.add_error('ttl', 'ttl must be a positive integer')
if ve.has_errors():
raise ve
return True
There is a more formal (and verbose) way to do this, in line with how the library is designed. This involves creating a custom validator and adding it to the validation ruleset for the pattern. Whether you take this approach depends on the extent to which the validators you need are reused or shared across custom patterns.
from coarnotify.core.notify import VALIDATORS
from coarnotify.patterns import AnnounceEndorsement
from coarnotify.validation import Validator, ValidationError
# create a validation function to check that a value is a positive integer
def positive_integer(obj, x):
if isinstance(x, int) and x > 0:
return True
raise ValueError("value must be a positive integer")
# create a custom validation ruleset with the new rule
RULES = VALIDATORS.rules()
RULES['ttl'] = {"default": positive_integer}
CUSTOM_VALIDATOR = Validator(rules=RULES)
class AnnounceEndorsementWithTTL(AnnounceEndorsement):
def __init__(self, stream: Union[ActivityStream, dict] = None,
validate_stream_on_construct=True,
validate_properties=True,
validators=None,
validation_context=None,
properties_by_reference=True):
# force override the default validator and kick construction up to the superclass
validators = CUSTOM_VALIDATOR
super(AnnounceEndorsement, self).__init__(stream=stream,
validate_stream_on_construct=validate_stream_on_construct,
validate_properties=validate_properties,
validators=validators,
validation_context=validation_context,
properties_by_reference=properties_by_reference)
@property
def ttl(self):
return self.get_property('ttl')
@ttl.setter
def ttl(self, value):
self.set_property('ttl', value)
def validate(self):
# ask the superclass to do its own validation first, and catch
# and keep any exceptions it raises to add to
ve = ValidationError()
try:
super(AnnounceEndorsementItem, self).validate()
except ValidationError as superve:
ve = superve
# now add our custom validation
self.required_and_validate(ve, "ttl", self.ttl)
if ve.has_errors():
raise ve
return True
Adding a complex/nested field¶
Sometimes we want to customise fields that are not in the top level of the pattern, but nested in one of the pattern parts. In order to do that we can override the pattern part with a custom implementation, and then we must wire in the custom part to the appropriate accessor on the pattern object.
For example, to add a custom object
to our AnnounceEndorsement
pattern, we would do the following:
First create our custom object, exteding NotifyObject
, which has a custom field imaginatively called custom_field
:
class AnnounceEndorsementObject(NotifyObject):
@property
def custom_field(self):
return self.get_property('custom_field')
@custom_field.setter
def custom_field(self, value):
self.set_property('custom_field', value)
Now we want it so when you call AnnounceEndorsement.object
you get an instance of our custom object, not the
default NotifyObject
. We do this by overriding the object
property on the AnnounceEndorsement
pattern:
from coarnotify.patterns import AnnounceEndorsement
class AnnounceEndorsementWithCustomObject(AnnounceEndorsement):
@property
def object(self):
obj = self.get_property(NotifyProperties.OBJECT)
if obj is not None:
return AnnounceEndorsementObject(obj,
validate_stream_on_construct=False,
validate_properties=self.validate_properties,
validators=self.validators,
validation_context=NotifyProperties.OBJECT,
properties_by_reference=self._properties_by_reference)
return None
Now when we access the object
property on an AnnounceEndorsementWithCustomObject
instance, we get an instance of
AnnounceEndorsementObject
.