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.