Skip to content

Commit

Permalink
Merge pull request #3925 from open-formulieren/feature/3688-target-paths
Browse files Browse the repository at this point in the history
[#3688] Add API endpoint to return available target paths
  • Loading branch information
Viicos authored Mar 8, 2024
2 parents e25dac8 + 2bc2b01 commit f4d3748
Show file tree
Hide file tree
Showing 13 changed files with 642 additions and 9 deletions.
81 changes: 81 additions & 0 deletions src/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4029,6 +4029,47 @@ paths:
$ref: '#/components/headers/X-Is-Form-Designer'
Content-Language:
$ref: '#/components/headers/Content-Language'
/api/v2/registration/plugins/objects-api/target-paths:
post:
operationId: registration_plugins_objects_api_target_paths_create
tags:
- registration
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/TargetPathsInput'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/TargetPathsInput'
multipart/form-data:
schema:
$ref: '#/components/schemas/TargetPathsInput'
required: true
security:
- cookieAuth: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/TargetPaths'
description: ''
headers:
X-Session-Expires-In:
$ref: '#/components/headers/X-Session-Expires-In'
X-CSRFToken:
$ref: '#/components/headers/X-CSRFToken'
X-Is-Form-Designer:
$ref: '#/components/headers/X-Is-Form-Designer'
Content-Language:
$ref: '#/components/headers/Content-Language'
parameters:
- in: header
name: X-CSRFToken
schema:
type: string
required: true
/api/v2/service-fetch-configurations:
get:
operationId: service_fetch_configurations_list
Expand Down Expand Up @@ -9841,6 +9882,46 @@ components:
* `_save` - Save
* `_addanother` - Save and add another
* `_continue` - Save and continue editing
TargetPaths:
type: object
properties:
targetPath:
type: array
items:
type: string
title: Segment of a JSON path
description: Representation of the JSON target location as a list of string
segments.
isRequired:
type: boolean
title: required
description: Wether the path is marked as required in the JSON Schema.
jsonSchema:
type: object
additionalProperties: {}
description: Corresponding (sub) JSON Schema of the target path.
required:
- isRequired
- jsonSchema
- targetPath
TargetPathsInput:
type: object
properties:
objecttypeUrl:
type: string
format: uri
description: The URL of the objecttype.
objecttypeVersion:
type: integer
description: The version of the objecttype.
variableJsonSchema:
type: object
additionalProperties: {}
description: The JSON Schema of the form variable.
required:
- objecttypeUrl
- objecttypeVersion
- variableJsonSchema
TemporaryFileUpload:
type: object
description: |-
Expand Down
31 changes: 31 additions & 0 deletions src/openforms/registrations/contrib/objects_api/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,34 @@ class ObjecttypeVersionSerializer(serializers.Serializer):
label=_("Integer version of the Objecttype."),
)
status = serializers.CharField(label=_("Status of the object type version"))


class TargetPathsSerializer(serializers.Serializer):
target_path = serializers.ListField(
child=serializers.CharField(label=_("Segment of a JSON path")),
label=_("target path"),
help_text=_(
"Representation of the JSON target location as a list of string segments."
),
)
is_required = serializers.BooleanField(
label=_("required"),
help_text=_("Wether the path is marked as required in the JSON Schema."),
)
json_schema = serializers.DictField(
label=_("json schema"),
help_text=_("Corresponding (sub) JSON Schema of the target path."),
)


class TargetPathsInputSerializer(serializers.Serializer):
objecttype_url = serializers.URLField(
label=_("objecttype url"), help_text=("The URL of the objecttype.")
)
objecttype_version = serializers.IntegerField(
label=_("objecttype version"), help_text=_("The version of the objecttype.")
)
variable_json_schema = serializers.DictField(
label=_("variable json schema"),
help_text=_("The JSON Schema of the form variable."),
)
7 changes: 6 additions & 1 deletion src/openforms/registrations/contrib/objects_api/api/urls.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.urls import path

from .views import ObjecttypesListView, ObjecttypeVersionsListView
from .views import ObjecttypesListView, ObjecttypeVersionsListView, TargetPathsListView

app_name = "objects_api"

Expand All @@ -15,4 +15,9 @@
ObjecttypeVersionsListView.as_view(),
name="object-type-versions",
),
path(
"target-paths",
TargetPathsListView.as_view(),
name="target-paths",
),
]
64 changes: 62 additions & 2 deletions src/openforms/registrations/contrib/objects_api/api/views.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import re
from typing import Any

from django.utils.translation import gettext_lazy as _

from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import authentication, permissions, views
from rest_framework import authentication, exceptions, permissions, views
from rest_framework.request import Request
from rest_framework.response import Response

from openforms.api.views import ListMixin

from ..client import get_objecttypes_client
from .serializers import ObjecttypeSerializer, ObjecttypeVersionSerializer
from ..json_schema import InvalidReference, iter_json_schema_paths, json_schema_matches
from .serializers import (
ObjecttypeSerializer,
ObjecttypeVersionSerializer,
TargetPathsInputSerializer,
TargetPathsSerializer,
)


