Skip to content

Commit

Permalink
✨ [#4980] Add json schema definition to formio components
Browse files Browse the repository at this point in the history
  • Loading branch information
viktorvanwijk committed Jan 8, 2025
1 parent e7df98f commit c7cb36e
Show file tree
Hide file tree
Showing 4 changed files with 249 additions and 9 deletions.
105 changes: 99 additions & 6 deletions src/openforms/formio/components/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from openforms.authentication.service import AuthAttribute
from openforms.config.models import GlobalConfiguration, MapTileLayer
from openforms.submissions.models import Submission
from openforms.typing import DataMapping
from openforms.typing import DataMapping, JSONObject
from openforms.utils.date import TIMEZONE_AMS, datetime_in_amsterdam, format_date_value
from openforms.utils.validators import BSNValidator, IBANValidator
from openforms.validations.service import PluginValidator
Expand All @@ -43,7 +43,12 @@
from .np_family_members.haal_centraal import get_np_family_members_haal_centraal
from .np_family_members.models import FamilyMembersTypeConfig
from .np_family_members.stuf_bg import get_np_family_members_stuf_bg
from .utils import _normalize_pattern, salt_location_message
from .utils import (
_normalize_pattern,
handle_json_schema,
salt_location_message,
to_multiple,
)

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -109,6 +114,12 @@ def build_serializer_field(
)
return serializers.ListField(child=base) if multiple else base

@staticmethod
def as_json_schema(component: Component) -> JSONObject:
multiple = component.get("multiple", False)
schema = {"title": "Date", "format": "date"}
return to_multiple(schema) if multiple else schema


class FormioDateTimeField(serializers.DateTimeField):
def validate_empty_values(self, data):
Expand Down Expand Up @@ -190,6 +201,12 @@ def build_serializer_field(
)
return serializers.ListField(child=base) if multiple else base

@staticmethod
def as_json_schema(component: Component) -> JSONObject:
multiple = component.get("multiple", False)
schema = {"title": "Date time", "format": "date-time"}
return to_multiple(schema) if multiple else schema


@register("map")
class Map(BasePlugin[MapComponent]):
Expand Down Expand Up @@ -222,6 +239,18 @@ def build_serializer_field(self, component: MapComponent) -> serializers.ListFie
)
return serializers.ListField(child=base, min_length=2, max_length=2)

@staticmethod
def as_json_schema(component: MapComponent) -> JSONObject:
return {
"title": "Map coordinate",
"type": "array",
"prefixItems": [
{"title": "Latitude", "type": "number"},
{"title": "Longitude", "type": "number"},
],
"items": False,
}


