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 9, 2025
1 parent 860399c commit 6affd84
Show file tree
Hide file tree
Showing 4 changed files with 324 additions and 9 deletions.
119 changes: 113 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_component_properties,
salt_location_message,
to_multiple,
)

logger = logging.getLogger(__name__)

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

@staticmethod
def as_json_schema(component: DateComponent) -> JSONObject:
label = component.get("label", "Date")

base = {"title": label, "format": "date"}
return handle_component_properties(base, component)


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

@staticmethod
def as_json_schema(component: Component) -> JSONObject:
label = component.get("label", "Date time")

base = {"title": label, "format": "date-time"}
return handle_component_properties(base, component)


@register("map")
class Map(BasePlugin[MapComponent]):
Expand Down Expand Up @@ -222,6 +241,24 @@ 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:
label = component.get("label", "Map coordinate")

base = {
"title": label,
"type": "array",
"prefixItems": [
{"title": "Latitude", "type": "number"},
{"title": "Longitude", "type": "number"},
],
"items": False,
"minItems": 2,
"maxItems": 2,
}

return base


@register("postcode")
class Postcode(BasePlugin[Component]):
Expand Down Expand Up @@ -253,8 +290,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 +311,13 @@ def build_serializer_field(
)
return serializers.ListField(child=base) if multiple else base

@staticmethod
def as_json_schema(component: Component) -> JSONObject:
label = component.get("label", "Postcode")

base = {"title": label, "type": "string"}
return handle_component_properties(base, component)


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

@staticmethod
def as_json_schema(component: Component) -> JSONObject:
# This component plugin is transformed into a SelectBoxes component, so a schema
# is not relevant here
raise NotImplementedError()


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

@staticmethod
def as_json_schema(component: Component) -> JSONObject:
label = component.get("label", "BSN")

base = {
"title": label,
"type": "string",
"pattern": "^\\d{9}",
"format": "nl-bsn",
}
return handle_component_properties(base, component)


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

@staticmethod
def as_json_schema(component: AddressNLComponent) -> JSONObject:
label = component.get("label", "Address NL")
base = {
"title": label,
"type": "object",
"properties": {
"city": {"type": "string"},
"houseLetter": {"type": "string"},
"houseNumber": {"type": "string"},
"houseNumberAddition": {"type": "string"},
"postcode": {"type": "string"},
"secretStreetCity": {
"description": "Secret for the combination of city and street name",
"type": "string",
},
"streetName": {"type": "string"},
},
"required": ["houseNumber", "postcode"],
}

return base


@register("cosign")
class Cosign(BasePlugin):
Expand All @@ -520,6 +605,14 @@ 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:
label = component.get("label", "Cosign email")

base = {"title": label, "type": "string", "format": "email"}

return base


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

@staticmethod
def as_json_schema(component: Component) -> JSONObject:
label = component.get("label", "IBAN")

base = {"title": label, "type": "string"}
return handle_component_properties(base, component)


@register("licenseplate")
class LicensePlate(BasePlugin):
Expand All @@ -556,8 +656,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 +679,10 @@ def build_serializer_field(
)

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

@staticmethod
def as_json_schema(component: Component) -> JSONObject:
label = component.get("label", "License plate")

base = {"title": label, "type": "string"}
return handle_component_properties(base, component)
32 changes: 32 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,32 @@ 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.
"""
title = f"Array of '{schema.pop("title")}'"
return {
"title": title,
"type": "array",
"items": schema,
}


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

0 comments on commit 6affd84

Please sign in to comment.