Skip to content

Commit

Permalink
🚚 [#4980] Move implementation of JSON schema generation
Browse files Browse the repository at this point in the history
openforms.forms.utils is import/export stuff, so created a dedicated module json_schema

The idea is to fully define the schema generation in terms of `FormVariable`, as we have three possible options:
* Formio component -> `FormVariable` with source set to component (we can get more info from the component
* User-defined variable -> `FormVariable` with source set to user_defined (we can't get more info at all)
* Static variable -> `FormVariable` that only exists in-memory and not in the DB, but the implementation details are outside of it

Implementation-wise:
* Formio component -> schema will be generated inside `FormVariable`. If it fails, fall back to basic schema based on data type.
* User-defined variable -> use basic schema based on data type
* Static variable -> generate schema separately, and assign it manually the `json_schema` property of a `FormVariable` instance
  • Loading branch information
viktorvanwijk committed Jan 24, 2025
1 parent e34554b commit 57406e0
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 107 deletions.
46 changes: 46 additions & 0 deletions src/openforms/forms/json_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from typing import Sequence, Iterator

from openforms.typing import JSONObject
from openforms.variables.service import get_static_variables

from .models import Form, FormVariable


def _iter_form_variables(form: Form) -> Iterator[FormVariable]:
"""Iterate over static variables and all form variables.
:param form: Form
"""
# Static variables are always available
yield from get_static_variables()
# Handle from variables holding dynamic data (component and user defined)
yield from form.formvariable_set.all()


def generate_json_schema(form: Form, limit_to_variables: Sequence[str]) -> JSONObject:
"""Generate a JSON schema from a form, for the specified variables.
:param form: The form to generate JSON schema for.
:param limit_to_variables: Variables that will be included in the schema.
:returns: A JSON schema representing the form variables.
"""
requested_variables_schema = {
key: variable.as_json_schema()
for variable in _iter_form_variables(form)
if (key := variable.key) in limit_to_variables
}

# process this with deep objects etc., the FormioData data structure might be
# useful here too

# Result
schema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": requested_variables_schema,
"required": limit_to_variables,
"additionalProperties": False,
}

return schema
41 changes: 40 additions & 1 deletion src/openforms/forms/models/form_variable.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from copy import deepcopy
from typing import TYPE_CHECKING

from django.db import models, transaction
Expand All @@ -13,9 +14,15 @@
is_layout_component,
iter_components,
)
from openforms.formio.registry import register as component_registry
from openforms.formio.validators import variable_key_validator
from openforms.prefill.constants import IdentifierRoles
from openforms.variables.constants import FormVariableDataTypes, FormVariableSources
from openforms.typing import JSONObject
from openforms.variables.constants import (
DATA_TYPE_TO_JSON_SCHEMA,
FormVariableDataTypes,
FormVariableSources,
)
from openforms.variables.utils import check_initial_value

from .form_definition import FormDefinition
Expand Down Expand Up @@ -198,6 +205,8 @@ class FormVariable(models.Model):
null=True,
)

_json_schema = None

objects = FormVariableManager()

class Meta:
Expand Down Expand Up @@ -249,6 +258,36 @@ class Meta:
def __str__(self):
return _("Form variable '{key}'").format(key=self.key)

@property
def json_schema(self) -> JSONObject | None:
return self._json_schema

@json_schema.setter
def json_schema(self, value: JSONObject):
self._json_schema = value

def as_json_schema(self) -> JSONObject:
"""Return JSON schema of form variable.
If the schema generation for a formio component fails, fall back to a basic
schema based on the data type.
"""
if self.source == FormVariableSources.component:
try:
component = self.form_definition.configuration_wrapper.component_map[
self.key
]
component_plugin = component_registry[component["type"]]
self.json_schema = component_plugin.as_json_schema(component)
except (AttributeError, KeyError): # pragma: no cover
pass

if self.json_schema is None:
self.json_schema = deepcopy(DATA_TYPE_TO_JSON_SCHEMA[self.data_type])
self.json_schema["title"] = self.name

return self.json_schema

def get_initial_value(self):
return self.initial_value

Expand Down
103 changes: 0 additions & 103 deletions src/openforms/forms/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,9 @@
from rest_framework.test import APIRequestFactory

