betty.openapi - Betty documentation

Hide navigation sidebar
Hide table of contents sidebar
Skip to content
Back to top
Toggle table of contents sidebar

# Source code for betty.openapi

    """
    Provide the OpenAPI specification.
    """

    from pathlib import Path
    from typing import Self, final

    from betty import about, model
    from betty.json.schema import FileBasedSchema
    from betty.locale.localizer import DEFAULT_LOCALIZER
    from betty.model import UserFacingEntity
    from betty.project import Project, ProjectSchema
    from betty.serde.dump import DumpMapping, Dump
    from betty.string import kebab_case_to_lower_camel_case

    class Specification:
        """
        Build OpenAPI specifications.
        """

        def __init__(self, project: Project):
            self._project = project

        async def build(self) -> DumpMapping[Dump]:
            """
            Build the OpenAPI specification.
            """
            static_url_generator = await self._project.static_url_generator
            specification: DumpMapping[Dump] = {
                "openapi": "3.1.0",
                "servers": [
                    {
                        "url": static_url_generator.generate("/", absolute=True),
                    }
                ],
                "info": {
                    "title": "Betty",
                    "version": about.version_label(),
                },
                "paths": {},
                "components": {
                    "responses": {
                        "401": {
                            "description": "Unauthorized",
                            "content": {
                                "application/json": {
                                    "schema": {
                                        "$ref": await ProjectSchema.def_url(
                                            self._project, "errorResponse"
                                        ),
                                    },
                                },
                            },
                        },
                        "403": {
                            "description": "Forbidden",
                            "content": {
                                "application/json": {
                                    "schema": {
                                        "$ref": await ProjectSchema.def_url(
                                            self._project, "errorResponse"
                                        ),
                                    },
                                },
                            },
                        },
                        "404": {
                            "description": "Not found",
                            "content": {
                                "application/json": {
                                    "schema": {
                                        "$ref": await ProjectSchema.def_url(
                                            self._project, "errorResponse"
                                        ),
                                    },
                                },
                            },
                        },
                    },
                    "parameters": {
                        "id": {
                            "name": "id",
                            "in": "path",
                            "required": True,
                            "description": "The ID for the resource to retrieve.",
                            "schema": {
                                "type": "string",
                            },
                        },
                    },
                    "schemas": {
                        "betty": {
                            "$ref": await ProjectSchema.url(self._project),
                        },
                    },
                },
            }

            # Add entity operations.
            for entity_type in await model.ENTITY_TYPE_REPOSITORY.select(UserFacingEntity):
                await entity_type.linked_data_schema(self._project)
                if self._project.configuration.clean_urls:
                    collection_path = f"/{entity_type.plugin_id()}/"
                    single_path = f"/{entity_type.plugin_id()}/{{id}}/"
                else:
                    collection_path = f"/{entity_type.plugin_id()}/index.json"
                    single_path = f"/{entity_type.plugin_id()}/{{id}}/index.json"
                entity_type_label = entity_type.plugin_label().localize(DEFAULT_LOCALIZER)
                specification["paths"].update(  # type: ignore[union-attr]
                    {
                        collection_path: {
                            "get": {
                                "summary": f"Retrieve the collection of {entity_type_label} entities.",
                                "responses": {
                                    "200": {
                                        "description": f"The collection of {entity_type_label} entities.",
                                        "content": {
                                            "application/json": {
                                                "schema": {
                                                    "$ref": await ProjectSchema.def_url(
                                                        self._project,
                                                        f"{kebab_case_to_lower_camel_case(entity_type.plugin_id())}EntityCollectionResponse",
                                                    ),
                                                },
                                            },
                                        },
                                    },
                                },
                                "tags": [entity_type_label],
                            },
                        },
                        single_path: {
                            "get": {
                                "summary": f"Retrieve a single {entity_type_label} entity.",
                                "responses": {
                                    "200": {
                                        "description": f"The {entity_type_label} entity.",
                                        "content": {
                                            "application/json": {
                                                "schema": {
                                                    "$ref": await ProjectSchema.def_url(
                                                        self._project,
                                                        f"{kebab_case_to_lower_camel_case(entity_type.plugin_id())}Entity",
                                                    ),
                                                },
                                            },
                                        },
                                    },
                                },
                                "tags": [entity_type_label],
                            },
                        },
                    }
                )

            # Add default behavior to all requests.
            for path in specification["paths"]:  # type: ignore[union-attr]
                specification["paths"][path]["get"]["responses"].update(  # type: ignore[call-overload, index, union-attr]
                    {
                        "401": {
                            "$ref": "#/components/responses/401",
                        },
                        "403": {
                            "$ref": "#/components/responses/403",
                        },
                        "404": {
                            "$ref": "#/components/responses/404",
                        },
                    }
                )

            return specification

    @final
    class SpecificationSchema(FileBasedSchema):
        """
        The OpenAPI Specification schema.
        """

        @classmethod
        async def new(cls) -> Self:
            """
            Create a new instance.
            """
            return await cls.new_for(
                Path(__file__).parent / "json" / "schemas" / "openapi-specification.json",
                def_name="openApiSpecification",
            )