@register("postcode")
class Postcode(BasePlugin[Component]):
Expand Down Expand Up @@ -253,8 +282,8 @@ def build_serializer_field(
# dynamically add in more kwargs based on the component configuration
extra = {}
validators = []
# adding in the validator is more explicit than changing to serialiers.RegexField,
# which essentially does the same.
# adding in the validator is more explicit than changing to
# serializers.RegexField, which essentially does the same.
if pattern := validate.get("pattern"):
validators.append(
RegexValidator(
Expand All @@ -274,6 +303,13 @@ def build_serializer_field(
)
return serializers.ListField(child=base) if multiple else base

@staticmethod
def as_json_schema(component: Component) -> JSONObject:
multiple = component.get("multiple", False)
# TODO-4980: add regex validator to 'pattern' property?
schema = {"title": "Postcode", "type": "string"}
return to_multiple(schema) if multiple else schema


class FamilyMembersHandler(Protocol):
def __call__(
Expand Down Expand Up @@ -362,6 +398,11 @@ def mutate_config_dynamically(
for value, label in child_choices
]

@staticmethod
def as_json_schema(component: Component) -> JSONObject:
# TODO-4980: is there a useful schema here?
raise NotImplementedError()


@register("bsn")
class BSN(BasePlugin[Component]):
Expand Down Expand Up @@ -393,6 +434,17 @@ def build_serializer_field(
)
return serializers.ListField(child=base) if multiple else base

@staticmethod
def as_json_schema(component: Component) -> JSONObject:
multiple = component.get("multiple", False)
schema = {
"title": "BSN",
"type": "string",
"pattern": "^\\d{9}",
"format": "nl-bsn",
}
return to_multiple(schema) if multiple else schema


class AddressValueSerializer(serializers.Serializer):
postcode = serializers.RegexField(
Expand Down Expand Up @@ -510,6 +562,30 @@ def build_serializer_field(
**extra,
)

@staticmethod
def as_json_schema(component: Component) -> JSONObject:
return {
"title": "Address NL",
"type": "object",
"properties": {
"city": {"title": "City", "type": "string"},
"houseLetter": {"title": "House letter", "type": "string"},
"houseNumber": {"title": "House number", "type": "string"},
"houseNumberAddition": {
"title": "House number addition",
"type": "string",
},
"postcode": {"title": "Postcode", "type": "string"},
"secretStreetCity": {
"title": "Secret city",
"description": "Secret for the combination of city and street name",
"type": "string",
},
"streetName": {"title": "Street name", "type": "string"},
},
"required": ["houseNumber", "postcode"],
}


@register("cosign")
class Cosign(BasePlugin):
Expand All @@ -520,6 +596,10 @@ def build_serializer_field(self, component: Component) -> serializers.EmailField
required = validate.get("required", False)
return serializers.EmailField(required=required, allow_blank=not required)

@staticmethod
def as_json_schema(component: Component) -> JSONObject:
return {"title": "Cosign email", "type": "email"}


@register("iban")
class Iban(BasePlugin):
Expand All @@ -542,6 +622,12 @@ def build_serializer_field(
)
return serializers.ListField(child=base) if multiple else base

@staticmethod
def as_json_schema(component: Component) -> JSONObject:
multiple = component.get("multiple", False)
schema = {"title": "IBAN", "type": "string"}
return to_multiple(schema) if multiple else schema


@register("licenseplate")
class LicensePlate(BasePlugin):
Expand All @@ -556,8 +642,8 @@ def build_serializer_field(

extra = {}
validators = []
# adding in the validator is more explicit than changing to serialiers.RegexField,
# which essentially does the same.
# adding in the validator is more explicit than changing to
# serializers.RegexField, which essentially does the same.
if pattern := validate.get("pattern"):
validators.append(
RegexValidator(
Expand All @@ -579,3 +665,10 @@ def build_serializer_field(
)

return serializers.ListField(child=base) if multiple else base

@staticmethod
def as_json_schema(component: Component) -> JSONObject:
# TODO-4980: add regex validator to 'pattern' property?
multiple = component.get("multiple", False)
schema = {"title": "License plate", "type": "string"}
return to_multiple(schema) if multiple else schema
31 changes: 31 additions & 0 deletions src/openforms/formio/components/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from django.utils.crypto import salted_hmac

from openforms.typing import JSONObject

from ..typing import Component

def _normalize_pattern(pattern: str) -> str:
"""
Expand All @@ -19,3 +22,31 @@ def salt_location_message(message_bits: dict[str, str]) -> str:
computed_message = f"{message_bits['postcode']}/{message_bits['number']}/{message_bits['city']}/{message_bits['street_name']}"
computed_hmac = salted_hmac("location_check", value=computed_message).hexdigest()
return computed_hmac


def to_multiple(schema: JSONObject) -> JSONObject:
"""Convert a JSON schema of a component to a schema of multiple components.
:param schema: JSON schema of a component.
:returns: JSON schema of multiple components.
"""
return {
"title": f"Array of {schema["title"].lower()}",
"type": "array",
"items": schema,
}


def handle_json_schema(schema: JSONObject, component: Component) -> JSONObject:
"""Handle JSON schema by:
- Evaluating the multiple property of the component, and adjust the schema
accordingly.
:param schema: JSON schema of a component.
:param component: Component.
:returns: Handled JSON schema.
"""
multiple = component.get("multiple", False)
return to_multiple(schema) if multiple else schema
Loading

0 comments on commit c7cb36e

Please sign in to comment.