from openforms.formio.migration_converters import CONVERTERS, DEFINITION_CONVERTERS
from openforms.formio.registry import register as component_registry
from openforms.formio.utils import iter_components
from openforms.typing import JSONObject
from openforms.variables.constants import FormVariableSources
from openforms.variables.registry import (
register_static_variable as static_variable_registry,
)
from openforms.variables.service import get_json_schema_for_user_defined_variable

from .api.datastructures import FormVariableWrapper
from .api.serializers import (
Expand Down Expand Up @@ -134,104 +129,6 @@ def form_to_json(form_id: int) -> dict:
return resources


def form_variables_to_json_schema(
form: Form, variables_to_include: Sequence[str]
) -> JSONObject:
"""Generate a JSON schema from a form, for the specified variables.
:param form: The form to generate JSON schema for.
:param variables_to_include: Variables that will be included in the schema.
:returns: A JSON schema representing the form variables.
"""

# Handle static variables
static_var_properties = {
key: static_variable_registry[key].as_json_schema()
for key in variables_to_include
# To ensure fetching form variables from static_variable_registry does not
# raise KeyError
if key in static_variable_registry
}

# Handle form variables
all_form_vars = {var.key: var for var in form.formvariable_set.all()}
form_var_properties = {
key: get_json_schema_from_form_variable(all_form_vars[key])
for key in variables_to_include
# To ensure fetching static variables from all_from_vars does not raise KeyError
if key in all_form_vars
}

# Required
required_form_variables = [
var
for var in form_var_properties.keys()
if is_form_variable_required(all_form_vars[var])
]

required = [*static_var_properties.keys(), *required_form_variables]

# Result
var_properties = {**static_var_properties, **form_var_properties}
schema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": var_properties,
"required": required,
"additionalProperties": False,
}

return schema


# TODO-4980: add to FormVariable?
def get_json_schema_from_form_variable(form_variable: FormVariable) -> JSONObject:
"""Return a JSON schema for a form variable.
:param form_variable: The form variable to generate JSON schema for.
:returns schema: A JSON schema representing the form variable.
"""

match form_variable.source:
case FormVariableSources.component:
form_def = form_variable.form_definition
component = form_def.configuration_wrapper.component_map[form_variable.key]
component_plugin = component_registry[component["type"]]
schema = component_plugin.as_json_schema(component)
case FormVariableSources.user_defined:
schema = get_json_schema_for_user_defined_variable(form_variable.data_type)
schema["title"] = form_variable.name
case _: # pragma: no cover
raise NotImplementedError("Unexpected form variable source")

return schema


# TODO-4980: add to FormVariable?
def is_form_variable_required(form_variable: FormVariable) -> bool:
"""Return if a form variable is required.
:param form_variable: The form variable to check.
:returns required: Whether the form variable is required.
"""

match form_variable.source:
case FormVariableSources.component:
form_def = form_variable.form_definition
component = form_def.configuration_wrapper.component_map[form_variable.key]

validate = component.get("validate", {})
required = validate.get("required", False)
case FormVariableSources.user_defined:
# User defined variables have no required property
required = True
case _: # pragma: no cover
raise NotImplementedError("Unexpected form variable source")

return required


def export_form(form_id, archive_name=None, response=None):
resources = form_to_json(form_id)

Expand Down
4 changes: 2 additions & 2 deletions src/openforms/registrations/contrib/json_dump/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
SelectBoxesComponent,
SelectComponent,
)
from openforms.forms.utils import form_variables_to_json_schema
from openforms.forms.json_schema import generate_json_schema
from openforms.submissions.models import Submission, SubmissionFileAttachment
from openforms.submissions.service import DataContainer
from openforms.typing import JSONObject, JSONValue
Expand Down Expand Up @@ -50,7 +50,7 @@ def register_submission(
}

# Generate schema
schema = form_variables_to_json_schema(submission.form, options["variables"])
schema = generate_json_schema(submission.form, options["variables"])

# Post-processing
post_process(values, schema, submission)
Expand Down
4 changes: 3 additions & 1 deletion src/openforms/variables/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ def as_json_schema(self) -> JSONObject:
return deepcopy(DATA_TYPE_TO_JSON_SCHEMA[self.data_type])

def get_static_variable(self, submission: Submission | None = None):
return FormVariable(
variable = FormVariable(
name=self.name,
key=self.identifier,
data_type=self.data_type,
initial_value=self.get_initial_value(submission=submission),
)
variable.json_schema = self.as_json_schema()
return variable

0 comments on commit 57406e0

Please sign in to comment.