diff --git a/build.py b/build.py index 3dc3864..1cba277 100644 --- a/build.py +++ b/build.py @@ -120,7 +120,7 @@ env = Environment(loader=FileSystemLoader(os.path.dirname(os.path.realpath(__file__))), autoescape=select_autoescape()) context = { - "version": "0.5.0", + "version": "0.5.1.dev.0", } with open("target/pyproject.toml", "w") as fp: diff --git a/pipeline/src/base.py b/pipeline/src/base.py index 0ce0677..19c0d25 100644 --- a/pipeline/src/base.py +++ b/pipeline/src/base.py @@ -9,6 +9,7 @@ from datetime import date, datetime from collections import defaultdict +from enum import Enum import json from typing import Union @@ -17,17 +18,31 @@ from .registry import Registry -def value_to_jsonld(value, include_empty_properties=True, embed_linked_nodes=True): +class LinkedNodeEmbedding(Enum): + ALWAYS = "always" + NEVER = "never" + IF_NECESSARY = "if necessary" + + +def value_to_jsonld(value, include_empty_properties=True, embed_linked_nodes=LinkedNodeEmbedding.ALWAYS): if isinstance(value, LinkedMetadata): - if embed_linked_nodes: + if embed_linked_nodes in (LinkedNodeEmbedding.ALWAYS, True): item = value.to_jsonld( with_context=False, include_empty_properties=include_empty_properties, embed_linked_nodes=embed_linked_nodes, ) - else: - if hasattr(value, "id") and value.id is None: + elif value.id is None: + if embed_linked_nodes == LinkedNodeEmbedding.IF_NECESSARY: + item = value.to_jsonld( + with_context=False, + include_empty_properties=include_empty_properties, + embed_linked_nodes=embed_linked_nodes, + ) + else: + assert embed_linked_nodes in (LinkedNodeEmbedding.NEVER, False) raise ValueError("Exporting as a stand-alone JSON-LD document requires @id to be defined.") + else: item = {"@id": value.id} elif isinstance(value, EmbeddedMetadata): item = value.to_jsonld( @@ -62,7 +77,12 @@ def has_property(self, name): return True return False - def to_jsonld(self, include_empty_properties=True, embed_linked_nodes=True, with_context=True): + def to_jsonld( + self, + include_empty_properties=True, + embed_linked_nodes=LinkedNodeEmbedding.ALWAYS, + with_context=True + ): """ Return a represention of this metadata node as a dictionary that can be directly serialized to JSON-LD. """ diff --git a/pipeline/src/collection.py b/pipeline/src/collection.py index d9e10b9..c762828 100644 --- a/pipeline/src/collection.py +++ b/pipeline/src/collection.py @@ -11,7 +11,7 @@ import json import os from .registry import lookup_type -from .base import Link +from .base import Link, LinkedNodeEmbedding DEFAULT_VERSION = "v5" @@ -149,7 +149,9 @@ def save(self, path, individual_files=False, include_empty_properties=False, gro "@context": data_context, "@graph": [ node.to_jsonld( - embed_linked_nodes=False, include_empty_properties=include_empty_properties, with_context=False + embed_linked_nodes=LinkedNodeEmbedding.NEVER, + include_empty_properties=include_empty_properties, + with_context=False ) for node in self ], @@ -179,7 +181,10 @@ def save(self, path, individual_files=False, include_empty_properties=False, gro else: file_path = os.path.join(path, f"{file_identifier}.jsonld") with open(file_path, "w") as fp: - data = node.to_jsonld(embed_linked_nodes=False, include_empty_properties=include_empty_properties) + data = node.to_jsonld( + embed_linked_nodes=LinkedNodeEmbedding.NEVER, + include_empty_properties=include_empty_properties + ) json.dump(data, fp, indent=2) output_paths.append(file_path) return output_paths diff --git a/pipeline/tests/test_collections.py b/pipeline/tests/test_collections.py index 8f5af96..6863266 100644 --- a/pipeline/tests/test_collections.py +++ b/pipeline/tests/test_collections.py @@ -6,6 +6,7 @@ import shutil import json +from openminds.base import LinkedNodeEmbedding from openminds.collection import Collection import openminds.latest.controlled_terms import openminds.latest.core as omcore @@ -45,8 +46,8 @@ def test_round_trip_single_file(): new_person = person break - p = person.to_jsonld(include_empty_properties=False, embed_linked_nodes=True) - np = new_person.to_jsonld(include_empty_properties=False, embed_linked_nodes=True) + p = person.to_jsonld(include_empty_properties=False, embed_linked_nodes=LinkedNodeEmbedding.ALWAYS) + np = new_person.to_jsonld(include_empty_properties=False, embed_linked_nodes=LinkedNodeEmbedding.ALWAYS) assert p == np @@ -72,8 +73,8 @@ def test_round_trip_multi_file(): new_person = person break - p = person.to_jsonld(include_empty_properties=False, embed_linked_nodes=True) - np = new_person.to_jsonld(include_empty_properties=False, embed_linked_nodes=True) + p = person.to_jsonld(include_empty_properties=False, embed_linked_nodes=LinkedNodeEmbedding.ALWAYS) + np = new_person.to_jsonld(include_empty_properties=False, embed_linked_nodes=LinkedNodeEmbedding.ALWAYS) assert p == np @@ -92,8 +93,8 @@ def test_round_trip_multi_file_group_by_schema(): new_person = person break - p = person.to_jsonld(include_empty_properties=False, embed_linked_nodes=True) - np = new_person.to_jsonld(include_empty_properties=False, embed_linked_nodes=True) + p = person.to_jsonld(include_empty_properties=False, embed_linked_nodes=LinkedNodeEmbedding.ALWAYS) + np = new_person.to_jsonld(include_empty_properties=False, embed_linked_nodes=LinkedNodeEmbedding.ALWAYS) assert p == np diff --git a/pipeline/tests/test_instantiation.py b/pipeline/tests/test_instantiation.py index bee2fef..723231a 100644 --- a/pipeline/tests/test_instantiation.py +++ b/pipeline/tests/test_instantiation.py @@ -7,7 +7,7 @@ import pytest -from openminds.base import Node, IRI, Link +from openminds.base import Node, IRI, Link, LinkedNodeEmbedding from utils import build_fake_node module_names = ( @@ -114,8 +114,74 @@ def test_link(): } assert my_dsv1.to_jsonld( include_empty_properties=False, - embed_linked_nodes=False + embed_linked_nodes=LinkedNodeEmbedding.NEVER ) == my_dsv2.to_jsonld( include_empty_properties=False, - embed_linked_nodes=False + embed_linked_nodes=LinkedNodeEmbedding.NEVER ) == expected + + +def test_linked_node_embedding(): + from openminds.v4.core import Organization, Person + from openminds.v4.core.actors.affiliation import Affiliation + + uni = Organization(full_name="University of Somewhere", id="_:001") + person_with_id = Person( + given_name="Ada", + family_name="Lovelace", + id="_:002", + affiliations=[Affiliation(member_of=uni)], + ) + person_without_id = Person( + given_name="Ada", + family_name="Lovelace", + affiliations=[Affiliation(member_of=uni)], + ) + + # ALWAYS: linked nodes are embedded inline + result = person_with_id.to_jsonld( + include_empty_properties=False, + embed_linked_nodes=LinkedNodeEmbedding.ALWAYS, + ) + affiliation = result["affiliation"][0] + assert affiliation["memberOf"]["@type"] == "https://openminds.om-i.org/types/Organization" + assert affiliation["memberOf"]["fullName"] == "University of Somewhere" + + # NEVER: linked nodes with id are replaced by {"@id": ...} + result = person_with_id.to_jsonld( + include_empty_properties=False, + embed_linked_nodes=LinkedNodeEmbedding.NEVER, + ) + affiliation = result["affiliation"][0] + assert affiliation["memberOf"] == {"@id": "_:001"} + + # NEVER: raises ValueError when a linked node has no id + uni_no_id = Organization(full_name="University of Nowhere") + person_with_unidentified_org = Person( + given_name="Ada", + family_name="Lovelace", + id="_:003", + affiliations=[Affiliation(member_of=uni_no_id)], + ) + with pytest.raises(ValueError, match="requires @id to be defined"): + person_with_unidentified_org.to_jsonld( + include_empty_properties=False, + embed_linked_nodes=LinkedNodeEmbedding.NEVER, + ) + + # IF_NECESSARY: linked nodes with id are replaced by {"@id": ...} + result = person_with_id.to_jsonld( + include_empty_properties=False, + embed_linked_nodes=LinkedNodeEmbedding.IF_NECESSARY, + ) + affiliation = result["affiliation"][0] + assert affiliation["memberOf"] == {"@id": "_:001"} + + # IF_NECESSARY: linked nodes without id are embedded inline + result = person_with_unidentified_org.to_jsonld( + include_empty_properties=False, + embed_linked_nodes=LinkedNodeEmbedding.IF_NECESSARY, + ) + affiliation = result["affiliation"][0] + assert affiliation["memberOf"]["@type"] == "https://openminds.om-i.org/types/Organization" + assert affiliation["memberOf"]["fullName"] == "University of Nowhere" diff --git a/pipeline/tests/test_regressions.py b/pipeline/tests/test_regressions.py index 06b1790..d1e6d16 100644 --- a/pipeline/tests/test_regressions.py +++ b/pipeline/tests/test_regressions.py @@ -5,6 +5,7 @@ import pytest from openminds import Collection, IRI +from openminds.base import LinkedNodeEmbedding import openminds.latest import openminds.v4 import openminds.v5 @@ -114,7 +115,11 @@ def test_issue0007a(om): om.core.Affiliation(member_of=uni2), ] - actual = person.to_jsonld(include_empty_properties=False, embed_linked_nodes=False, with_context=True) + actual = person.to_jsonld( + include_empty_properties=False, + embed_linked_nodes=LinkedNodeEmbedding.NEVER, + with_context=True + ) expected = { "@context": {"@vocab": "https://openminds.om-i.org/props/"}, "@id": "_:001", @@ -192,7 +197,11 @@ def test_issue0007b(om): om.core.Membership(member=person2) ] - actual = uni1.to_jsonld(include_empty_properties=False, embed_linked_nodes=False, with_context=True) + actual = uni1.to_jsonld( + include_empty_properties=False, + embed_linked_nodes=LinkedNodeEmbedding.NEVER, + with_context=True + ) expected = { "@context": {"@vocab": "https://openminds.om-i.org/props/"}, "@id": "_:002", @@ -273,7 +282,11 @@ def test_issue0008a(om): family_name="Professor", affiliations=[om.core.Affiliation(member_of=uni1, end_date=date(2023, 9, 30))], ) - actual = person.to_jsonld(include_empty_properties=False, embed_linked_nodes=False, with_context=True) + actual = person.to_jsonld( + include_empty_properties=False, + embed_linked_nodes=LinkedNodeEmbedding.NEVER, + with_context=True + ) expected = { "@context": {"@vocab": "https://openminds.om-i.org/props/"}, "@id": "_:002", @@ -308,7 +321,11 @@ def test_issue0008b(om): id="_:001", memberships=om.core.Membership(member=person, end_date=date(2023, 9, 30)) ) - actual = uni1.to_jsonld(include_empty_properties=False, embed_linked_nodes=False, with_context=True) + actual = uni1.to_jsonld( + include_empty_properties=False, + embed_linked_nodes=LinkedNodeEmbedding.NEVER, + with_context=True + ) expected = { '@context': {'@vocab': 'https://openminds.om-i.org/props/'}, '@id': '_:001',