"""
Provide `JSON-LD <https://json-ld.org/>`_ utilities.
"""
from __future__ import annotations
from abc import abstractmethod, ABC
from collections.abc import MutableSequence
from inspect import getmembers
from pathlib import Path
from typing import TYPE_CHECKING, cast, Self, Generic, final
from typing_extensions import TypeVar, override
from betty.json.schema import FileBasedSchema, Schema, Object
from betty.serde.dump import DumpMapping, Dump
from betty.string import snake_case_to_lower_camel_case
if TYPE_CHECKING:
from betty.project import Project
from betty.ancestry.link import Link
_T = TypeVar("_T")
_SchemaTypeT = TypeVar("_SchemaTypeT", bound=Schema, default=Schema, covariant=True)
_DumpT = TypeVar("_DumpT", bound=Dump, default=Dump)
[docs]
async def dump_schema(
project: Project,
dump: DumpMapping[Dump],
linked_data_dumpable: LinkedDataDumpable[Object, DumpMapping[Dump]],
) -> None:
"""
Add the $schema item to a JSON-LD dump.
"""
from betty.project import ProjectSchema
schema = await linked_data_dumpable.linked_data_schema(project)
if schema.def_name:
dump["$schema"] = await ProjectSchema.def_url(project, schema.def_name)
[docs]
class LinkedDataDumpable(Generic[_SchemaTypeT, _DumpT]):
"""
Describe an object that can be dumped to linked data.
"""
[docs]
@classmethod
@abstractmethod
async def linked_data_schema(cls, project: Project) -> _SchemaTypeT:
"""
Define the `JSON Schema <https://json-schema.org/>`_ for :py:meth:`betty.json.linked_data.LinkedDataDumpable.dump_linked_data`.
"""
pass
[docs]
@abstractmethod
async def dump_linked_data(self, project: Project) -> _DumpT:
"""
Dump this instance to `JSON-LD <https://json-ld.org/>`_.
"""
pass
[docs]
class JsonLdObject(Object):
"""
A JSON Schema for an object with JSON-LD.
"""
[docs]
def __init__(
self,
json_ld_schema: JsonLdSchema,
*,
def_name: str | None = None,
title: str | None = None,
description: str | None = None,
):
super().__init__(
def_name=def_name,
title=title,
description=description,
)
self._schema["allOf"] = [json_ld_schema.embed(self)]
[docs]
class LinkedDataDumpableJsonLdObject(
LinkedDataDumpable[JsonLdObject, DumpMapping[Dump]], ABC
):
"""
A :py:class:`betty.json.linked_data.LinkedDataDumpable` implementation for object/mapping data.
This is helpful when working with diamond class hierarchies where parent classes that may not be the root class want
to make changes to the linked data, and expect an :py:class`betty.json.schema.Object` schema and a
:py:type:`betty.serde.dump.DumpMapping` dump.
"""
[docs]
@override
@classmethod
async def linked_data_schema(cls, project: Project) -> JsonLdObject:
schema = JsonLdObject(await JsonLdSchema.new())
for attr_name, class_attr_value in getmembers(cls):
if isinstance(class_attr_value, LinkedDataDumpableProvider):
linked_data_dumpable = class_attr_value
schema.add_property(
snake_case_to_lower_camel_case(attr_name),
await linked_data_dumpable.linked_data_schema_for(project),
True,
)
return schema
[docs]
@override
async def dump_linked_data(self, project: Project) -> DumpMapping[Dump]:
dump: DumpMapping[Dump] = {}
await dump_schema(project, dump, self)
for attr_name, class_attr_value in getmembers(type(self)):
if isinstance(class_attr_value, LinkedDataDumpableProvider):
dump[
snake_case_to_lower_camel_case(attr_name)
] = await class_attr_value.dump_linked_data_for(project, self)
return dump
[docs]
class LinkedDataDumpableProvider(Generic[_T, _SchemaTypeT, _DumpT], ABC):
"""
Provide linked data for instances of a target type.
"""
[docs]
@abstractmethod
async def linked_data_schema_for(self, project: Project) -> _SchemaTypeT:
"""
Define the `JSON Schema <https://json-schema.org/>`_ for :py:meth:`betty.json.linked_data.LinkedDataDumpableProvider.dump_linked_data_for`.
"""
pass
[docs]
@abstractmethod
async def dump_linked_data_for(self, project: Project, target: _T) -> _DumpT:
"""
Dump the given target to `JSON-LD <https://json-ld.org/>`_.
"""
pass
[docs]
def dump_context(dump: DumpMapping[Dump], **context_definitions: str) -> None:
"""
Add one or more contexts to a dump.
"""
context_dump = cast(DumpMapping[Dump], dump.setdefault("@context", {}))
for key, context_definition in context_definitions.items():
context_dump[key] = context_definition
[docs]
async def dump_link(dump: DumpMapping[Dump], project: Project, *links: Link) -> None:
"""
Add one or more links to a dump.
"""
link_dump = cast(MutableSequence[DumpMapping[Dump]], dump.setdefault("links", []))
for link in links:
link_dump.append(await link.dump_linked_data(project))
[docs]
@final
class JsonLdSchema(FileBasedSchema):
"""
A `JSON-LD <https://json-ld.org/>`_ JSON Schema reference.
"""
[docs]
@classmethod
async def new(cls) -> Self:
"""
Create a new instance.
"""
return await cls.new_for(
Path(__file__).parent / "schemas" / "json-ld.json",
def_name="jsonLd",
title="JSON-LD",
)