Source code for betty.openapi
"""
Provide the OpenAPI specification.
"""
from pathlib import Path
from typing import Self
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
[docs]
class Specification:
"""
Build OpenAPI specifications.
"""
[docs]
def __init__(self, project: Project):
self._project = project
[docs]
async def build(self) -> DumpMapping[Dump]:
"""
Build the OpenAPI specification.
"""
specification: DumpMapping[Dump] = {
"openapi": "3.1.0",
"servers": [
{
"url": self._project.static_url_generator.generate(
"/", absolute=True
),
}
],
"info": {
"title": "Betty",
"version": await about.version_label(),
},
"paths": {},
"components": {
"responses": {
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": ProjectSchema.def_url(
self._project, "errorResponse"
),
},
},
},
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": ProjectSchema.def_url(
self._project, "errorResponse"
),
},
},
},
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": 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": 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": 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": 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
[docs]
class SpecificationSchema(FileBasedSchema):
"""
The OpenAPI Specification schema.
"""
[docs]
@classmethod
async def new(cls) -> Self:
"""
Create a new instance.
"""
return await cls.new_for(
Path(__file__).parent / "json" / "schemas" / "openapi-specification.json",
name="openApiSpecification",
)