Source code for gemseo.core.grammars.json_schema

# Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License version 3 as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
"""JSON schema handler."""
from __future__ import annotations

from collections import abc
from typing import Any
from typing import Iterator

from genson import SchemaBuilder
from genson.schema.strategies import Object


class _MergeRequiredStrategy(Object):
    """A genson Object with a modified required attribute handling.

    Genson Object does not merge the required attribute on purpose.
    See :ref:`https://github.com/wolverdude/GenSON#genson`.
    This class will merge the required attributes.
    """

    # Do not merge the name and id properties.
    KEYWORDS = Object.KEYWORDS + ("name", "id")

    def add_schema(self, schema) -> None:
        """Add a schema and merge the required attribute.

        Args:
            schema: A schema to be added.
        """
        if "required" not in schema or self._required is None:
            super().add_schema(schema)
        else:
            # Backup the current required before updating it with the new ones.
            required = set(self._required)
            super().add_schema(schema)
            self._required = required | set(schema["required"])

    def add_object(self, obj: Any) -> None:
        """Add an object and merge the required attribute.

        Args:
            obj: An object to be added.
        """
        if self._required is None:
            super().add_object(obj)
        else:
            # Backup the current required before updating it with the new ones.
            required = set(self._required)
            super().add_object(obj)
            self._required = required | set(obj.keys())


class _MultipleMeta(type(abc.Mapping), type(SchemaBuilder)):
    """Required base class for inheriting from multiple classes with meta classes."""


[docs]class MutableMappingSchemaBuilder(abc.Mapping, SchemaBuilder, metaclass=_MultipleMeta): """A mutable genson SchemaBuilder with a dictionary-like interface. The :class:`SchemaBuilder` does not provide a way to mutate directly the properties of a schema (these are stored deeply). For ease of usage, this class brings the properties closer to the surface, and the mutability is only provided by the ability to delete a property. """ EXTRA_STRATEGIES = (_MergeRequiredStrategy,) def __getitem__(self, key: str) -> dict[str, Any]: # Immutable workaround because self.properties is a defaultdict. if key in self.properties: return self.properties[key] else: raise KeyError(key) def __iter__(self) -> Iterator[str]: return iter(self.properties) def __len__(self) -> int: return len(self.properties) def __delitem__(self, key) -> None: del self.properties[key] if key in self.required: self.required.remove(key) @property def properties(self) -> dict[str, Any]: """Return the properties. Returns: The existing properties, otherwise an empty dictionary. """ try: return self._root_node._active_strategies[0]._properties except (AttributeError, IndexError): return {} @property def required(self) -> set[str]: """Return the required properties. Returns: The required properties, otherwise an empty set. """ try: required = self._root_node._active_strategies[0]._required except (AttributeError, IndexError): return set() if required is None: return set() return required