diff --git a/setup.py b/setup.py
index 6893746cd7..49a9a2b615 100644
--- a/setup.py
+++ b/setup.py
@@ -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="""
diff --git a/src/plone/restapi/configure.zcml b/src/plone/restapi/configure.zcml
index 2f79ec0c7a..7b0d123259 100644
--- a/src/plone/restapi/configure.zcml
+++ b/src/plone/restapi/configure.zcml
@@ -23,6 +23,15 @@
+
+
+
diff --git a/src/plone/restapi/services/content/get.py b/src/plone/restapi/services/content/get.py
index 5ab08d45c9..ae9dde331c 100644
--- a/src/plone/restapi/services/content/get.py
+++ b/src/plone/restapi/services/content/get.py
@@ -1,11 +1,84 @@
from plone.restapi.interfaces import ISerializeToJson
from plone.restapi.services import Service
from zope.component import queryMultiAdapter
+from zope.publisher.interfaces.browser import IBrowserRequest
+from plone.app.customerize import registration
+from zope.component import getUtilitiesFor
+from plone.dexterity.interfaces import IDexterityFTI
+from plone import api
+from zope.globalrequest import getRequest
class ContentGet(Service):
"""Returns a serialized content object."""
+ @classmethod
+ def __restapi_doc_component_schemas_extension__(cls):
+ doc = super(ContentGet, cls).__restapi_doc_component_schemas_extension__()
+
+ # recupero le interfacce per cui il service รจ stato registrato
+ services = [
+ i
+ for i in registration.getViews(IBrowserRequest)
+ if getattr(i.factory, "__name__", "") == cls.__name__
+ ]
+ required_interfaces = [x.required[0] for x in services]
+
+ # recupero i tipi di contenuto che implementano quelle interfacce
+ ftis = []
+ for name, fti in getUtilitiesFor(IDexterityFTI):
+ if name == "Plone Site":
+ instance = api.portal.get()
+ else:
+ try:
+ instance = fti.constructInstance(api.portal.get(), id=name)
+ except Exception:
+ instance = getattr(api.portal.get(), "name", None)
+
+ for interface in required_interfaces:
+ if interface.providedBy(instance):
+ ftis.append((fti, instance))
+ break
+
+ # chiamo il serializer registrato per ISerializeToJson per ogni tipo
+ # di contenuto
+ definition = {}
+ for fti in ftis:
+ serializer = queryMultiAdapter((fti[1], getRequest()), ISerializeToJson)
+ if method := getattr(serializer, "__restapi_doc_component_schema__", None):
+ definition.update(**method(fti[1], getRequest()))
+
+ return definition
+
+ @classmethod
+ def __restapi_doc__(cls):
+ return {
+ "get": {
+ "summary": "Content",
+ "description": "Content data",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "$ContextType",
+ }
+ }
+ },
+ },
+ "501": {
+ "description": "ServerError",
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/ErrorResponse"}
+ }
+ },
+ },
+ },
+ }
+ }
+
def reply(self):
serializer = queryMultiAdapter((self.context, self.request), ISerializeToJson)
diff --git a/src/plone/restapi/services/model.py b/src/plone/restapi/services/model.py
new file mode 100644
index 0000000000..647ace26ea
--- /dev/null
+++ b/src/plone/restapi/services/model.py
@@ -0,0 +1,12 @@
+from pydantic import BaseModel, Field
+
+
+class ErrorDefinitionDTO(BaseModel):
+ type: str = Field(..., description="The type of error")
+ message: str = Field(
+ ..., description="A human-readable message describing the error"
+ )
+
+
+class ErrorOutputDTO(BaseModel):
+ error: ErrorDefinitionDTO
diff --git a/src/plone/restapi/services/swagger/__init__.py b/src/plone/restapi/services/swagger/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/plone/restapi/services/swagger/configure.zcml b/src/plone/restapi/services/swagger/configure.zcml
new file mode 100644
index 0000000000..50a0f0c378
--- /dev/null
+++ b/src/plone/restapi/services/swagger/configure.zcml
@@ -0,0 +1,14 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/plone/restapi/services/swagger/get.py b/src/plone/restapi/services/swagger/get.py
new file mode 100644
index 0000000000..39a2bd17ce
--- /dev/null
+++ b/src/plone/restapi/services/swagger/get.py
@@ -0,0 +1,177 @@
+# -*- coding: utf-8 -*-
+from plone import api
+from plone.app.customerize import registration
+from plone.restapi.services import Service
+from plone.restapi.interfaces import ISerializeToJson
+from Products.CMFCore.utils import getToolByName
+from zope.component import getMultiAdapter
+from zope.globalrequest import getRequest
+from zope.publisher.interfaces.browser import IBrowserRequest
+
+import importlib
+import logging
+import transaction
+
+logger = logging.getLogger("Plone")
+
+
+class SwaggerDefinitions(Service):
+
+ def inject_schemas(self, doc, schemas):
+ def inject(d):
+ for k, v in d.items():
+ if isinstance(v, dict):
+ inject(v)
+ else:
+ if k == "$ref" and "$" in v:
+ d[k] = schemas[v]
+
+ inject(doc)
+
+ def get_services_by_ct(self):
+ portal_types = getToolByName(api.portal.get(), "portal_types")
+ services_by_ct = {}
+ services = [
+ i
+ for i in registration.getViews(IBrowserRequest)
+ if "plone.rest.zcml" in getattr(i.factory, "__module__", "")
+ ]
+
+ for portal_type in portal_types.listTypeInfo():
+ portal_type_services = []
+
+ if not getattr(portal_type, "klass", None):
+ continue
+
+ module_name = ".".join(getattr(portal_type, "klass", ".").split(".")[:-1])
+ module = importlib.import_module(module_name)
+ klass = getattr(
+ module, getattr(portal_type, "klass", ".").split(".")[-1], None
+ )
+
+ for service in services:
+ if service.required[0].implementedBy(klass):
+ portal_type_services.append(service)
+
+ if portal_type_services:
+ services_by_ct[portal_type.id.replace(" ", "")] = portal_type_services
+
+ return services_by_ct
+
+ def get_doc_by_service(self, service):
+ # Supposed to be extended later
+ doc = getattr(service.factory, "__restapi_doc__", None)
+ if callable(doc):
+ return doc()
+
+ def get_doc_schemas_by_service(self, service):
+ doc = getattr(
+ service.factory, "__restapi_doc_component_schemas_extension__", None
+ )
+ if callable(doc):
+ return doc()
+
+ def reply(self):
+ """
+ Service to define an OpenApi definition for api
+ """
+
+ openapi_doc_boilerplate = {
+ "openapi": "3.0.0",
+ "info": {
+ "version": "1.0.0",
+ "title": api.portal.get().Title(),
+ "description": f"RESTApi description for a {api.portal.get().Title()} site", # noqa
+ },
+ "servers": [
+ {
+ "url": "http://localhost:8080/",
+ "description": "Site API",
+ "x-sandbox": False,
+ "x-healthCheck": {
+ "interval": "300",
+ "url": "https://demo.plone.org",
+ "timeout": "15",
+ },
+ }
+ ],
+ "components": {
+ "securitySchemes": {
+ "bearerAuth": {
+ "type": "http",
+ "scheme": "bearer",
+ "bearerFormat": "JWT",
+ }
+ },
+ "schemas": {
+ "ContentType": {
+ "type": "object",
+ "properties": {
+ "error": {
+ "type": "object",
+ "properties": {
+ "title": {
+ "type": "string",
+ "description": "Title",
+ },
+ },
+ }
+ },
+ }
+ },
+ },
+ "paths": {},
+ "security": [{"bearerAuth": []}],
+ }
+
+ with api.env.adopt_roles(["Manager"]):
+ for ct, services in self.get_services_by_ct().items():
+ doc_template = {}
+ doc_template["parameters"] = []
+
+ path_parameter = {
+ "in": "path",
+ "name": ct,
+ "required": True,
+ "description": f"Path to the {ct}",
+ "schema": {
+ "type": "string",
+ "example": "",
+ },
+ }
+
+ doc_template["parameters"].append(path_parameter)
+
+ for service in services:
+ service_doc = self.get_doc_by_service(service)
+
+ if not service_doc:
+ logger.warning(
+ f"No documentation found for /{ct}/{'@' + service.name.split('@')[-1]}" # noqa
+ )
+ continue
+
+ doc = {**doc_template, **service_doc}
+
+ api_name = (
+ len(service.name.split("@")) > 1
+ and "@" + service.name.split("@")[1]
+ or ""
+ )
+
+ openapi_doc_boilerplate["paths"][f"/{'{' + ct + '}'}/{api_name}"] = doc
+
+ # Extend the components
+ component = self.get_doc_schemas_by_service(service)
+
+ if component:
+ openapi_doc_boilerplate["components"]["schemas"].update(component)
+
+ self.inject_schemas(
+ doc,
+ schemas={"$ContextType": f"#/components/schemas/{ct}"},
+ )
+
+ transaction.abort()
+
+ return openapi_doc_boilerplate