Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Openapi schema generation #1790

Draft
wants to merge 21 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ def read(filename):
"Products.CMFPlone>=5.2",
"PyJWT>=1.7.0",
"pytz",
"pyyaml",
"pyDantic",
],
extras_require={"test": TEST_REQUIRES},
entry_points="""
Expand Down
9 changes: 9 additions & 0 deletions src/plone/restapi/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@
<include package="plone.rest" />
<include package="plone.schema" />

<plone:CORSPolicy
allow_origin="*"
allow_methods="DELETE,GET,OPTIONS,PATCH,POST,PUT"
allow_credentials="true"
expose_headers="Content-Length,X-My-Header"
allow_headers="Accept,Authorization,Content-Type,X-Custom-Header"
max_age="3600"
/>

<include
package="plone.app.caching"
zcml:condition="installed plone.app.caching"
Expand Down
135 changes: 120 additions & 15 deletions src/plone/restapi/serializer/dxcontent.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from plone.restapi.serializer.nextprev import NextPrevious
from plone.restapi.services.locking import lock_info
from plone.restapi.serializer.utils import get_portal_type_title
from plone import api
from plone.rfc822.interfaces import IPrimaryFieldInfo
from plone.supermodel.utils import mergedTaggedValueDict
from Products.CMFCore.utils import getToolByName
Expand Down Expand Up @@ -55,6 +56,77 @@ def get_allow_discussion_value(context, request, result):
@implementer(ISerializeToJson)
@adapter(IDexterityContent, Interface)
class SerializeToJson:
@classmethod
def __restapi_doc_component_schema__(cls, context, request):
fields_adapter = []
portal_types = getToolByName(api.portal.get(), "portal_types")
for schema in iterSchemata(context):
for name, field in getFields(schema).items():
fields_adapter.append(
(
name,
queryMultiAdapter((field, context, request), IFieldSerializer),
)
)

schema = {}
for name, field in fields_adapter:
method = getattr(field, "__restapi_schema_json_type__", None)

if callable(method):
schema[name] = field.__restapi_schema_json_type__()
else:
schema[name] = {type: "string"}

return {
"PreviousItemSchema": {
"type": "string",
},
"NextItemSchema": {
"type": "string",
},
"WorkingCopy": {"type": "string"},
"WorkingCopyOf": {"type": "string"},
"LockInfo": {"type": "string"},
"ExpandableItems": {"type": "string"},
"TargetUrl": {"type": "string"},
"ParentShema": {"type": "string"},
portal_types.get(context.portal_type).id.replace(" ", ""): {
"type": "object",
"properties": {
"@id": {"type": "string"},
"id": {"type": "string"},
"@type": {"type": "string"},
"type_title": {"type": "string"},
"parent": {
"items": {"$ref": "#/components/schemas/ParentShema"},
},
"created": {"type": "string"},
"modified": {"type": "string"},
"review_state": {"type": "string"},
"UID": {"type": "string"},
"version": {"type": "string"},
"layout": {"type": "string"},
"is_folderish": {"type": "boolean"},
"previous_item": {
"type": "array",
"items": {"$ref": "#/components/schemas/PreviousItemSchema"},
},
"next_item": {
"type": "array",
"items": {"$ref": "#/components/schemas/NextItemSchema"},
},
"working_copy": {"$ref": "#/components/schemas/WorkingCopy"},
"working_copy_of": {"$ref": "#/components/schemas/WorkingCopyOf"},
"lock": {"$ref": "#/components/schemas/LockInfo"},
"@components": {"$ref": "#/components/schemas/ExpandableItems"},
**schema,
"targetUrl": {"$ref": "#/components/schemas/TargetUrl"},
"allow_discussion": {"type": "boolean"},
},
},
}

def __init__(self, context, request):
self.context = context
self.request = request
Expand Down Expand Up @@ -97,7 +169,10 @@ def __call__(self, version=None, include_items=True):
try:
nextprevious = NextPrevious(obj)
result.update(
{"previous_item": nextprevious.previous, "next_item": nextprevious.next}
{
"previous_item": nextprevious.previous,
"next_item": nextprevious.next,
}
)
except ValueError:
# If we're serializing an old version that was renamed or moved,
Expand All @@ -117,20 +192,9 @@ def __call__(self, version=None, include_items=True):
# Insert expandable elements
result.update(expandable_elements(self.context, self.request))

# Insert field values
for schema in iterSchemata(self.context):
read_permissions = mergedTaggedValueDict(schema, READ_PERMISSIONS_KEY)

for name, field in getFields(schema).items():
if not self.check_permission(read_permissions.get(name), obj):
continue

# serialize the field
serializer = queryMultiAdapter(
(field, obj, self.request), IFieldSerializer
)
value = serializer()
result[json_compatible(name)] = value
for name, serializer in self._get_context_field_serializers(obj):
value = serializer()
result[json_compatible(name)] = value

target_url = getMultiAdapter(
(self.context, self.request), IObjectPrimaryFieldTarget
Expand All @@ -142,6 +206,21 @@ def __call__(self, version=None, include_items=True):

return result

def _get_context_field_serializers(self, obj):
# Insert field values
for schema in iterSchemata(self.context):
read_permissions = mergedTaggedValueDict(schema, READ_PERMISSIONS_KEY)

for name, field in getFields(schema).items():
if not self.check_permission(read_permissions.get(name), obj):
continue

# serialize the field
yield (
name,
queryMultiAdapter((field, obj, self.request), IFieldSerializer),
)

def _get_workflow_state(self, obj):
wftool = getToolByName(self.context, "portal_workflow")
review_state = wftool.getInfoFor(ob=obj, name="review_state", default=None)
Expand All @@ -166,6 +245,32 @@ def check_permission(self, permission_name, obj):
@implementer(ISerializeToJson)
@adapter(IDexterityContainer, Interface)
class SerializeFolderToJson(SerializeToJson):
@classmethod
def __restapi_doc_component_schema__(cls, context, request):
result = super(cls, SerializeFolderToJson).__restapi_doc_component_schema__(
context, request
)
# portal_types = getToolByName(api.portal.get(), "portal_types")

# ct: dict = result[portal_types.get(context.portal_type).id.replace(" ", "")]

result.update({"BrainItem": {"type": "string"}})
# ct.update(
# {
# "items": {
# "type": "array",
# "items": {
# "$ref": "#/components/schemas/BrainItem",
# "is_folderish": {"type": "boolean"},
# "items_total": {"type": "integer"},
# "batching": {"type": "string"}
# },
# },
# }
# )

return result

def _build_query(self):
path = "/".join(self.context.getPhysicalPath())
query = {
Expand Down
5 changes: 5 additions & 0 deletions src/plone/restapi/serializer/dxfields.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from zope.schema.interfaces import IField
from zope.schema.interfaces import ITextLine
from zope.schema.interfaces import IVocabularyTokenized
from zope.schema import _bootstrapfields

import logging

Expand All @@ -36,6 +37,10 @@ def __init__(self, field, context, request):
self.request = request
self.field = field

def __restapi_schema_json_type__(self):
type = {str: "string", bool: "boolean", int: "integer"}.get(self.field._type)
return {"type": type or "string"}

def __call__(self):
return json_compatible(self.get_value())

Expand Down
96 changes: 80 additions & 16 deletions src/plone/restapi/serializer/site.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,87 @@
@implementer(ISerializeToJson)
@adapter(IPloneSiteRoot, Interface)
class SerializeSiteRootToJson:
@classmethod
def __restapi_doc_component_schema__(cls, context, request):
fields_adapter = []
for schema in iterSchemata(context):
for name, field in getFields(schema).items():
fields_adapter.append(
(
name,
queryMultiAdapter((field, context, request), IFieldSerializer),
)
)

schema = {}
for name, field in fields_adapter:
method = getattr(field, "__restapi_schema_json_type__", None)

if callable(method):
schema[name] = field.__restapi_schema_json_type__()
else:
schema[name] = {type: "string"}

return {
"ParentShema": {"type": "string"},
"LockInfo": {"type": "string"},
"Block": {"type": "string"},
"BlocksLayout": {"type": "string"},
"PloneSite": {
"type": "object",
"properties": {
"@id": {"type": "string"},
"id": {"type": "string"},
"@type": {"type": "string"},
"type_title": {"type": "string"},
"title": {"type": "string"},
"parent": {
"items": {"$ref": "#/components/schemas/ParentShema"},
},
"is_folderish": {"type": "boolean"},
"description": {"type": "string"},
"review_state": {"type": "string"},
**schema,
"lock": {"$ref": "#/components/schemas/LockInfo"},
"blocks": {
"type": "array",
"items": {"$ref": "#/components/schemas/Block"},
},
"blocks_layout": {
"type": "object",
"items": {"$ref": "#/components/schemas/BlocksLayout"},
},
"items_total": {"type": "integer"},
"batching": {"type": "string"},
"items": {
"type": "array",
"items": {"$ref": "#/components/schemas/BrainItem"},
},
"allow_discussion": {"type": "boolean"},
},
},
}

def __init__(self, context, request):
self.context = context
self.request = request

def _get_context_field_serializers(self):
for schema in iterSchemata(self.context):
read_permissions = mergedTaggedValueDict(schema, READ_PERMISSIONS_KEY)

for name, field in getFields(schema).items():
if not self.check_permission(read_permissions.get(name), self.context):
continue

# serialize the field
yield (
name,
queryMultiAdapter(
(field, self.context, self.request), IFieldSerializer
),
)

def _build_query(self):
path = "/".join(self.context.getPhysicalPath())
query = {
Expand Down Expand Up @@ -80,22 +157,9 @@ def __call__(self, version=None):
ob=self.context, name="review_state", default=None
)

# Insert Plone Site DX root field values
for schema in iterSchemata(self.context):
read_permissions = mergedTaggedValueDict(schema, READ_PERMISSIONS_KEY)

for name, field in getFields(schema).items():
if not self.check_permission(
read_permissions.get(name), self.context
):
continue

# serialize the field
serializer = queryMultiAdapter(
(field, self.context, self.request), IFieldSerializer
)
value = serializer()
result[json_compatible(name)] = value
for name, serializer in self._get_context_field_serializers():
value = serializer()
result[json_compatible(name)] = value

# Insert locking information
result.update({"lock": lock_info(self.context)})
Expand Down
9 changes: 9 additions & 0 deletions src/plone/restapi/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from plone.restapi.permissions import UseRESTAPI
from zExceptions import Unauthorized

from plone.restapi.services.model import ErrorOutputDTO, ErrorDefinitionDTO

import json


Expand All @@ -12,6 +14,13 @@
class Service(RestService):
"""Base class for Plone REST API services"""

@classmethod
def __restapi_doc_component_schemas_extension__(cls):
return {
"ErrorDefinitionDTO": ErrorDefinitionDTO.schema(),
"ErrorResponse": ErrorOutputDTO.schema(ref_template="#/components/schemas/{model}") # noqa
}

content_type = "application/json"

def render(self):
Expand Down
1 change: 0 additions & 1 deletion src/plone/restapi/services/auth/configure.zcml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:plone="http://namespaces.plone.org/plone"
xmlns:zcml="http://namespaces.zope.org/zcml"
>

<plone:service
Expand Down
Loading
Loading