Skip to content

Commit

Permalink
[#3607] Refactor validation plugins
Browse files Browse the repository at this point in the history
... to be up to date with the current OF standard
Only implementation is changed in this commit, tests will follow
  • Loading branch information
Viicos committed Dec 19, 2023
1 parent 0a01b2d commit 33db635
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 116 deletions.
11 changes: 5 additions & 6 deletions src/openforms/contrib/brk/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from openforms.authentication.constants import AuthAttribute
from openforms.submissions.models import Submission
from openforms.validations.base import BasePlugin
from openforms.validations.registry import register

from .client import NoServiceConfigured, SearchParams, get_client
Expand Down Expand Up @@ -56,15 +57,13 @@ class ValueSerializer(serializers.Serializer):
value = AddressValueSerializer()


@register(
"brk-zakelijk-gerechtigd",
verbose_name=_("BRK - Zakelijk gerechtigd"),
for_components=("addressNL",),
)
@register("brk-Zaakgerechtigde")
@deconstructible
class BRKZakelijkGerechtigdeValidator:
class BRKZaakgerechtigdeValidator(BasePlugin[AddressValue]):

value_serializer = ValueSerializer
verbose_name = _("BRK - Zaakgerechtigde")
for_components = ("addressNL",)

error_messages = {
"no_bsn": _("No BSN is available to validate your address."),
Expand Down
36 changes: 20 additions & 16 deletions src/openforms/contrib/kvk/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
from requests import RequestException

from openforms.utils.validators import validate_digits, validate_rsin
from openforms.validations.registry import StringValueSerializer, register
from openforms.validations.base import BasePlugin
from openforms.validations.registry import register

from .client import NoServiceConfigured, SearchParams, get_client

Expand All @@ -20,7 +21,7 @@ class NumericBaseValidator:
"too_short": _("%(type)s should have %(size)i characters."),
}

def __call__(self, value):
def __call__(self, value: str):
validate_digits(value)

if len(value) != self.value_size:
Expand Down Expand Up @@ -51,8 +52,6 @@ class KVKRemoteBaseValidator:
query_param: Literal["kvkNummer", "rsin", "vestigingsnummer"]
value_label: str

value_serializer = StringValueSerializer

error_messages = {
"not_found": _("%(type)s does not exist."),
"too_short": _("%(type)s should have %(size)i characters."),
Expand Down Expand Up @@ -84,38 +83,43 @@ def __call__(self, value: str) -> bool:
return True


@register("kvk-kvkNumber", verbose_name=_("KvK number"), for_components=("textfield",))
@register("kvk-kvkNumber")
@deconstructible
class KVKNumberRemoteValidator(KVKRemoteBaseValidator):
class KVKNumberRemoteValidator(BasePlugin[str], KVKRemoteBaseValidator):
query_param = "kvkNummer"
value_label = _("KvK number")

def __call__(self, value, submission):
verbose_name = _("KvK number")
for_components = ("textfield",)

def __call__(self, value: str, submission):
validate_kvk(value)
super().__call__(value)


@register("kvk-rsin", verbose_name=_("KvK RSIN"), for_components=("textfield",))
@register("kvk-rsin")
@deconstructible
class KVKRSINRemoteValidator(KVKRemoteBaseValidator):
class KVKRSINRemoteValidator(BasePlugin[str], KVKRemoteBaseValidator):
query_param = "rsin"
value_label = _("RSIN")

def __call__(self, value, submission):
verbose_name = _("KvK RSIN")
for_components = ("textfield",)

def __call__(self, value: str, submission):
validate_rsin(value)
super().__call__(value)


@register(
"kvk-branchNumber",
verbose_name=_("KvK branch number"),
for_components=("textfield",),
)
@register("kvk-branchNumber")
@deconstructible
class KVKBranchNumberRemoteValidator(KVKRemoteBaseValidator):
query_param = "vestigingsnummer"
value_label = _("Branch number")

def __call__(self, value, submission):
verbose_name = _("KvK branch number")
for_components = ("textfield",)

def __call__(self, value: str, submission):
validate_branchNumber(value)
super().__call__(value)
8 changes: 4 additions & 4 deletions src/openforms/plugins/registry.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Callable, Generic, TypeVar
from typing import TYPE_CHECKING, Callable, Generic, Iterator, TypeVar

from django.db import OperationalError

Expand Down Expand Up @@ -35,7 +35,7 @@ def __call__(
def decorator(plugin_cls: type[PluginType_co]) -> type[PluginType_co]:
if len(unique_identifier) > UNIQUE_ID_MAX_LENGTH:
raise ValueError(
f"The unique identifier '{unique_identifier}' is longer then {UNIQUE_ID_MAX_LENGTH} characters."
f"The unique identifier '{unique_identifier}' is longer than {UNIQUE_ID_MAX_LENGTH} characters."
)
if unique_identifier in self._registry:
raise ValueError(
Expand All @@ -50,7 +50,7 @@ def decorator(plugin_cls: type[PluginType_co]) -> type[PluginType_co]:

return decorator

def check_plugin(self, plugin):
def check_plugin(self, plugin: PluginType_co):
# validation hook
pass

Expand All @@ -63,7 +63,7 @@ def __getitem__(self, key: str):
def __contains__(self, key: str):
return key in self._registry

def iter_enabled_plugins(self):
def iter_enabled_plugins(self) -> Iterator[PluginType_co]:
try:
config = GlobalConfiguration.get_solo()
assert isinstance(config, GlobalConfiguration)
Expand Down
2 changes: 1 addition & 1 deletion src/openforms/registrations/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class Registry(BaseRegistry[BasePlugin]):

module = "registrations"

def check_plugin(self, plugin):
def check_plugin(self, plugin: BasePlugin):
if not plugin.configuration_options:
raise ValueError(
"Please specify 'configuration_options' attribute for plugin class."
Expand Down
34 changes: 34 additions & 0 deletions src/openforms/validations/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from abc import ABC, abstractmethod
from typing import ClassVar, Generic, TypeVar

from rest_framework import serializers

from openforms.plugins.plugin import AbstractBasePlugin
from openforms.submissions.models import Submission

T = TypeVar("T")
"""A type variable representing the type of the value being validated by the plugin."""


class StringValueSerializer(serializers.Serializer):
"""A default serializer that accepts ``value`` as a string."""

value = serializers.CharField()


class BasePlugin(ABC, AbstractBasePlugin, Generic[T]):

value_serializer: ClassVar[type[serializers.BaseSerializer]] = StringValueSerializer
"""The serializer to be used to validate the value."""

for_components: ClassVar[tuple[str, ...]] = tuple()
"""The components that can make use of this validator."""

@property
def is_enabled(self) -> bool:
# TODO always enabled for now, see: https://github.com/open-formulieren/open-forms/issues/1149
return True

@abstractmethod
def __call__(self, value: T, submission: Submission) -> bool:
pass
82 changes: 17 additions & 65 deletions src/openforms/validations/registry.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import dataclasses
import logging
from typing import Callable, Iterable, List, Type, Union
from typing import Callable, Iterable, List, Type, TypeVar, Union

from django.core.exceptions import ValidationError as DJ_ValidationError
from django.utils.translation import gettext_lazy as _
Expand All @@ -13,15 +13,9 @@
from openforms.submissions.models import Submission
from openforms.typing import JSONValue

logger = logging.getLogger(__name__)

ValidatorType = Callable[[JSONValue, Submission], bool]


class StringValueSerializer(serializers.Serializer):
"""A default serializer that accepts ``value`` as a string."""
from .base import BasePlugin

value = serializers.CharField()
logger = logging.getLogger(__name__)


@dataclasses.dataclass()
Expand All @@ -30,20 +24,6 @@ class ValidationResult:
messages: List[str] = dataclasses.field(default_factory=list)


@dataclasses.dataclass()
class RegisteredValidator:
identifier: str
verbose_name: str
callable: ValidatorType
for_components: tuple[str]
is_demo_plugin: bool = False
# TODO always enabled for now, see: https://github.com/open-formulieren/open-forms/issues/1149
is_enabled: bool = True

def __call__(self, *args, **kwargs) -> bool:
return self.callable(*args, **kwargs)


def flatten(iterables: Iterable) -> List[str]:
def _flat(it):
if isinstance(it, str):
Expand All @@ -58,7 +38,10 @@ def _flat(it):
return list(_flat(iterables))


class Registry(BaseRegistry[RegisteredValidator]):
T = TypeVar("T")


class Registry(BaseRegistry[BasePlugin[T]]):
"""
A registry for the validations module plugins.
Expand All @@ -70,51 +53,14 @@ class Registry(BaseRegistry[RegisteredValidator]):

module = "validations"

def __call__(
self,
identifier: str,
verbose_name: str,
is_demo_plugin: bool = False,
for_components: tuple[str] = tuple(),
*args,
**kwargs,
) -> Callable:
def decorator(validator: Union[Type, ValidatorType]):
if identifier in self._registry:
raise ValueError(
f"The unique identifier '{identifier}' is already present "
"in the registry."
)

call = validator
assert hasattr(
call, "value_serializer"
), "Plugins must define a 'value_serializer' attribute"

if isinstance(call, type):
call = validator()
if not callable(call):
raise ValueError(f"Validator '{identifier}' is not callable.")

self._registry[identifier] = RegisteredValidator(
identifier=identifier,
verbose_name=verbose_name,
callable=call,
for_components=for_components,
is_demo_plugin=is_demo_plugin,
)
return validator

return decorator

@elasticapm.capture_span("app.validations.validate")
def validate(
self, plugin_id: str, value: JSONValue, submission: Submission
self, plugin_id: str, value: T, submission: Submission
) -> ValidationResult:
try:
validator = self._registry[plugin_id]
except KeyError:
logger.warning(f"called unregistered plugin_id '{plugin_id}'")
logger.warning("called unregistered plugin_id %s", plugin_id)
return ValidationResult(
False,
messages=[
Expand All @@ -124,15 +70,15 @@ def validate(
],
)

if not getattr(validator.callable, "is_enabled", True):
if not getattr(validator, "is_enabled", True):
return ValidationResult(
False,
messages=[
_("plugin '{plugin_id}' not enabled").format(plugin_id=plugin_id)
],
)

SerializerClass = validator.callable.value_serializer
SerializerClass = validator.value_serializer
serializer = SerializerClass(data={"value": value})

# first, run the cheap validation to check that the data actually conforms to the expected schema.
Expand All @@ -151,6 +97,12 @@ def validate(
else:
return ValidationResult(True)

def check_plugin(self, plugin: BasePlugin) -> None:
if not hasattr(plugin, "value_serializer"):
raise ValueError(
f"Validator '{plugin.identifier}' must have a 'value_serializer' attribute."
)


# Sentinel to provide the default registry. You an easily instantiate another
# :class:`Registry` object to use as dependency injection in tests.
Expand Down
Loading

0 comments on commit 33db635

Please sign in to comment.