@extend_schema_view(
Expand Down Expand Up @@ -49,3 +60,52 @@ class ObjecttypeVersionsListView(ListMixin, views.APIView):
def get_objects(self) -> list[dict[str, Any]]:
with get_objecttypes_client() as client:
return client.list_objecttype_versions(self.kwargs["submission_uuid"])


class TargetPathsListView(views.APIView):
authentication_classes = (authentication.SessionAuthentication,)
permission_classes = (permissions.IsAdminUser,)

@extend_schema(
request=TargetPathsInputSerializer, responses={200: TargetPathsSerializer}
)
def post(self, request: Request, *args: Any, **kwargs: Any):
input_serializer = TargetPathsInputSerializer(data=request.data)
input_serializer.is_valid(raise_exception=True)

# Regex taken from django.urls.converters.UUIDConverter
match = re.search(
r"([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\/?$",
input_serializer.validated_data["objecttype_url"],
)
if not match:
raise exceptions.ValidationError(
detail={"objecttype_url": _("Invalid URL.")}
)

objecttype_uuid = match.group()

with get_objecttypes_client() as client:

json_schema = client.get_objecttype_version(
objecttype_uuid, input_serializer.validated_data["objecttype_version"]
)["jsonSchema"]

return_data = [
{
"target_path": json_path.segments,
"is_required": json_path.required,
"json_schema": json_schema,
}
for json_path, json_schema in iter_json_schema_paths(
json_schema, fail_fast=False
)
if not isinstance(json_schema, InvalidReference)
if json_schema_matches(
variable_schema=input_serializer.validated_data["variable_json_schema"],
target_schema=json_schema,
)
]

output_serializer = TargetPathsSerializer(many=True, instance=return_data)
return Response(data=output_serializer.data)
42 changes: 39 additions & 3 deletions src/openforms/registrations/contrib/objects_api/json_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,18 @@ def startswith(self, other: JsonSchemaPath | list[str], /) -> bool:

@overload
def iter_json_schema_paths(
json_schema: ObjectSchema, fail_fast: Literal[False]
json_schema: ObjectSchema, *, fail_fast: Literal[False]
) -> Iterator[tuple[JsonSchemaPath, ObjectSchema | InvalidReference]]: ...


@overload
def iter_json_schema_paths(
json_schema: ObjectSchema, fail_fast: Literal[True] = ...
json_schema: ObjectSchema, *, fail_fast: Literal[True] = ...
) -> Iterator[tuple[JsonSchemaPath, ObjectSchema]]: ...


def iter_json_schema_paths(
json_schema: ObjectSchema, fail_fast: bool = True
json_schema: ObjectSchema, *, fail_fast: bool = True
) -> Iterator[tuple[JsonSchemaPath, ObjectSchema | InvalidReference]]:
"""Recursively iterate over the JSON Schema paths, resolving references if required.
Expand Down Expand Up @@ -168,3 +168,39 @@ def get_missing_required_paths(
missing_paths.append(r_path.segments)

return missing_paths


def json_schema_matches(
*, variable_schema: ObjectSchema, target_schema: ObjectSchema
) -> bool:
"""Return whether the deduced JSON Schema of a variable is compatible with the target object (sub) JSON Schema.
In other terms, this determines whether the variable can be mapped to a specific location. Currently,
only a limited subset of features is supported. For instance, ``format`` constraints are supported
if the type is a string, however no inspection is done on ``properties`` if it is an object.
"""
if "type" not in target_schema:
return True

if "type" not in variable_schema:
# 'type' is in target but not in variable
return False

target_types: str | list[str] = target_schema["type"]
if not isinstance(target_types, list):
target_types = [target_types]

variable_types: str | list[str] = variable_schema["type"]
if not isinstance(variable_types, list):
variable_types = [variable_types]

if not set(variable_types).issubset(target_types):
return False

if "string" in target_types and (target_format := target_schema.get("format")):
variable_format = variable_schema.get("format")
if variable_format is None:
return False
return variable_format == target_format

return True
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
interactions:
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate, br
Authorization:
- Token 171be5abaf41e7856b423ad513df1ef8f867ff48
Connection:
- keep-alive
User-Agent:
- python-requests/2.31.0
method: GET
uri: http://localhost:8001/api/v2/objecttypes/39da819c-ac6c-4037-ae2b-6bfc39f6564b/versions
response:
body:
string: '{"count":0,"next":null,"previous":null,"results":[]}'
headers:
Allow:
- GET, POST, HEAD, OPTIONS
Content-Length:
- '52'
Content-Type:
- application/json
Referrer-Policy:
- same-origin
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- DENY
status:
code: 200
message: OK
version: 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
interactions:
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate, br
Authorization:
- Token 171be5abaf41e7856b423ad513df1ef8f867ff48
Connection:
- keep-alive
User-Agent:
- python-requests/2.31.0
method: GET
uri: http://localhost:8001/api/v2/objecttypes/3edfdaf7-f469-470b-a391-bb7ea015bd6f/versions
response:
body:
string: '{"count":1,"next":null,"previous":null,"results":[{"url":"http://localhost:8001/api/v2/objecttypes/3edfdaf7-f469-470b-a391-bb7ea015bd6f/versions/1","version":1,"objectType":"http://localhost:8001/api/v2/objecttypes/3edfdaf7-f469-470b-a391-bb7ea015bd6f","status":"published","jsonSchema":{"$id":"https://example.com/tree.schema.json","type":"object","title":"Tree","$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"height":{"type":"integer","description":"The
height of the tree."}}},"createdAt":"2024-02-08","modifiedAt":"2024-02-08","publishedAt":"2024-02-08"}]}'
headers:
Allow:
- GET, POST, HEAD, OPTIONS
Content-Length:
- '585'
Content-Type:
- application/json
Referrer-Policy:
- same-origin
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- DENY
status:
code: 200
message: OK
version: 1
Loading

0 comments on commit f4d3748

Please sign in to comment.